diff --git a/Cargo.toml b/Cargo.toml index c6dd76738a655..ae4637c77c9f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -282,6 +282,7 @@ bevy_internal = { path = "crates/bevy_internal", version = "0.12.0", default-fea [dev-dependencies] rand = "0.8.0" ron = "0.8.0" +flate2 = "1.0" serde = { version = "1", features = ["derive"] } bytemuck = "1.7" # Needed to poll Task examples @@ -1077,6 +1078,17 @@ description = "Demonstrates various methods to load assets" category = "Assets" wasm = false +[[example]] +name = "asset_decompression" +path = "examples/asset/asset_decompression.rs" +doc-scrape-examples = true + +[package.metadata.example.asset_decompression] +name = "Asset Decompression" +description = "Demonstrates loading a compressed asset" +category = "Assets" +wasm = false + [[example]] name = "custom_asset" path = "examples/asset/custom_asset.rs" diff --git a/assets/data/compressed_image.png.gz b/assets/data/compressed_image.png.gz new file mode 100644 index 0000000000000..3d96da393547e Binary files /dev/null and b/assets/data/compressed_image.png.gz differ diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 9810e08097736..008312f200b70 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -562,6 +562,62 @@ impl<'a> LoadContext<'a> { self.loader_dependencies.insert(path, hash); Ok(loaded_asset) } + + /// Loads the asset at the given `path` directly from the provided `reader`. This is an async function that will wait until the asset is fully loaded before + /// returning. Use this if you need the _value_ of another asset in order to load the current asset, and that value comes from your [`Reader`]. + /// For example, if you are deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a + /// "load dependency". + /// + /// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadAndSave`] preprocessor, + /// changing a "load dependency" will result in re-processing of the asset. + /// + /// [`Process`]: crate::processor::Process + /// [`LoadAndSave`]: crate::processor::LoadAndSave + pub async fn load_direct_with_reader<'b>( + &mut self, + reader: &mut Reader<'_>, + path: impl Into>, + ) -> Result { + let path = path.into().into_owned(); + + let loader = self + .asset_server + .get_path_asset_loader(&path) + .await + .map_err(|error| LoadDirectError { + dependency: path.clone(), + error: error.into(), + })?; + + let meta = loader.default_meta(); + + let loaded_asset = self + .asset_server + .load_with_meta_loader_and_reader( + &path, + meta, + &*loader, + reader, + false, + self.populate_hashes, + ) + .await + .map_err(|error| LoadDirectError { + dependency: path.clone(), + error, + })?; + + let info = loaded_asset + .meta + .as_ref() + .and_then(|m| m.processed_info().as_ref()); + + let hash = info.map(|i| i.full_hash).unwrap_or_default(); + + self.loader_dependencies.insert(path, hash); + + Ok(loaded_asset) + } } /// An error produced when calling [`LoadContext::read_asset_bytes`] diff --git a/examples/README.md b/examples/README.md index 70b1842643858..0450ef88159b8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -181,6 +181,7 @@ Example | Description Example | Description --- | --- +[Asset Decompression](../examples/asset/asset_decompression.rs) | Demonstrates loading a compressed asset [Asset Loading](../examples/asset/asset_loading.rs) | Demonstrates various methods to load assets [Asset Processing](../examples/asset/processing/asset_processing.rs) | Demonstrates how to process and load custom assets [Custom Asset](../examples/asset/custom_asset.rs) | Implements a custom asset loader diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs new file mode 100644 index 0000000000000..16538d9fca6d9 --- /dev/null +++ b/examples/asset/asset_decompression.rs @@ -0,0 +1,138 @@ +//! Implements loader for a Gzip compressed asset. + +use bevy::utils::thiserror; +use bevy::{ + asset::{ + io::{Reader, VecReader}, + AssetLoader, AsyncReadExt, ErasedLoadedAsset, LoadContext, LoadDirectError, + }, + prelude::*, + reflect::TypePath, + utils::BoxedFuture, +}; +use flate2::read::GzDecoder; +use std::io::prelude::*; +use std::marker::PhantomData; +use thiserror::Error; + +#[derive(Asset, TypePath)] +pub struct GzAsset { + pub uncompressed: ErasedLoadedAsset, +} + +#[derive(Default)] +pub struct GzAssetLoader; + +/// Possible errors that can be produced by [`GzAssetLoader`] +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum GzAssetLoaderError { + /// An [IO](std::io) Error + #[error("Could not load asset: {0}")] + Io(#[from] std::io::Error), + /// An error caused when the asset path cannot be used ot determine the uncompressed asset type. + #[error("Could not determine file path of uncompressed asset")] + IndeterminateFilePath, + /// An error caused by the internal asset loader. + #[error("Could not load contained asset: {0}")] + LoadDirectError(#[from] LoadDirectError), +} + +impl AssetLoader for GzAssetLoader { + type Asset = GzAsset; + type Settings = (); + type Error = GzAssetLoaderError; + fn load<'a>( + &'a self, + reader: &'a mut Reader, + _settings: &'a (), + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + let compressed_path = load_context.path(); + let file_name = compressed_path + .file_name() + .ok_or(GzAssetLoaderError::IndeterminateFilePath)? + .to_string_lossy(); + let uncompressed_file_name = file_name + .strip_suffix(".gz") + .ok_or(GzAssetLoaderError::IndeterminateFilePath)?; + let contained_path = compressed_path.join(uncompressed_file_name); + + let mut bytes_compressed = Vec::new(); + + reader.read_to_end(&mut bytes_compressed).await?; + + let mut decoder = GzDecoder::new(bytes_compressed.as_slice()); + + let mut bytes_uncompressed = Vec::new(); + + decoder.read_to_end(&mut bytes_uncompressed)?; + + // Now that we have decompressed the asset, let's pass it back to the + // context to continue loading + + let mut reader = VecReader::new(bytes_uncompressed); + + let uncompressed = load_context + .load_direct_with_reader(&mut reader, contained_path) + .await?; + + Ok(GzAsset { uncompressed }) + }) + } + + fn extensions(&self) -> &[&str] { + &["gz"] + } +} + +#[derive(Component, Default)] +struct Compressed { + compressed: Handle, + _phantom: PhantomData, +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .init_asset::() + .init_asset_loader::() + .add_systems(Startup, setup) + .add_systems(Update, decompress::) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2dBundle::default()); + + commands.spawn(( + Compressed:: { + compressed: asset_server.load("data/compressed_image.png.gz"), + ..default() + }, + Sprite::default(), + TransformBundle::default(), + VisibilityBundle::default(), + )); +} + +fn decompress( + mut commands: Commands, + asset_server: Res, + mut compressed_assets: ResMut>, + query: Query<(Entity, &Compressed)>, +) { + for (entity, Compressed { compressed, .. }) in query.iter() { + let Some(GzAsset { uncompressed }) = compressed_assets.remove(compressed) else { + continue; + }; + + let uncompressed = uncompressed.take::().unwrap(); + + commands + .entity(entity) + .remove::>() + .insert(asset_server.add(uncompressed)); + } +}