asset_decompression/
asset_decompression.rs1use 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#[non_exhaustive]
25#[derive(Debug, Error)]
26enum GzAssetLoaderError {
27 #[error("Could not load asset: {0}")]
29 Io(#[from] std::io::Error),
30 #[error("Could not determine file path of uncompressed asset")]
32 IndeterminateFilePath,
33 #[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 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}