Files
bevy/examples/asset/asset_decompression.rs
andriyDev e9e15e516c Replace the NestedLoader API with NestedLoadBuilder (matching LoadBuilder). (#23979)
# Objective

- Followup to #23663.
- Make the nested loading API easier to understand (when reading from
docs.rs).

## Solution

- Remove the type-state stuff from `NestedLoader`. Make each way of
calling load just be its own function.
- Add override_unapproved to the `NestedLoader`.
- Rename `NestedLoader` to `NestedLoadBuilder` (to match `LoadBuilder`).
- Rename `.loader()` to `.load_builder()` just like
`AssetServer::load_builder`.

Some decisions:

- I included `override_unapproved` to match LoadBuilder. We could remove
this to say "loaders shouldn't be able to override", but I don't think
that's problematic, and I'd rather have it for completeness.
- I omitted `with_guard` from `NestedLoadBuilder`. This option doesn't
really make sense, plus you could just use the async methods to do this
instead.
- Since we omitted the `with_guard` (since it really doesn't make sense
in this context), I decided not to reuse `LoadBuilder` in any way here.
Either way, we need to deal with dependencies, so we couldn't just use
it directly anyway.
- I just created all 9 load variants as separate methods. That's a lot
compared to the 4 variants in LoadBuilder. We could reduce it to 6 load
variants, if we kept the `with_reader` stuff as a type-state like we did
with `NestedLoader`. I'm not a fan of how many variants we have, but I
don't think there's a way to do this without opening up impossible
configurations, like doing with_reader for a deferred call - this
doesn't work!
2026-04-28 05:13:40 +00:00

134 lines
3.8 KiB
Rust

//! Implements loader for a Gzip compressed asset.
use bevy::{
asset::{
io::{Reader, VecReader},
AssetLoader, ErasedLoadedAsset, LoadContext, LoadDirectError,
},
prelude::*,
reflect::TypePath,
};
use flate2::read::GzDecoder;
use std::{io::prelude::*, marker::PhantomData};
use thiserror::Error;
#[derive(Asset, TypePath)]
struct GzAsset {
uncompressed: ErasedLoadedAsset,
}
#[derive(Default, TypePath)]
struct GzAssetLoader;
/// Possible errors that can be produced by [`GzAssetLoader`]
#[non_exhaustive]
#[derive(Debug, Error)]
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 to 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;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let compressed_path = load_context.path();
let file_name = compressed_path
.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
.resolve_embed_str(uncompressed_file_name)
.map_err(|_| GzAssetLoaderError::IndeterminateFilePath)?;
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_builder()
.load_untyped_value_from_reader(contained_path, &mut reader)
.await?;
Ok(GzAsset { uncompressed })
}
fn extensions(&self) -> &[&str] {
&["gz"]
}
}
#[derive(Component, Default)]
struct Compressed<T> {
compressed: Handle<GzAsset>,
_phantom: PhantomData<T>,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_asset::<GzAsset>()
.init_asset_loader::<GzAssetLoader>()
.add_systems(Startup, setup)
.add_systems(Update, decompress::<Sprite, Image>)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
commands.spawn(Compressed::<Image> {
compressed: asset_server.load("data/compressed_image.png.gz"),
..default()
});
}
fn decompress<T: Component + From<Handle<A>>, A: Asset>(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut compressed_assets: ResMut<Assets<GzAsset>>,
query: Query<(Entity, &Compressed<A>)>,
) {
for (entity, Compressed { compressed, .. }) in query.iter() {
let Some(GzAsset { uncompressed }) = compressed_assets.remove(compressed) else {
continue;
};
let uncompressed = uncompressed.take::<A>().unwrap();
commands
.entity(entity)
.remove::<Compressed<A>>()
.insert(T::from(asset_server.add(uncompressed)));
}
}