micro_games_kit/
assets.rs

1use crate::game::GameSubsystem;
2use anput::world::World;
3use core::str;
4use fontdue::Font;
5use image::{GenericImage, GenericImageView, RgbaImage};
6use keket::{
7    database::{
8        handle::AssetHandle,
9        path::{AssetPath, AssetPathStatic},
10        AssetDatabase,
11    },
12    fetch::{
13        container::{ContainerAssetFetch, ContainerPartialFetch},
14        AssetFetch,
15    },
16    protocol::{
17        bytes::BytesAssetProtocol, group::GroupAssetProtocol, text::TextAssetProtocol,
18        AssetProtocol,
19    },
20};
21use kira::sound::static_sound::StaticSoundData;
22use serde::{Deserialize, Serialize};
23use spitfire_glow::renderer::GlowTextureFormat;
24use std::{
25    borrow::Cow,
26    collections::HashMap,
27    error::Error,
28    io::{Cursor, Read, Write},
29    ops::Range,
30    path::Path,
31};
32
33pub fn name_from_path<'a>(path: &'a AssetPath<'a>) -> &'a str {
34    path.meta_items()
35        .find(|(key, _)| *key == "as")
36        .map(|(_, value)| value)
37        .unwrap_or(path.path())
38}
39
40pub fn make_database(fetch: impl AssetFetch) -> AssetDatabase {
41    AssetDatabase::default()
42        .with_protocol(BytesAssetProtocol)
43        .with_protocol(TextAssetProtocol)
44        .with_protocol(GroupAssetProtocol)
45        .with_protocol(ShaderAssetProtocol)
46        .with_protocol(TextureAssetProtocol)
47        .with_protocol(FontAssetProtocol)
48        .with_protocol(SoundAssetProtocol)
49        .with_fetch(fetch)
50}
51
52pub fn make_memory_database(package: &[u8]) -> Result<AssetDatabase, Box<dyn Error>> {
53    Ok(make_database(ContainerAssetFetch::new(
54        AssetPackage::decode(package)?,
55    )))
56}
57
58pub fn make_directory_database(
59    directory: impl AsRef<Path>,
60) -> Result<AssetDatabase, Box<dyn Error>> {
61    Ok(make_database(ContainerAssetFetch::new(
62        AssetPackage::from_directory(directory)?,
63    )))
64}
65
66#[derive(Debug, Default, Serialize, Deserialize)]
67struct AssetPackageRegistry {
68    mappings: HashMap<String, Range<usize>>,
69}
70
71#[derive(Default)]
72pub struct AssetPackage {
73    registry: AssetPackageRegistry,
74    content: Vec<u8>,
75}
76
77impl AssetPackage {
78    pub fn from_directory(directory: impl AsRef<Path>) -> Result<Self, Box<dyn Error>> {
79        fn visit_dirs(
80            dir: &Path,
81            root: &str,
82            registry: &mut AssetPackageRegistry,
83            content: &mut Cursor<Vec<u8>>,
84        ) -> std::io::Result<()> {
85            if dir.is_dir() {
86                for entry in std::fs::read_dir(dir)? {
87                    let entry = entry?;
88                    let path = entry.path();
89                    let name = path.file_name().unwrap().to_str().unwrap();
90                    let name = if root.is_empty() {
91                        name.to_owned()
92                    } else {
93                        format!("{}/{}", root, name)
94                    };
95                    if path.is_dir() {
96                        visit_dirs(&path, &name, registry, content)?;
97                    } else {
98                        let bytes = std::fs::read(path)?;
99                        let start = content.position() as usize;
100                        content.write_all(&bytes)?;
101                        let end = content.position() as usize;
102                        registry.mappings.insert(name, start..end);
103                    }
104                }
105            }
106            Ok(())
107        }
108
109        let directory = directory.as_ref();
110        let mut registry = AssetPackageRegistry::default();
111        let mut content = Cursor::new(Vec::default());
112        visit_dirs(directory, "", &mut registry, &mut content)?;
113        Ok(AssetPackage {
114            registry,
115            content: content.into_inner(),
116        })
117    }
118
119    pub fn decode(bytes: &[u8]) -> Result<Self, Box<dyn Error>> {
120        let mut stream = Cursor::new(bytes);
121        let mut size = 0u32.to_be_bytes();
122        stream.read_exact(&mut size)?;
123        let size = u32::from_be_bytes(size) as usize;
124        let mut registry = vec![0u8; size];
125        stream.read_exact(&mut registry)?;
126        let registry = toml::from_str(str::from_utf8(&registry)?)?;
127        let mut content = Vec::default();
128        stream.read_to_end(&mut content)?;
129        Ok(Self { registry, content })
130    }
131
132    pub fn encode(&self) -> Result<Vec<u8>, Box<dyn Error>> {
133        let mut stream = Cursor::new(Vec::default());
134        let registry = toml::to_string(&self.registry)?;
135        let registry = registry.as_bytes();
136        stream.write_all(&(registry.len() as u32).to_be_bytes())?;
137        stream.write_all(registry)?;
138        stream.write_all(&self.content)?;
139        Ok(stream.into_inner())
140    }
141
142    pub fn paths(&self) -> impl Iterator<Item = &str> {
143        self.registry.mappings.keys().map(|key| key.as_str())
144    }
145}
146
147impl ContainerPartialFetch for AssetPackage {
148    fn load_bytes(&mut self, path: AssetPath) -> Result<Vec<u8>, Box<dyn Error>> {
149        if let Some(range) = self.registry.mappings.get(path.path()).cloned() {
150            if range.end <= self.content.len() {
151                Ok(self.content[range].to_owned())
152            } else {
153                Err(format!(
154                    "Asset: `{}` out of content bounds! Bytes range: {:?}, content byte size: {}",
155                    path,
156                    range,
157                    self.content.len()
158                )
159                .into())
160            }
161        } else {
162            Err(format!("Asset: `{}` not present in package!", path).into())
163        }
164    }
165}
166
167impl std::fmt::Debug for AssetPackage {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        f.debug_struct("AssetPackage")
170            .field("registry", &self.registry)
171            .finish_non_exhaustive()
172    }
173}
174
175pub struct ShaderAsset {
176    pub vertex: Cow<'static, str>,
177    pub fragment: Cow<'static, str>,
178}
179
180impl ShaderAsset {
181    pub fn new(vertex: &'static str, fragment: &'static str) -> Self {
182        Self {
183            vertex: vertex.into(),
184            fragment: fragment.into(),
185        }
186    }
187}
188
189pub struct ShaderAssetSubsystem;
190
191impl GameSubsystem for ShaderAssetSubsystem {
192    fn run(&mut self, context: crate::context::GameContext, _: f32) {
193        for entity in context.assets.storage.added().iter_of::<ShaderAsset>() {
194            if let Some((path, asset)) = context
195                .assets
196                .storage
197                .lookup_one::<true, (&AssetPathStatic, &ShaderAsset)>(entity)
198            {
199                context.draw.shaders.insert(
200                    name_from_path(&path).to_owned().into(),
201                    context
202                        .graphics
203                        .shader(asset.vertex.trim(), asset.fragment.trim())
204                        .unwrap(),
205                );
206            }
207        }
208        for entity in context.assets.storage.removed().iter_of::<ShaderAsset>() {
209            if let Some(path) = context
210                .assets
211                .storage
212                .lookup_one::<true, &AssetPathStatic>(entity)
213            {
214                context.draw.shaders.remove(name_from_path(&path));
215            }
216        }
217    }
218}
219
220pub struct ShaderAssetProtocol;
221
222impl AssetProtocol for ShaderAssetProtocol {
223    fn name(&self) -> &str {
224        "shader"
225    }
226
227    fn process_bytes(
228        &mut self,
229        handle: AssetHandle,
230        storage: &mut World,
231        bytes: Vec<u8>,
232    ) -> Result<(), Box<dyn Error>> {
233        enum Mode {
234            Vertex,
235            Fragment,
236        }
237
238        let mut vertex = String::default();
239        let mut fragment = String::default();
240        let mut mode = Mode::Vertex;
241        for line in std::str::from_utf8(&bytes)?.lines() {
242            let trimmed = line.trim();
243            if let Some(comment) = trimmed.strip_prefix("///") {
244                let comment = comment.trim().to_lowercase();
245                if comment == "[vertex]" {
246                    mode = Mode::Vertex;
247                    continue;
248                }
249                if comment == "[fragment]" {
250                    mode = Mode::Fragment;
251                    continue;
252                }
253            }
254            match mode {
255                Mode::Vertex => {
256                    vertex.push_str(line);
257                    vertex.push('\n');
258                }
259                Mode::Fragment => {
260                    fragment.push_str(line);
261                    fragment.push('\n');
262                }
263            }
264        }
265
266        storage.insert(
267            handle.entity(),
268            (ShaderAsset {
269                vertex: vertex.into(),
270                fragment: fragment.into(),
271            },),
272        )?;
273
274        Ok(())
275    }
276}
277
278pub struct TextureAsset {
279    pub image: RgbaImage,
280    pub cols: u32,
281    pub rows: u32,
282}
283
284pub struct TextureAssetSubsystem;
285
286impl GameSubsystem for TextureAssetSubsystem {
287    fn run(&mut self, context: crate::context::GameContext, _: f32) {
288        for entity in context.assets.storage.added().iter_of::<TextureAsset>() {
289            if let Some((path, asset)) = context
290                .assets
291                .storage
292                .lookup_one::<true, (&AssetPathStatic, &TextureAsset)>(entity)
293            {
294                let pages = asset.cols * asset.rows;
295                context.draw.textures.insert(
296                    name_from_path(&path).to_owned().into(),
297                    context
298                        .graphics
299                        .texture(
300                            asset.image.width(),
301                            asset.image.height() / pages,
302                            pages,
303                            GlowTextureFormat::Rgba,
304                            Some(asset.image.as_raw()),
305                        )
306                        .unwrap(),
307                );
308            }
309        }
310        for entity in context.assets.storage.removed().iter_of::<TextureAsset>() {
311            if let Some(path) = context
312                .assets
313                .storage
314                .lookup_one::<true, &AssetPathStatic>(entity)
315            {
316                context.draw.textures.remove(name_from_path(&path));
317            }
318        }
319    }
320}
321
322pub struct TextureAssetProtocol;
323
324impl AssetProtocol for TextureAssetProtocol {
325    fn name(&self) -> &str {
326        "texture"
327    }
328
329    fn process_bytes(
330        &mut self,
331        handle: AssetHandle,
332        storage: &mut World,
333        bytes: Vec<u8>,
334    ) -> Result<(), Box<dyn Error>> {
335        let path = storage.component::<true, AssetPathStatic>(handle.entity())?;
336        let mut cols = 1;
337        let mut rows = 1;
338        for (key, value) in path.meta_items() {
339            if key == "cols" || key == "c" {
340                cols = value.parse().unwrap_or(1);
341            } else if key == "rows" || key == "r" {
342                rows = value.parse().unwrap_or(1);
343            }
344        }
345        let mut image = image::load_from_memory(&bytes)
346            .map_err(|_| format!("Failed to load texture: {:?}", path.path()))?
347            .into_rgba8();
348        drop(path);
349        let pages = cols * rows;
350        image = if cols > 1 || rows > 1 {
351            let width = image.width() / cols;
352            let height = image.height() / rows;
353            let mut result = RgbaImage::new(width, height * pages);
354            for row in 0..rows {
355                for col in 0..cols {
356                    let view = image.view(col * width, row * height, width, height);
357                    result
358                        .copy_from(&*view, 0, (row * cols + col) * height)
359                        .unwrap();
360                }
361            }
362            result
363        } else {
364            image
365        };
366
367        storage.insert(handle.entity(), (TextureAsset { image, cols, rows },))?;
368
369        Ok(())
370    }
371}
372
373pub struct FontAsset {
374    pub font: Font,
375}
376
377pub struct FontAssetSubsystem;
378
379impl GameSubsystem for FontAssetSubsystem {
380    fn run(&mut self, context: crate::context::GameContext, _: f32) {
381        for entity in context.assets.storage.added().iter_of::<FontAsset>() {
382            if let Some((path, asset)) = context
383                .assets
384                .storage
385                .lookup_one::<true, (&AssetPathStatic, &FontAsset)>(entity)
386            {
387                context
388                    .draw
389                    .fonts
390                    .insert(name_from_path(&path).to_owned(), asset.font.clone());
391            }
392        }
393        for entity in context.assets.storage.removed().iter_of::<FontAsset>() {
394            if let Some(path) = context
395                .assets
396                .storage
397                .lookup_one::<true, &AssetPathStatic>(entity)
398            {
399                context.draw.fonts.remove(name_from_path(&path));
400            }
401        }
402    }
403}
404
405pub struct FontAssetProtocol;
406
407impl AssetProtocol for FontAssetProtocol {
408    fn name(&self) -> &str {
409        "font"
410    }
411
412    fn process_bytes(
413        &mut self,
414        handle: AssetHandle,
415        storage: &mut World,
416        bytes: Vec<u8>,
417    ) -> Result<(), Box<dyn Error>> {
418        let path = storage.component::<true, AssetPathStatic>(handle.entity())?;
419        let font = Font::from_bytes(bytes, Default::default())
420            .map_err(|_| format!("Failed to load font: {:?}", path.path()))?;
421        drop(path);
422
423        storage.insert(handle.entity(), (FontAsset { font },))?;
424
425        Ok(())
426    }
427}
428
429pub struct SoundAsset {
430    pub data: StaticSoundData,
431}
432
433pub struct SoundAssetSubsystem;
434
435impl GameSubsystem for SoundAssetSubsystem {
436    fn run(&mut self, context: crate::context::GameContext, _: f32) {
437        for entity in context.assets.storage.added().iter_of::<SoundAsset>() {
438            if let Some((path, asset)) = context
439                .assets
440                .storage
441                .lookup_one::<true, (&AssetPathStatic, &SoundAsset)>(entity)
442            {
443                context
444                    .audio
445                    .sounds
446                    .insert(name_from_path(&path).to_owned(), asset.data.clone());
447            }
448        }
449        for entity in context.assets.storage.removed().iter_of::<SoundAsset>() {
450            if let Some(path) = context
451                .assets
452                .storage
453                .lookup_one::<true, &AssetPathStatic>(entity)
454            {
455                context.audio.sounds.remove(name_from_path(&path));
456            }
457        }
458    }
459}
460
461pub struct SoundAssetProtocol;
462
463impl AssetProtocol for SoundAssetProtocol {
464    fn name(&self) -> &str {
465        "sound"
466    }
467
468    fn process_bytes(
469        &mut self,
470        handle: AssetHandle,
471        storage: &mut World,
472        bytes: Vec<u8>,
473    ) -> Result<(), Box<dyn Error>> {
474        let path = storage.component::<true, AssetPathStatic>(handle.entity())?;
475        let data = StaticSoundData::from_cursor(Cursor::new(bytes))
476            .map_err(|_| format!("Failed to load sound: {:?}", path.path()))?;
477        drop(path);
478
479        storage.insert(handle.entity(), (SoundAsset { data },))?;
480
481        Ok(())
482    }
483}