fennel_core/resources/
mod.rs

1use std::{any::Any, cell::Ref, collections::HashMap, fs, path::PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{graphics::Graphics, resources::{font::DummyFont, image::Image}};
6
7pub mod image;
8pub mod font;
9
10/// Manages a collection of loadable resources indexed by their name
11pub struct ResourceManager {
12    /// Map resource name to a type that implements [`LoadableResource`] trait
13    pub resources: HashMap<String, Box<dyn LoadableResource>>,
14}
15
16unsafe impl Send for ResourceManager {}
17unsafe impl Sync for ResourceManager {}
18
19#[derive(Deserialize, Serialize, Debug)]
20enum AssetType {
21    Image,
22    Audio,
23    Font
24}
25
26#[derive(Deserialize, Serialize, Debug)]
27/// Manifest structure of an asset presented in manifest
28struct Asset {
29    name: String,
30    path: String,
31    #[serde(rename(deserialize = "type"))]
32    class: AssetType
33}
34
35#[derive(Deserialize, Debug)]
36/// Assets package manifest
37struct Manifest {
38    pub assets: Vec<Asset>
39}
40
41/// Trait that all loadable assets must implement
42pub trait LoadableResource: Any {
43    /// Load a resource from `path` and return it boxed
44    ///
45    /// # Arguments
46    /// `path`: path to the resoruce file
47    /// `graphics`: current [`Graphics`] instance which holds `texture_creator` and `ttf_context`
48    /// `size`: optional size for any resoruce that needs it
49    ///
50    /// # Errors
51    /// Returns an error if the file cannot be read or parsed
52    fn load(
53        path: PathBuf,
54        name: String,
55        graphics: &mut Graphics,
56        size: Option<f32>
57    ) -> anyhow::Result<Box<dyn LoadableResource>>
58    where
59        Self: Sized;
60
61    /// Eaasy-to-use identifier for the resource
62    fn name(&self) -> String;
63
64    /// Return a mutable slice that the graphics thread can pass to SDL
65    ///
66    /// If the resource does not have a buffer, then it mustn't implement this function
67    fn as_mut_slice(&self) -> Option<&mut [u8]> {
68        None
69    }
70
71    /// Return an immutable slice for read‑only access
72    ///
73    /// If the resource does not have a buffer, then it mustn't implement this function
74    fn as_slice(&self) -> Option<Ref<'_, [u8]>> {
75        None
76    }
77}
78
79/// evil &Box\<dyn LoadableResource> to &T
80#[allow(clippy::borrowed_box)] // i have no idea how can this be done better because here we box a
81// trait
82/// Downcast a '&Box<dyn LoadableResource>' to a concrete type 
83pub fn downcast_ref<T: 'static + LoadableResource>(
84    b: &Box<dyn LoadableResource>,
85) -> anyhow::Result<&T> {
86    let dyn_ref: &dyn LoadableResource = b.as_ref();
87
88    let any_ref = dyn_ref as &dyn Any;
89
90    Ok(any_ref
91        .downcast_ref::<T>()
92        .expect("incorrect concrete type"))
93}
94
95impl ResourceManager {
96    /// Create a new manager with empty `resources` field
97    pub fn new() -> Self {
98        Self {
99            resources: HashMap::new(),
100        }
101    }
102
103    /// Loads assets from a directory, which must contain a manifest
104    ///
105    /// # Errors
106    /// Returns an error if manifest does not exist in the target directory
107    pub fn load_dir(&mut self, path: PathBuf, graphics: &mut Graphics) -> anyhow::Result<()> {
108        let manifest_file = fs::read(path.join("manifest.toml"))?;
109        let manifest: Manifest = toml::from_slice(&manifest_file)?;
110        for asset in manifest.assets {
111            match asset.class {
112                AssetType::Image => {
113                    let path = path.join(asset.path);
114                    let image = Image::load(path.clone(), path.to_str().unwrap().to_string(), graphics, None)?;
115                    println!("{:?}", image.name());
116                    self.cache_asset(image)?;
117                },
118                AssetType::Audio => {},
119                AssetType::Font => {
120                    let path = path.join(asset.path);
121                    let font = DummyFont::load(path, asset.name, graphics, None)?;
122                    println!("{:?}", font.name());
123                    self.cache_asset(font)?;
124                }
125            }
126        }
127        Ok(())
128    }
129
130    /// Insert a loaded asset into the cache
131    ///
132    /// The asset is stored under the key returned by `asset.name()`
133    pub fn cache_asset(&mut self, asset: Box<dyn LoadableResource>) -> anyhow::Result<()> {
134        self.resources.insert(asset.name(), asset);
135        Ok(())
136    }
137
138    // here i have NO fucking idea should it be `&Box<dyn LoadableResource>` or whatever
139    // self.resources.get returns a reference to the resource, so basically a reference to Box
140    // but afaik Box is a pointer, and for me it feels a bit fucking wrong to uh return a
141    // reference to a pointer >:3 and also clippy is angry at me for doing this
142    #[allow(clippy::borrowed_box)] // same reason as in `as_concrete`
143    pub fn get_asset(&self, name: String) -> anyhow::Result<&Box<dyn LoadableResource>> {
144        let asset = self.resources.get(&name).unwrap_or_else(|| {
145            panic!("asset {name} not found")
146        });
147        Ok(asset)
148    }
149
150    /// Check if a resource is cached
151    pub fn is_cached(&self, name: String) -> bool {
152        self.resources.contains_key(&name)
153    }
154}
155
156impl Default for ResourceManager {
157    /// `default()` is equivalent to `ResourceManager::new()`.
158    fn default() -> Self {
159        Self::new()
160    }
161}