bones_asset/
asset.rs

1use std::{
2    path::{Path, PathBuf},
3    pin::Pin,
4    sync::{
5        atomic::{AtomicU32, Ordering::SeqCst},
6        Arc,
7    },
8};
9
10use append_only_vec::AppendOnlyVec;
11use bones_utils::prelude::*;
12use dashmap::DashMap;
13use event_listener::{Event, EventListener};
14use futures_lite::future::Boxed as BoxedFuture;
15use parking_lot::Mutex;
16use semver::VersionReq;
17
18use crate::prelude::*;
19
20/// The unique ID for an asset pack.
21///
22/// Asset pack IDs are made up of a human-readable label, and a unique identifier. For example:
23///
24/// > awesome-pack_01h502c309fddv1vq1gwa918e8
25///
26/// These IDs can be generated with the [TypeID gen][gen] utility.
27///
28/// [gen]: https://zicklag.github.io/type-id-gen/
29pub type AssetPackId = LabeledId;
30
31/// An asset pack contains assets that are loaded by the game.
32///
33/// The game's built-in assets are contained the the core asset pack, and mods or other assets may
34/// also be loaded.
35#[derive(Clone, Debug)]
36pub struct AssetPack {
37    /// The display name of the asset pack.
38    pub name: String,
39    /// The unique ID of the asset pack.
40    pub id: AssetPackId,
41    /// The version number of the asset pack.
42    pub version: Version,
43
44    /// The game [`VersionReq`] this asset pack is compatible with.
45    pub game_version: VersionReq,
46
47    /// Schemas provided in the asset pack.
48    pub schemas: Vec<&'static Schema>,
49    /// The root asset for the asset pack.
50    pub root: UntypedHandle,
51}
52
53/// Specifies an asset pack, and it's exact version.
54#[derive(Clone, Debug, PartialEq, Eq, Hash)]
55pub struct AssetPackSpec {
56    /// The ID of the asset pack.
57    pub id: AssetPackId,
58    /// The version of the asset pack.
59    pub version: Version,
60}
61
62impl std::fmt::Display for AssetPackSpec {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        let Self { id, version } = self;
65        write!(f, "{id}@{version}")
66    }
67}
68
69/// A requirement specifier for an asset pack, made up of the asset pack's [`LabeledId`] and it's
70/// [`VersionReq`].
71#[derive(Debug, Clone)]
72pub struct AssetPackReq {
73    /// The asset pack ID.
74    pub id: LabeledId,
75    /// The version of the asset pack.
76    pub version: VersionReq,
77}
78
79/// A schema reference, containing the ID of the pack that defined the schema, and the name of the
80/// schema in the pack.
81#[derive(Clone, Debug)]
82pub struct SchemaPath {
83    /// The ID of the pack, or [`None`] if it refers to the core pack.
84    pub pack: Option<AssetPackReq>,
85    /// The name of the schema.
86    pub name: String,
87}
88
89/// Struct containing all the game's loaded assets, including the default assets and
90/// asset-packs/mods.
91pub struct LoadedAssets {
92    /// The game's default asset pack.
93    pub default: UntypedHandle,
94    /// Extra asset packs. The key is the the name of the asset pack.
95    pub packs: HashMap<String, UntypedHandle>,
96}
97
98/// The progress that has been made loading the game assets.
99#[derive(Debug, Clone, Default)]
100pub struct AssetLoadProgress {
101    assets_to_load: Arc<AtomicU32>,
102    assets_downloaded: Arc<AtomicU32>,
103    assets_loaded: Arc<AtomicU32>,
104    assets_errored: Arc<AtomicU32>,
105    /// The event notifier that is used to wake interested tasks that are waiting for asset load
106    /// to progress.
107    event: Arc<Event>,
108}
109
110impl AssetLoadProgress {
111    /// Increment the number of assets that need to be loaded by one.
112    pub fn inc_to_load(&self) {
113        self.assets_to_load.fetch_add(1, SeqCst);
114    }
115
116    /// Increment the number of assets that have errored during loading.
117    pub fn inc_errored(&self) {
118        self.assets_errored.fetch_add(1, SeqCst);
119    }
120
121    /// Increment the number of assets that have been downloaded by one.
122    pub fn inc_downloaded(&self) {
123        self.assets_downloaded.fetch_add(1, SeqCst);
124    }
125
126    /// Increment the number of assets that have been loaded by one.
127    pub fn inc_loaded(&self) {
128        self.assets_loaded.fetch_add(1, SeqCst);
129        self.event.notify(usize::MAX);
130    }
131
132    /// Get whether or not all the assets are done loading.
133    ///
134    /// > **Note:** Assets that have errored while loading are still counted as "done loading".
135    pub fn is_finished(&self) -> bool {
136        let loaded = self.assets_loaded.load(SeqCst);
137        let pending = self.assets_to_load.load(SeqCst);
138        let errored = self.assets_errored.load(SeqCst);
139        loaded != 0 && (loaded + errored) == pending
140    }
141
142    /// Get the number of assets that have been downloaded and loaded by their asset loaders.
143    pub fn loaded(&self) -> u32 {
144        self.assets_loaded.load(SeqCst)
145    }
146
147    /// Get the number of assets that have errored while loading.
148    pub fn errored(&self) -> u32 {
149        self.assets_errored.load(SeqCst)
150    }
151
152    /// Get the number of assets that must be loaded.
153    ///
154    /// Since assets are discovered as they are loaded this number may not be the final
155    /// asset count and may increase as more assets are discovered.
156    pub fn to_load(&self) -> u32 {
157        self.assets_to_load.load(SeqCst)
158    }
159
160    /// Get the number of assets that have had their data downloaded. Once an asset is downloaded
161    /// we have the raw bytes, but it may not have been processed by it's asset loader.
162    pub fn downloaded(&self) -> u32 {
163        self.assets_downloaded.load(SeqCst)
164    }
165
166    /// Get an event listener that will be notified each time asset load progress
167    /// has been updated.
168    pub fn listen(&self) -> Pin<Box<EventListener>> {
169        self.event.listen()
170    }
171}
172
173// TODO: Think of alternative to dashmap.
174// Dashmap is annoying to use because it wraps all returned assets from our API in dashmap
175// its reference container type to manage the locking. We should try to come up with a
176// way to manage the concurrent asset loading without requring the locks if possible.
177
178/// Stores assets for later retrieval.
179#[derive(Default, Clone, Debug)]
180pub struct AssetStore {
181    /// Maps the handle of the asset to it's content ID.
182    pub asset_ids: DashMap<UntypedHandle, Cid>,
183    /// Content addressed cache of raw bytes for asset data.
184    ///
185    /// Storing asset data in this ways allows you to easily replicate assets to other players over
186    /// the network by comparing available [`Cid`]s.
187    pub asset_data: DashMap<Cid, Vec<u8>>,
188    /// Maps asset content IDs, to assets that have been loaded by an asset loader from the raw
189    /// bytes.
190    pub assets: DashMap<Cid, LoadedAsset>,
191    /// Maps the asset [`AssetLoc`] to it's handle.
192    pub path_handles: DashMap<AssetLoc, UntypedHandle>,
193
194    /// List of assets that depend on the given assets.
195    pub reverse_dependencies: DashMap<UntypedHandle, HashSet<UntypedHandle>>,
196    /// Lists the packs that have not been loaded due to an incompatible game version.
197    pub incompabile_packs: DashMap<String, PackfileMeta>,
198
199    /// The core asset pack, if it's been loaded.
200    pub core_pack: Arc<Mutex<Option<AssetPack>>>,
201    /// The asset packs that have been loaded.
202    pub packs: DashMap<AssetPackSpec, AssetPack>,
203    /// Maps the directory names of asset packs to their [`AssetPackSpec`].
204    pub pack_dirs: DashMap<String, AssetPackSpec>,
205}
206
207/// Contains that path to an asset, and the pack_dir that it was loaded from.
208///
209/// A pack of [`None`] means that it was loaded from the core pack.
210#[derive(Clone, PartialEq, Eq, Hash, Debug)]
211pub struct AssetLoc {
212    /// The path to the asset in it's pack.
213    pub path: PathBuf,
214    /// The pack_dir of the pack that the asset is in.
215    pub pack: Option<String>,
216}
217
218impl AssetLoc {
219    /// Borrow as an [`AssetLocRef`].
220    pub fn as_ref(&self) -> AssetLocRef<'_> {
221        AssetLocRef {
222            pack: self.pack.as_deref(),
223            path: &self.path,
224        }
225    }
226}
227
228impl From<&AssetLocRef<'_>> for AssetLoc {
229    fn from(value: &AssetLocRef<'_>) -> Self {
230        AssetLoc {
231            path: value.path.to_owned(),
232            pack: value.pack.map(|x| x.to_owned()),
233        }
234    }
235}
236
237/// A borrowed version of [`AssetLoc`].
238#[derive(Clone, PartialEq, Eq, Hash, Debug, Copy)]
239pub struct AssetLocRef<'a> {
240    /// The path to the asset in it's pack.
241    pub path: &'a Path,
242    /// The pack_dir of the pack that the asset is in.
243    pub pack: Option<&'a str>,
244}
245
246impl<'a> From<(&'a Path, Option<&'a str>)> for AssetLocRef<'a> {
247    fn from((path, pack): (&'a Path, Option<&'a str>)) -> Self {
248        Self { pack, path }
249    }
250}
251
252impl AssetLocRef<'_> {
253    /// Clone data to an owned [`AssetLoc`].
254    pub fn to_owned(&self) -> AssetLoc {
255        self.into()
256    }
257}
258
259/// An asset that has been loaded.
260#[derive(Clone, Debug, Deref, DerefMut)]
261pub struct LoadedAsset {
262    /// The content ID of the loaded asset.
263    ///
264    /// This is a hash of the contents of the asset's binary data and all of the cids of it's
265    /// dependencies.
266    pub cid: Cid,
267    /// The asset pack this was loaded from, or [`None`] if it is from the default pack.
268    pub pack_spec: Option<AssetPackSpec>,
269    /// The pack and path the asset was loaded from.
270    pub loc: AssetLoc,
271    /// The content IDs of any assets needed by this asset as a dependency.
272    pub dependencies: Vec<UntypedHandle>,
273    /// The loaded data of the asset.
274    #[deref]
275    pub data: SchemaBox,
276}
277
278/// An identifier for an asset.
279#[derive(Clone, Debug, Eq, PartialEq, Hash, Default)]
280pub struct AssetInfo {
281    /// The unique ID of the asset pack this asset is located in.
282    pub pack: Cid,
283    /// The path to the asset, relative to the root of the asset pack.
284    pub path: PathBuf,
285}
286
287/// Context provided to custom asset loaders in the [`AssetLoader::load`] method.
288pub struct AssetLoadCtx {
289    /// The asset server.
290    pub asset_server: AssetServer,
291    /// The location of the asset.
292    pub loc: AssetLoc,
293    /// The [`Cid`]s of the assets this asset depends on.
294    ///
295    /// This is automatically updated when calling [`AssetLoadCtx::load_asset`].
296    pub dependencies: Arc<AppendOnlyVec<UntypedHandle>>,
297}
298
299impl AssetLoadCtx {
300    /// Load another asset as a child of this asset.
301    pub fn load_asset(&mut self, path: &Path) -> anyhow::Result<UntypedHandle> {
302        let handle = self.asset_server.load_asset(AssetLocRef {
303            path,
304            pack: self.loc.as_ref().pack,
305        });
306        self.dependencies.push(handle);
307        Ok(handle)
308    }
309}
310
311/// A custom assset loader.
312pub trait AssetLoader: Sync + Send + 'static {
313    /// Load the asset from raw bytes.
314    fn load(&self, ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>>;
315}
316
317/// A custom asset loader implementation for a metadata asset.
318///
319/// This is similar in purpose to implementing [`AssetLoader`], but instead of loading from bytes,
320/// it loads from the deserialized [`SchemaRefMut`] of a metadata asset and must be added as a
321/// schema type data.
322#[derive(HasSchema)]
323#[schema(no_clone, no_default)]
324pub struct SchemaMetaAssetLoader(
325    pub  fn(
326        ctx: &mut MetaAssetLoadCtx,
327        ptr: SchemaRefMut<'_>,
328        deserialzer: &mut dyn erased_serde::Deserializer,
329    ) -> anyhow::Result<()>,
330);
331
332impl SchemaMetaAssetLoader {
333    /// Load the asset
334    pub fn load<'a, 'de, D>(
335        &self,
336        ctx: &mut MetaAssetLoadCtx,
337        ptr: SchemaRefMut<'a>,
338        deserializer: D,
339    ) -> Result<(), erased_serde::Error>
340    where
341        D: serde::Deserializer<'de>,
342    {
343        use serde::de::Error;
344        let mut de = <dyn erased_serde::Deserializer>::erase(deserializer);
345        (self.0)(ctx, ptr, &mut de).map_err(|e| erased_serde::Error::custom(e.to_string()))
346    }
347}
348
349/// A [type data][bones_schema::alloc::TypeDatas] that indicates how to load a type as an asset.
350#[derive(HasSchema)]
351#[schema(opaque, no_default, no_clone)]
352pub enum AssetKind {
353    /// This is a metadata asset that can be loaded from JSON or YAML files.
354    Metadata {
355        /// The `extension` is the portion of the extension that comes before the `.json`, `.yml`,
356        /// or `.yaml` extension. For example, if the `extension` was set to `weapon`, then the asset
357        /// could be loaded from `.weapon.json`, `.weapon.yml`, or `.weapon.yaml` files.
358        extension: String,
359    },
360    /// An asset with a custom asset loader.
361    Custom {
362        /// The loader implementation for the asset.
363        loader: Box<dyn AssetLoader>,
364        /// The list of file extensions to load this asset from.
365        extensions: Vec<String>,
366    },
367}
368
369/// Helper function to return type data for a metadata asset.
370///
371/// # Example
372///
373/// This is meant to be used in a `type_data` attribute when deriving [`HasSchema`].
374///
375/// ```
376/// # use bones_asset::prelude::*;
377/// # use glam::*;
378/// #[derive(HasSchema, Default, Clone)]
379/// #[type_data(metadata_asset("atlas"))]
380/// #[repr(C)]
381/// struct AtlasMeta {
382///     pub tile_size: Vec2,
383///     pub grid_size: UVec2,
384/// }
385/// ```
386pub fn metadata_asset(extension: &str) -> AssetKind {
387    AssetKind::Metadata {
388        extension: extension.into(),
389    }
390}
391
392/// Helper function to return type data for a custom asset loader.
393///
394/// # Example
395///
396/// This is meant to be used in a `type_data` attribute when deriving [`HasSchema`].
397///
398/// ```
399/// # use bones_asset::prelude::*;
400/// #[derive(HasSchema, Default, Clone)]
401/// #[type_data(asset_loader("png", PngLoader))]
402/// #[repr(C)]
403/// struct Image {
404///     data: SVec<u8>,
405///     width: u32,
406///     height: u32,
407/// }
408///
409/// struct PngLoader;
410/// impl AssetLoader for PngLoader {
411///     fn load(&self, ctx: AssetLoadCtx, data: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
412///         Box::pin(async move {
413///             todo!("Load PNG from data");
414///         })
415///     }
416/// }
417/// ```
418pub fn asset_loader<L: AssetLoader, E: Into<AssetExtensions>>(
419    extensions: E,
420    loader: L,
421) -> AssetKind {
422    AssetKind::Custom {
423        loader: Box::new(loader),
424        extensions: extensions.into().0,
425    }
426}
427
428/// Helper type for storing asset extensions.
429pub struct AssetExtensions(Vec<String>);
430impl<'a, const N: usize> From<[&'a str; N]> for AssetExtensions {
431    fn from(value: [&'a str; N]) -> Self {
432        Self(value.iter().map(|x| x.to_string()).collect())
433    }
434}
435impl<'a> From<&'a str> for AssetExtensions {
436    fn from(value: &'a str) -> Self {
437        Self(vec![value.to_string()])
438    }
439}