asset_decompression/
asset_decompression.rs

1//! Implements loader for a Gzip compressed asset.
2
3use bevy::{
4    asset::{
5        io::{Reader, VecReader},
6        AssetLoader, ErasedLoadedAsset, LoadContext, LoadDirectError,
7    },
8    prelude::*,
9    reflect::TypePath,
10};
11use flate2::read::GzDecoder;
12use std::{io::prelude::*, marker::PhantomData};
13use thiserror::Error;
14
15#[derive(Asset, TypePath)]
16struct GzAsset {
17    uncompressed: ErasedLoadedAsset,
18}
19
20#[derive(Default)]
21struct GzAssetLoader;
22
23/// Possible errors that can be produced by [`GzAssetLoader`]
24#[non_exhaustive]
25#[derive(Debug, Error)]
26enum GzAssetLoaderError {
27    /// An [IO](std::io) Error
28    #[error("Could not load asset: {0}")]
29    Io(#[from] std::io::Error),
30    /// An error caused when the asset path cannot be used to determine the uncompressed asset type.
31    #[error("Could not determine file path of uncompressed asset")]
32    IndeterminateFilePath,
33    /// An error caused by the internal asset loader.
34    #[error("Could not load contained asset: {0}")]
35    LoadDirectError(#[from] LoadDirectError),
36}
37
38impl AssetLoader for GzAssetLoader {
39    type Asset = GzAsset;
40    type Settings = ();
41    type Error = GzAssetLoaderError;
42
43    async fn load(
44        &self,
45        reader: &mut dyn Reader,
46        _settings: &(),
47        load_context: &mut LoadContext<'_>,
48    ) -> Result<Self::Asset, Self::Error> {
49        let compressed_path = load_context.path();
50        let file_name = compressed_path
51            .file_name()
52            .ok_or(GzAssetLoaderError::IndeterminateFilePath)?
53            .to_string_lossy();
54        let uncompressed_file_name = file_name
55            .strip_suffix(".gz")
56            .ok_or(GzAssetLoaderError::IndeterminateFilePath)?;
57        let contained_path = compressed_path.join(uncompressed_file_name);
58
59        let mut bytes_compressed = Vec::new();
60
61        reader.read_to_end(&mut bytes_compressed).await?;
62
63        let mut decoder = GzDecoder::new(bytes_compressed.as_slice());
64
65        let mut bytes_uncompressed = Vec::new();
66
67        decoder.read_to_end(&mut bytes_uncompressed)?;
68
69        // Now that we have decompressed the asset, let's pass it back to the
70        // context to continue loading
71
72        let mut reader = VecReader::new(bytes_uncompressed);
73
74        let uncompressed = load_context
75            .loader()
76            .with_unknown_type()
77            .immediate()
78            .with_reader(&mut reader)
79            .load(contained_path)
80            .await?;
81
82        Ok(GzAsset { uncompressed })
83    }
84
85    fn extensions(&self) -> &[&str] {
86        &["gz"]
87    }
88}
89
90#[derive(Component, Default)]
91struct Compressed<T> {
92    compressed: Handle<GzAsset>,
93    _phantom: PhantomData<T>,
94}
95
96fn main() {
97    App::new()
98        .add_plugins(DefaultPlugins)
99        .init_asset::<GzAsset>()
100        .init_asset_loader::<GzAssetLoader>()
101        .add_systems(Startup, setup)
102        .add_systems(Update, decompress::<Sprite, Image>)
103        .run();
104}
105
106fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
107    commands.spawn(Camera2d);
108
109    commands.spawn(Compressed::<Image> {
110        compressed: asset_server.load("data/compressed_image.png.gz"),
111        ..default()
112    });
113}
114
115fn decompress<T: Component + From<Handle<A>>, A: Asset>(
116    mut commands: Commands,
117    asset_server: Res<AssetServer>,
118    mut compressed_assets: ResMut<Assets<GzAsset>>,
119    query: Query<(Entity, &Compressed<A>)>,
120) {
121    for (entity, Compressed { compressed, .. }) in query.iter() {
122        let Some(GzAsset { uncompressed }) = compressed_assets.remove(compressed) else {
123            continue;
124        };
125
126        let uncompressed = uncompressed.take::<A>().unwrap();
127
128        commands
129            .entity(entity)
130            .remove::<Compressed<A>>()
131            .insert(T::from(asset_server.add(uncompressed)));
132    }
133}