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(®istry)?)?;
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}