bones_asset/
server.rs

1use std::{
2    path::{Path, PathBuf},
3    sync::{Arc, Mutex},
4};
5
6use anyhow::Context;
7use append_only_vec::AppendOnlyVec;
8use async_channel::{Receiver, Sender};
9use bevy_tasks::IoTaskPool;
10use bones_utils::{default, Deref, DerefMut, UlidExt};
11use dashmap::{
12    mapref::one::{
13        MappedRef as MappedMapRef, MappedRefMut as MappedMapRefMut, Ref as MapRef,
14        RefMut as MapRefMut,
15    },
16    DashMap,
17};
18use once_cell::sync::Lazy;
19use parking_lot::{MappedMutexGuard, MutexGuard};
20use semver::VersionReq;
21use serde::{de::DeserializeSeed, Deserialize};
22#[allow(unused_imports)]
23use tracing::info;
24use ulid::Ulid;
25
26use crate::prelude::*;
27
28mod schema_loader;
29
30/// Struct responsible for loading assets into it's contained [`AssetStore`], using an [`AssetIo`]
31/// implementation.
32#[derive(HasSchema, Deref, DerefMut, Clone)]
33pub struct AssetServer {
34    #[deref]
35    /// The asset server inner state.
36    pub inner: Arc<AssetServerInner>,
37    /// The [`AssetIo`] implementation used to load assets.
38    pub io: Arc<dyn AssetIo>,
39}
40
41/// The inner state of the asset server.
42pub struct AssetServerInner {
43    /// The version of the game. This is used to evaluate whether asset packs are compatible with
44    /// the game.
45    pub game_version: Mutex<Version>,
46    /// The asset store.
47    pub store: AssetStore,
48    /// Sender for asset changes, used by the [`AssetIo`] implementation or other tasks to trigger
49    /// hot reloads.
50    pub asset_change_send: Sender<ChangedAsset>,
51    /// Receiver for asset changes, used to implement hot reloads.
52    pub asset_change_recv: Receiver<ChangedAsset>,
53    /// The asset load progress.
54    pub load_progress: AssetLoadProgress,
55}
56
57/// An ID for an asset that has changed.
58pub enum ChangedAsset {
59    /// The location of an asset that has changed.
60    Loc(AssetLoc),
61    /// The [`Cid`] of an asset that has changed.
62    Handle(UntypedHandle),
63}
64
65impl Default for AssetServer {
66    fn default() -> Self {
67        Self {
68            inner: default(),
69            io: Arc::new(DummyIo::new([])),
70        }
71    }
72}
73
74impl Default for AssetServerInner {
75    fn default() -> Self {
76        let (asset_change_send, asset_change_recv) = async_channel::unbounded();
77        Self {
78            game_version: Mutex::new(Version::new(0, 0, 0)),
79            store: default(),
80            load_progress: default(),
81            asset_change_send,
82            asset_change_recv,
83        }
84    }
85}
86
87/// YAML format for the core asset pack's `pack.yaml` file.
88#[derive(Debug, Clone, Deserialize)]
89pub struct CorePackfileMeta {
90    /// The path to the root asset for the pack.
91    pub root: PathBuf,
92    /// The paths to schema definitions to be loaded from this pack.
93    #[serde(default)]
94    pub schemas: Vec<PathBuf>,
95}
96
97/// YAML format for asset packs' `pack.yaml` file.
98#[derive(Debug, Clone, Deserialize)]
99pub struct PackfileMeta {
100    /// User friendly pack name.
101    pub name: String,
102    /// The unique ID of the asset pack.
103    pub id: AssetPackId,
104    /// The version of the asset pack.
105    pub version: Version,
106    /// The required game version to be compatible with this asset pack.
107    pub game_version: VersionReq,
108    /// The paths to schema definitions to be loaded from this pack.
109    #[serde(default)]
110    pub schemas: Vec<PathBuf>,
111    /// The path to the root asset for the pack.
112    pub root: PathBuf,
113}
114
115/// The [`AssetPackId`] of the core pack.
116pub static CORE_PACK_ID: Lazy<AssetPackId> =
117    Lazy::new(|| AssetPackId::new_with_ulid("core", Ulid(0)).unwrap());
118
119/// An error returned when an asset pack does not support the game version.
120#[derive(Debug, Clone)]
121pub struct IncompatibleGameVersionError {
122    /// The version of the game that the pack is not compatible with.
123    pub game_version: Version,
124    /// The directory of the pack that
125    pub pack_dir: String,
126    /// The metadata of the pack that could not be loaded.
127    pub pack_meta: PackfileMeta,
128}
129
130impl std::error::Error for IncompatibleGameVersionError {}
131impl std::fmt::Display for IncompatibleGameVersionError {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(
134            f,
135            "Asset pack `{}` v{} from folder `{}` is only compatible with game versions matching {}, not {}",
136            self.pack_meta.id,
137            self.pack_meta.version,
138            self.pack_dir,
139            self.pack_meta.game_version,
140            self.game_version
141        )
142    }
143}
144
145#[derive(Debug)]
146struct LoaderNotFound {
147    name: String,
148}
149impl std::fmt::Display for LoaderNotFound {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        write!(
152            f,
153            "Schema/loader not found for schema/extension: {}\n\
154            You may need to register the asset by calling `MyAsset::schema()`",
155            self.name
156        )
157    }
158}
159impl std::error::Error for LoaderNotFound {}
160
161impl AssetServer {
162    /// Initialize a new [`AssetServer`].
163    pub fn new<Io: AssetIo + 'static>(io: Io, version: Version) -> Self {
164        Self {
165            inner: Arc::new(AssetServerInner {
166                game_version: Mutex::new(version),
167                ..default()
168            }),
169            io: Arc::new(io),
170        }
171    }
172
173    /// Load the bytes of the asset at the given path, but return only the [`Cid`].
174    ///
175    /// The loaded bytes can be accessed from [`asset_server.store.asset_data`][AssetStore::asset_data]
176    /// using the [`Cid`].
177    ///
178    /// If `force` is false, the bytes will be loaded from cache if possible.
179    pub async fn load_asset_bytes(&self, loc: AssetLoc, force: bool) -> anyhow::Result<Cid> {
180        // Load asset data from cache if present
181        if !force {
182            let cid = self
183                .store
184                .path_handles
185                .get(&loc)
186                .and_then(|handle| self.store.asset_ids.get(&handle));
187            if let Some(cid) = cid {
188                return Ok(*cid);
189            }
190        }
191
192        // Load the data for the asset path
193        let data = self.io.load_file(loc.as_ref()).await?;
194
195        // Compute the Cid
196        let mut cid = Cid::default();
197        cid.update(&data);
198
199        // Insert the data into the cache
200        self.store.asset_data.insert(cid, data);
201
202        // Return the Cid
203        Ok(cid)
204    }
205
206    /// Tell the asset backend to watch for changes and trigger hot reloads for changed assets.
207    pub fn watch_for_changes(&self) {
208        self.io.watch(self.asset_change_send.clone());
209    }
210
211    /// Set the [`AssetIo`] implementation.
212    ///
213    /// This should almost always be called before calling [`load_assets()`][Self::load_assets].
214    pub fn set_io<Io: AssetIo + 'static>(&mut self, io: Io) {
215        self.io = Arc::new(io);
216    }
217
218    /// Responds to any asset changes reported by the [`AssetIo`] implementation.
219    ///
220    /// This must be called or asset changes will be ignored. Additionally, the [`AssetIo`]
221    /// implementation must be able to detect asset changes or this will do nothing.
222    pub fn handle_asset_changes<F: FnMut(&mut AssetServer, UntypedHandle)>(
223        &mut self,
224        mut handle_change: F,
225    ) {
226        while let Ok(changed) = self.asset_change_recv.try_recv() {
227            match changed {
228                ChangedAsset::Loc(loc) => {
229                    let handle = self.load_asset_forced(loc.as_ref());
230                    handle_change(self, handle)
231                }
232                ChangedAsset::Handle(handle) => {
233                    let entry = self
234                        .store
235                        .path_handles
236                        .iter()
237                        .find(|entry| *entry.value() == handle)
238                        .unwrap();
239                    let loc = entry.key().to_owned();
240                    drop(entry);
241                    self.load_asset_forced(loc.as_ref());
242                    handle_change(self, handle)
243                }
244            }
245        }
246    }
247
248    /// Load all assets. This is usually done in an async task.
249    pub async fn load_assets(&self) -> anyhow::Result<()> {
250        // Load the core asset pack
251        self.load_pack(None).await?;
252
253        // Load the user asset packs
254        for pack_dir in self.io.enumerate_packs().await? {
255            // Load the asset pack
256            let pack_result = self.load_pack(Some(&pack_dir)).await;
257            if let Err(e) = pack_result {
258                match e.downcast::<IncompatibleGameVersionError>() {
259                    // Check for a compatibility error
260                    Ok(e) => {
261                        tracing::warn!(
262                            "Not loading pack `{}` because it requires game version \
263                            `{}` and this is version `{}`",
264                            e.pack_meta.name,
265                            e.pack_meta.game_version,
266                            e.game_version,
267                        );
268                        // Add it to the list of incompatible packs.
269                        self.store.incompabile_packs.insert(e.pack_dir, e.pack_meta);
270                    }
271                    // If this is another kind of error, return the error
272                    Err(e) => {
273                        return Err(e).context(format!("Error loading asset pack: {pack_dir}"))
274                    }
275                }
276            }
277        }
278
279        Ok(())
280    }
281
282    /// Load the asset pack with the given folder name, or else the default pack if [`None`].
283    pub async fn load_pack(&self, pack: Option<&str>) -> anyhow::Result<AssetPackSpec> {
284        // Load the core pack differently
285        if pack.is_none() {
286            return self
287                .load_core_pack()
288                .await
289                .context("Error loading core asset pack");
290        }
291
292        // Load the asset packfile
293        let packfile_contents = self
294            .io
295            .load_file((Path::new("pack.yaml"), pack).into())
296            .await?;
297        let meta: PackfileMeta = serde_yaml::from_slice(&packfile_contents)?;
298        tracing::debug!(?pack, ?meta, "Loaded asset pack meta.");
299
300        // If the game version doesn't match, then don't continue loading this pack.
301        if !meta.game_version.matches(&self.game_version()) {
302            return Err(IncompatibleGameVersionError {
303                game_version: self.game_version(),
304                pack_dir: pack.unwrap().to_owned(),
305                pack_meta: meta,
306            }
307            .into());
308        }
309
310        // Store the asset pack spec associated to the pack dir name.
311        if let Some(pack_dir) = pack {
312            self.store.pack_dirs.insert(
313                pack_dir.into(),
314                AssetPackSpec {
315                    id: meta.id,
316                    version: meta.version.clone(),
317                },
318            );
319        }
320
321        // Load the schemas
322        let schemas = self.load_pack_schemas(pack, &meta.schemas).await?;
323
324        // Load the asset and produce a handle
325        let root_loc = AssetLocRef {
326            path: &meta.root,
327            pack,
328        };
329
330        // Return the loaded asset pack.
331        let spec = AssetPackSpec {
332            id: meta.id,
333            version: meta.version.clone(),
334        };
335        self.store.packs.insert(
336            spec.clone(),
337            AssetPack {
338                name: meta.name,
339                id: meta.id,
340                version: meta.version,
341                game_version: meta.game_version,
342                schemas,
343                root: default(),
344            },
345        );
346        let root_handle = self.load_asset(root_loc);
347        self.store.packs.get_mut(&spec).unwrap().root = root_handle;
348
349        Ok(spec)
350    }
351
352    /// Load the core asset pack.
353    pub async fn load_core_pack(&self) -> anyhow::Result<AssetPackSpec> {
354        // Load the core asset packfile
355        let packfile_contents = self
356            .io
357            .load_file(AssetLocRef {
358                path: Path::new("pack.yaml"),
359                pack: None,
360            })
361            .await
362            .context("Could not load pack file")?;
363        let meta: CorePackfileMeta = serde_yaml::from_slice(&packfile_contents)?;
364        tracing::debug!(?meta, "Loaded core pack meta.");
365
366        // Load the asset and produce a handle
367        let root_loc = AssetLocRef {
368            path: &meta.root,
369            pack: None,
370        };
371        let handle = self.load_asset(root_loc);
372
373        // Load the schemas
374        let schemas = self.load_pack_schemas(None, &meta.schemas).await?;
375
376        // Return the loaded asset pack.
377        let game_version = self.game_version();
378
379        let id = *CORE_PACK_ID;
380        *self.store.core_pack.lock() = Some(AssetPack {
381            name: "Core".into(),
382            id,
383            version: game_version.clone(),
384            game_version: VersionReq {
385                comparators: [semver::Comparator {
386                    op: semver::Op::Exact,
387                    major: game_version.major,
388                    minor: Some(game_version.minor),
389                    patch: Some(game_version.patch),
390                    pre: game_version.pre.clone(),
391                }]
392                .to_vec(),
393            },
394            schemas,
395            root: handle,
396        });
397
398        Ok(AssetPackSpec {
399            id,
400            version: game_version.clone(),
401        })
402    }
403
404    /// Load an asset.
405    pub fn load_asset(&self, loc: AssetLocRef<'_>) -> UntypedHandle {
406        self.impl_load_asset(loc, false)
407    }
408
409    /// Like [`load_asset()`][Self::load_asset] but forces the asset to reload, even it if has
410    /// already been loaded.
411    pub fn load_asset_forced(&self, loc: AssetLocRef<'_>) -> UntypedHandle {
412        self.impl_load_asset(loc, true)
413    }
414
415    fn impl_load_asset(&self, loc: AssetLocRef<'_>, force: bool) -> UntypedHandle {
416        // Get the asset pool
417        let pool = IoTaskPool::get();
418
419        // Absolutize the asset location
420        let loc = AssetLoc {
421            path: loc.path.absolutize_from("/").unwrap().into_owned(),
422            pack: loc.pack.map(|x| x.to_owned()),
423        };
424
425        // If this isn't a force reload
426        if !force {
427            // And we already have an asset handle created for this path
428            if let Some(handle) = self.store.path_handles.get(&loc) {
429                // Return the existing handle and stop processing
430                return *handle;
431            }
432        }
433
434        // Determine the handle and insert it into the path_handles map
435        let mut should_load = true;
436        let handle = *self
437            .store
438            .path_handles
439            .entry(loc.clone())
440            // If we've already loaded this asset before
441            .and_modify(|handle| {
442                // If we aren't forcing a reload and we already have asset data, we don't need to
443                // trigger another load.
444                if self.store.asset_ids.get(handle).is_some() && !force {
445                    should_load = false;
446                }
447            })
448            // If this is an unloaded location, create a new handle for it
449            .or_insert(UntypedHandle {
450                rid: Ulid::create(),
451            });
452
453        if should_load {
454            // Add one more asset that needs loading.
455            self.load_progress.inc_to_load();
456
457            // Spawn a task to load the asset
458            let server = self.clone();
459            let loc_ = loc.clone();
460            pool.spawn(async move {
461                tracing::debug!(?loc, ?force, "Loading asset");
462                let loc = loc_;
463                let result = async {
464                    let cid = server.load_asset_bytes(loc.clone(), force).await?;
465                    server.load_progress.inc_downloaded();
466                    let data = server
467                        .store
468                        .asset_data
469                        .get(&cid)
470                        .expect("asset not loaded")
471                        .clone();
472
473                    // Try to load a metadata asset if it has a YAML/JSON extension, if that doesn't work, and
474                    // it has a schema not found error, try to load a data asset for the same path, if that
475                    // doesn't work and it is an extension not found error, return the metadata error message.
476                    let partial = if path_is_metadata(&loc.path) {
477                        match server.load_metadata_asset(loc.as_ref(), &data).await {
478                            Err(meta_err) => {
479                                if meta_err.downcast_ref::<LoaderNotFound>().is_some() {
480                                    match server.load_data_asset(loc.as_ref(), &data).await {
481                                        Err(data_err) => {
482                                            if data_err.downcast_ref::<LoaderNotFound>().is_some() {
483                                                Err(meta_err)
484                                            } else {
485                                                Err(data_err)
486                                            }
487                                        }
488                                        ok => ok,
489                                    }
490                                } else {
491                                    Err(meta_err)
492                                }
493                            }
494                            ok => ok,
495                        }
496                    } else {
497                        server.load_data_asset(loc.as_ref(), &data).await
498                    }?;
499
500                    let loaded_asset = LoadedAsset {
501                        cid: partial.cid,
502                        pack_spec: loc.pack.as_ref().map(|x| {
503                            server
504                                .store
505                                .pack_dirs
506                                .get(x.as_str())
507                                .expect("Pack dir not loaded properly")
508                                .clone()
509                        }),
510                        loc: loc.to_owned(),
511                        dependencies: partial.dependencies,
512                        data: partial.data,
513                    };
514                    // Loaded assets are gotten through a line of key/value maps starting with a handle.
515                    // The handle gets a cid, which gets the loaded asset. Since we are doing this in async,
516                    // when we update the server data, we need to update the key/value pairs in the reverse
517                    // of the 'get' order so that any handle at any time has an ultimate link to a loaded asset.
518
519                    server.store.assets.insert(partial.cid, loaded_asset);
520
521                    // Update reverse dependencies
522                    for dep in server
523                        .store
524                        .assets
525                        .get(&partial.cid)
526                        .unwrap()
527                        .dependencies
528                        .iter()
529                    {
530                        server
531                            .store
532                            .reverse_dependencies
533                            .entry(*dep)
534                            .or_default()
535                            .insert(handle);
536                    }
537
538                    // Assets are gotten with handles so after this, the asset will be noticeably updated.
539                    let previous_cid = server.store.asset_ids.insert(handle, partial.cid);
540
541                    // We can then remove any old data that we're not using anymore.
542                    // If there is already loaded asset data for this path
543                    if let Some(cid) = previous_cid {
544                        // If no other handles use this content
545                        if server.store.asset_ids.iter().all(|map| *map.value() != cid) {
546                            // Remove the old asset data
547                            tracing::debug!(?cid, "Removing asset content");
548                            let (_, previous_asset) = server.store.assets.remove(&cid).unwrap();
549
550                            // Remove the previous asset's reverse dependencies.
551                            //
552                            // aka. now that we are removing the old asset, none of the assets that the
553                            // old asset dependended on should have a reverse dependency record saying that
554                            // this asset depends on it.
555                            //
556                            // In other words, this asset is removed and doesn't depend on anything else
557                            // anymore.
558                            for dep in previous_asset.dependencies.iter() {
559                                server
560                                    .store
561                                    .reverse_dependencies
562                                    .get_mut(dep)
563                                    .unwrap()
564                                    .remove(&handle);
565                            }
566                        }
567
568                        // If there are any assets that depended on this asset, they now need to be re-loaded.
569                        if let Some(rev_deps) = server.store.reverse_dependencies.get(&handle) {
570                            for dep in rev_deps.iter() {
571                                server
572                                    .asset_change_send
573                                    .try_send(ChangedAsset::Handle(*dep))
574                                    .unwrap();
575                            }
576                        }
577                    }
578
579                    server.load_progress.inc_loaded();
580
581                    Ok::<_, anyhow::Error>(())
582                }
583                .await;
584
585                if let Err(e) = result {
586                    server.load_progress.inc_errored();
587                    tracing::error!("Error loading asset: {e}");
588                }
589            })
590            .detach();
591        }
592
593        handle
594    }
595
596    async fn load_metadata_asset<'a>(
597        &'a self,
598        loc: AssetLocRef<'a>,
599        contents: &[u8],
600    ) -> anyhow::Result<PartialAsset> {
601        // Get the schema for the asset
602        let filename = loc
603            .path
604            .file_name()
605            .ok_or_else(|| anyhow::format_err!("Invalid asset filename"))?
606            .to_str()
607            .ok_or_else(|| anyhow::format_err!("Invalid unicode in filename"))?;
608        let before_extension = filename.rsplit_once('.').unwrap().0;
609        let schema_name = before_extension
610            .rsplit_once('.')
611            .map(|x| x.1)
612            .unwrap_or(before_extension);
613        let schema = SCHEMA_REGISTRY
614            .schemas
615            .iter()
616            .find(|schema| {
617                schema
618                    .type_data
619                    .get::<AssetKind>()
620                    .and_then(|asset_kind| match asset_kind {
621                        AssetKind::Metadata { extension } => Some(extension == schema_name),
622                        _ => None,
623                    })
624                    .unwrap_or(false)
625            })
626            .ok_or_else(|| LoaderNotFound {
627                name: schema_name.into(),
628            })?;
629        let mut dependencies = Vec::new();
630
631        let mut cid = Cid::default();
632
633        // NOTE: If changing cid computation logic, please update `CidDebugTrace` impl if possible.
634        //
635        // Tracks inputs to asset cid for debug tracing.
636        #[cfg(feature = "cid_debug_trace")]
637        let mut cid_debug = CidDebugTrace::new(schema.full_name, loc.path);
638
639        // Use the schema name and the file contents to create a unique, content-addressed ID for
640        // the asset.
641        cid.update(schema.full_name.as_bytes());
642
643        #[cfg(feature = "cid_debug_trace")]
644        {
645            cid_debug.cid_after_schema_fullname = cid;
646        }
647
648        cid.update(contents);
649
650        #[cfg(feature = "cid_debug_trace")]
651        {
652            cid_debug.cid_after_contents = cid;
653        }
654
655        let loader = MetaAssetLoadCtx {
656            server: self,
657            loc,
658            schema,
659            dependencies: &mut dependencies,
660        };
661        let data = if loc.path.extension().unwrap().to_str().unwrap() == "json" {
662            let mut deserializer = serde_json::Deserializer::from_slice(contents);
663            loader.deserialize(&mut deserializer)?
664        } else {
665            let deserializer = serde_yaml::Deserializer::from_slice(contents);
666            loader.deserialize(deserializer)?
667        };
668
669        // Update Cid with the Cids of it's dependencies
670
671        // TODO: Dependencies should be sorted as a consistent order is required to
672        // compute the same cid. We can't sort Vec<UntypedHandle>, runtime IDs vary each run.
673        //
674        // dependencies.sort();
675
676        // For now, gather cids and sort them before update
677        let mut dep_cids: Vec<Cid> = vec![];
678        for dep in &dependencies {
679            let dep_cid = loop {
680                let listener = self.load_progress.listen();
681                let Some(cid) = self.store.asset_ids.get(dep) else {
682                    listener.await;
683                    continue;
684                };
685                break *cid;
686            };
687            // cid.update(dep_cid.0.as_slice());
688            dep_cids.push(dep_cid);
689        }
690
691        dep_cids.sort();
692        for dep_cid in dep_cids {
693            cid.update(dep_cid.0.as_slice());
694
695            #[cfg(feature = "cid_debug_trace")]
696            {
697                // TODO: Get dep_cid asset_loc for cid debug trace,
698                // It is currently None, needs fix / alterative idenifying metadata.
699
700                let asset_loc = self.store.assets.get(&cid).map(|x| x.loc.clone());
701                cid_debug.cid_after_deps.push((dep_cid, cid, asset_loc));
702            }
703        }
704
705        #[cfg(feature = "cid_debug_trace")]
706        {
707            // log asset cid trace
708            cid_debug.final_cid = cid;
709            info!("{cid_debug}");
710        }
711
712        Ok(PartialAsset {
713            cid,
714            dependencies,
715            data,
716        })
717    }
718
719    async fn load_data_asset<'a>(
720        &self,
721        loc: AssetLocRef<'a>,
722        contents: &'a [u8],
723    ) -> anyhow::Result<PartialAsset> {
724        // Get the schema for the asset
725        let filename = loc
726            .path
727            .file_name()
728            .ok_or_else(|| anyhow::format_err!("Invalid asset filename"))?
729            .to_str()
730            .ok_or_else(|| anyhow::format_err!("Invalid unicode in filename"))?;
731        let (_name, extension) = filename.split_once('.').unwrap();
732        let (loader, schema) = SCHEMA_REGISTRY
733            .schemas
734            .iter()
735            .find_map(|schema| {
736                if let Some(asset_kind) = schema.type_data.get::<AssetKind>() {
737                    match asset_kind {
738                        AssetKind::Custom { extensions, loader } => {
739                            if extensions
740                                .iter()
741                                .any(|ext| ext == extension || ext == filename)
742                            {
743                                Some((loader, schema))
744                            } else {
745                                None
746                            }
747                        }
748                        _ => None,
749                    }
750                } else {
751                    None
752                }
753            })
754            .ok_or_else(|| LoaderNotFound {
755                name: extension.into(),
756            })?;
757
758        let dependencies = Arc::new(AppendOnlyVec::new());
759
760        let mut cid = Cid::default();
761        // Use the schema name and the file contents to create a unique, content-addressed ID for
762        // the asset.
763        cid.update(schema.full_name.as_bytes());
764        cid.update(contents);
765
766        let ctx = AssetLoadCtx {
767            asset_server: self.clone(),
768            loc: loc.to_owned(),
769            dependencies: dependencies.clone(),
770        };
771        let sbox = loader.load(ctx, contents).await?;
772
773        // Update Cid with the Cids of it's dependencies
774        let dependencies = dependencies.iter().cloned().collect::<Vec<_>>();
775
776        // TODO: Dependencies should be sorted as a consistent order is required to
777        // compute the same cid. We can't sort Vec<UntypedHandle>, runtime IDs vary each run.
778        //
779        // dependencies.sort();
780
781        // For now, gather cids and sort them before update
782        let mut dep_cids: Vec<Cid> = vec![];
783
784        for dep in &dependencies {
785            let dep_cid = loop {
786                let listener = self.load_progress.listen();
787                let Some(cid) = self.store.asset_ids.get(dep) else {
788                    listener.await;
789                    continue;
790                };
791                break *cid;
792            };
793            dep_cids.push(dep_cid);
794            // cid.update(dep_cid.0.as_slice());
795        }
796        dep_cids.sort();
797        for dep_cid in dep_cids {
798            cid.update(dep_cid.0.as_slice());
799        }
800
801        Ok(PartialAsset {
802            cid,
803            data: sbox,
804            dependencies,
805        })
806    }
807
808    /// Borrow a [`LoadedAsset`] associated to the given handle.
809    pub fn get_asset_untyped(&self, handle: UntypedHandle) -> Option<MapRef<'_, Cid, LoadedAsset>> {
810        let cid = self.store.asset_ids.get(&handle)?;
811        self.store.assets.get(&cid)
812    }
813
814    /// Borrow a [`LoadedAsset`] associated to the given handle.
815    pub fn get_asset_untyped_mut(
816        &self,
817        handle: UntypedHandle,
818    ) -> Option<MapRefMut<'_, Cid, LoadedAsset>> {
819        let cid = self.store.asset_ids.get(&handle)?;
820        self.store.assets.get_mut(&cid)
821    }
822
823    /// Read the core asset pack.
824    ///
825    /// # Panics
826    ///
827    /// Panics if the assets have not be loaded yet with [`AssetServer::load_assets`].
828    #[track_caller]
829    pub fn core(&self) -> MappedMutexGuard<'_, AssetPack> {
830        MutexGuard::map(self.store.core_pack.lock(), |x| x.as_mut().unwrap())
831    }
832
833    /// Get the core asset pack's root asset.
834    pub fn root<T: HasSchema>(&self) -> MappedMapRef<'_, Cid, LoadedAsset, T> {
835        self.get(self.core().root.typed())
836    }
837
838    /// Get the core asset pack's root asset as a type-erased [`SchemaBox`].
839    pub fn untyped_root(&self) -> MappedMapRef<'_, Cid, LoadedAsset, SchemaBox> {
840        self.get_untyped(self.core().root)
841    }
842
843    /// Read the loaded asset packs.
844    pub fn packs(&self) -> &DashMap<AssetPackSpec, AssetPack> {
845        &self.store.packs
846    }
847
848    /// Borrow a loaded asset.
849    ///
850    /// # Panics
851    ///
852    /// Panics if the asset is not loaded or if the asset with the given handle doesn't have a
853    /// schema matching `T`.
854    #[track_caller]
855    pub fn get<T: HasSchema>(&self, handle: Handle<T>) -> MappedMapRef<'_, Cid, LoadedAsset, T> {
856        self.try_get(handle)
857            .expect("asset not found (handle has no cid)")
858            .expect("asset does not have matching schema for given type")
859    }
860
861    /// Borrow a loaded asset.
862    ///
863    /// # Panics
864    ///
865    /// Panics if the asset is not loaded.
866    #[track_caller]
867    pub fn get_untyped(
868        &self,
869        handle: UntypedHandle,
870    ) -> MappedMapRef<'_, Cid, LoadedAsset, SchemaBox> {
871        self.try_get_untyped(handle).unwrap()
872    }
873
874    /// Borrow a loaded asset.
875    ///
876    /// # Panics
877    ///
878    /// Panics if the asset is not loaded.
879    #[track_caller]
880    pub fn get_untyped_mut(
881        &self,
882        handle: UntypedHandle,
883    ) -> MappedMapRefMut<'_, Cid, LoadedAsset, SchemaBox> {
884        self.try_get_untyped_mut(handle).unwrap()
885    }
886
887    /// Borrow a loaded asset.
888    ///
889    /// Returns None if asset not found and Some([`SchemaMismatchError`]) if type cast fails.
890    pub fn try_get<T: HasSchema>(
891        &self,
892        handle: Handle<T>,
893    ) -> Option<
894        Result<
895            MappedMapRef<'_, Cid, LoadedAsset, T, std::collections::hash_map::RandomState>,
896            SchemaMismatchError,
897        >,
898    > {
899        let cid = self.store.asset_ids.get(&handle.untyped())?;
900        Some(
901            MapRef::try_map(self.store.assets.get(&cid).unwrap(), |x| {
902                let asset = &x.data;
903
904                // If this is a handle to a schema box, then return the schema box directly without casting
905                if T::schema() == <SchemaBox as HasSchema>::schema() {
906                    // SOUND: the above comparison verifies that T is concretely a SchemaBox so &Schemabox
907                    // is the same as &T.
908                    Some(unsafe { std::mem::transmute::<&SchemaBox, &T>(asset) })
909                } else {
910                    asset.try_cast_ref().ok()
911                }
912            })
913            .map_err(|_| SchemaMismatchError),
914        )
915    }
916
917    /// Borrow a loaded asset.
918    pub fn try_get_untyped(
919        &self,
920        handle: UntypedHandle,
921    ) -> Option<MappedMapRef<'_, Cid, LoadedAsset, SchemaBox>> {
922        let cid = self.store.asset_ids.get(&handle)?;
923        Some(MapRef::map(self.store.assets.get(&cid).unwrap(), |x| {
924            &x.data
925        }))
926    }
927
928    /// Borrow a loaded asset.
929    pub fn try_get_untyped_mut(
930        &self,
931        handle: UntypedHandle,
932    ) -> Option<MappedMapRefMut<'_, Cid, LoadedAsset, SchemaBox>> {
933        let cid = self.store.asset_ids.get_mut(&handle)?;
934        Some(MapRefMut::map(
935            self.store.assets.get_mut(&cid).unwrap(),
936            |x| &mut x.data,
937        ))
938    }
939
940    /// Get handle of loaded asset from content id [`Cid`].
941    pub fn get_handle_from_cid<T>(&self, cid: &Cid) -> Handle<T> {
942        self.get_untyped_handle_from_cid(cid).typed()
943    }
944
945    /// Get untyped handle of loaded asset from content id [`Cid`].
946    pub fn get_untyped_handle_from_cid(&self, cid: &Cid) -> UntypedHandle {
947        self.try_get_untyped_handle_from_cid(cid).unwrap()
948    }
949
950    /// Try to get handle of loaded asset from content id [`Cid`].
951    pub fn try_get_handle_from_cid<T>(&self, cid: &Cid) -> Option<Handle<T>> {
952        Some(self.try_get_untyped_handle_from_cid(cid)?.typed())
953    }
954
955    /// Try to get untyped handle of loaded asset from content id [`Cid`].
956    pub fn try_get_untyped_handle_from_cid(&self, cid: &Cid) -> Option<UntypedHandle> {
957        let loaded_asset = self.store.assets.get(cid)?;
958        Some(*self.store.path_handles.get(&loaded_asset.loc)?)
959    }
960
961    /// Mutably borrow a loaded asset.
962    ///
963    /// # Panics
964    ///
965    /// Panics if the asset is not loaded or if the asset asset with the given handle doesn't have a
966    /// schema matching `T`.
967    #[track_caller]
968    pub fn get_mut<T: HasSchema>(
969        &mut self,
970        handle: &Handle<T>,
971    ) -> MappedMapRefMut<'_, Cid, LoadedAsset, T> {
972        let cid = self
973            .store
974            .asset_ids
975            .get(&handle.untyped())
976            .expect(NO_ASSET_MSG);
977        MapRefMut::map(self.store.assets.get_mut(&cid).unwrap(), |x| {
978            let asset = &mut x.data;
979
980            // If this is a handle to a schema box, then return the schema box directly without casting
981            if T::schema() == <SchemaBox as HasSchema>::schema() {
982                // SOUND: the above comparison verifies that T is concretely a SchemaBox so &mut Schemabox
983                // is the same as &mut T.
984                unsafe { std::mem::transmute::<&mut SchemaBox, &mut T>(asset) }
985            } else {
986                asset.cast_mut()
987            }
988        })
989    }
990
991    /// Load the schemas for an asset pack.
992    async fn load_pack_schemas(
993        &self,
994        pack: Option<&str>,
995        schema_paths: &[PathBuf],
996    ) -> anyhow::Result<Vec<&'static Schema>> {
997        let mut schemas = Vec::with_capacity(schema_paths.len());
998        for schema_path in schema_paths {
999            let contents = self
1000                .io
1001                .load_file(AssetLocRef {
1002                    path: schema_path,
1003                    pack,
1004                })
1005                .await?;
1006
1007            let pack_schema: schema_loader::PackSchema = serde_yaml::from_slice(&contents)?;
1008            let schema = pack_schema.0;
1009            tracing::debug!(?pack, ?schema.name, "Loaded schema from pack.");
1010            schemas.push(schema);
1011        }
1012        Ok(schemas)
1013    }
1014
1015    /// Get the game version config, used when making sure asset packs are compatible with this
1016    /// game version.
1017    pub fn game_version(&self) -> Version {
1018        self.game_version.lock().unwrap().clone()
1019    }
1020
1021    /// Set the game version config, used when making sure asset packs are compatible with this
1022    /// game version.
1023    pub fn set_game_version(&self, version: Version) {
1024        *self.game_version.lock().unwrap() = version;
1025    }
1026}
1027
1028/// Partial of a [`LoadedAsset`] used internally while loading is in progress.
1029struct PartialAsset {
1030    pub cid: Cid,
1031    pub data: SchemaBox,
1032    pub dependencies: Vec<UntypedHandle>,
1033}
1034
1035const NO_ASSET_MSG: &str = "Asset not loaded";
1036fn path_is_metadata(path: &Path) -> bool {
1037    let Some(ext) = path.extension().and_then(|ext| ext.to_str()) else {
1038        return false;
1039    };
1040    ext == "yaml" || ext == "yml" || ext == "json"
1041}
1042
1043pub use metadata::*;
1044mod metadata {
1045    use bones_utils::LabeledId;
1046    use serde::de::{DeserializeSeed, Error, Unexpected, VariantAccess, Visitor};
1047    use ustr::{ustr, Ustr};
1048
1049    use super::*;
1050
1051    /// Context provided while loading a metadata asset.
1052    pub struct MetaAssetLoadCtx<'srv> {
1053        /// The asset server.
1054        pub server: &'srv AssetServer,
1055        /// The dependency list of this asset. This should be updated by asset loaders as
1056        /// dependencies are added.
1057        pub dependencies: &'srv mut Vec<UntypedHandle>,
1058        /// The location that the asset is being loaded from.
1059        pub loc: AssetLocRef<'srv>,
1060        /// The schema of the asset being loaded.
1061        pub schema: &'static Schema,
1062    }
1063
1064    impl<'asset, 'de> DeserializeSeed<'de> for MetaAssetLoadCtx<'asset> {
1065        type Value = SchemaBox;
1066
1067        fn deserialize<D>(mut self, deserializer: D) -> Result<Self::Value, D::Error>
1068        where
1069            D: serde::Deserializer<'de>,
1070        {
1071            // Allocate the object.
1072            let mut ptr = SchemaBox::default(self.schema);
1073
1074            SchemaPtrLoadCtx {
1075                ctx: &mut self,
1076                ptr: ptr.as_mut(),
1077            }
1078            .deserialize(deserializer)?;
1079
1080            Ok(ptr)
1081        }
1082    }
1083
1084    /// The load context for a [`SchemaRefMut`].
1085    pub struct SchemaPtrLoadCtx<'a, 'srv, 'ptr> {
1086        /// The metadata asset load context.
1087        pub ctx: &'a mut MetaAssetLoadCtx<'srv>,
1088        /// The pointer to load.
1089        pub ptr: SchemaRefMut<'ptr>,
1090    }
1091
1092    impl<'a, 'srv, 'ptr, 'de> DeserializeSeed<'de> for SchemaPtrLoadCtx<'a, 'srv, 'ptr> {
1093        type Value = ();
1094
1095        fn deserialize<D>(mut self, deserializer: D) -> Result<Self::Value, D::Error>
1096        where
1097            D: serde::Deserializer<'de>,
1098        {
1099            // Load asset handles.
1100            if self
1101                .ptr
1102                .schema()
1103                .type_data
1104                .get::<SchemaAssetHandle>()
1105                .is_some()
1106            {
1107                let path_string = String::deserialize(deserializer)?;
1108                let mut pack = self.ctx.loc.pack.map(|x| x.to_owned());
1109                let relative_path;
1110                if let Some((pack_prefix, path)) = path_string.split_once(':') {
1111                    let pack_id = LabeledId::new(pack_prefix).map_err(|e| D::Error::custom(format!("Error parsing pack prefix while parsing asset path `{path_string}`: {e}")))?;
1112                    if pack_prefix == "core" {
1113                        pack = None;
1114                    } else {
1115                        pack = Some(self.ctx
1116                            .server
1117                            .store
1118                            .pack_dirs
1119                            .iter()
1120                            .find(|x| x.value().id == pack_id)
1121                            .map(|x| x.key().to_owned())
1122                            .ok_or_else(|| {
1123                                D::Error::custom(format!("Dependent pack {pack_id} not loaded when trying to load asset with path: {path_string}."))
1124                            })?);
1125                    }
1126                    relative_path = path;
1127                } else {
1128                    relative_path = &path_string;
1129                };
1130                let relative_path = PathBuf::from(relative_path);
1131                let path = relative_path
1132                    .absolutize_from(self.ctx.loc.path.parent().unwrap())
1133                    .unwrap();
1134                let handle = self.ctx.server.load_asset((&*path, pack.as_deref()).into());
1135                self.ctx.dependencies.push(handle);
1136                *self
1137                    .ptr
1138                    .try_cast_mut()
1139                    .map_err(|e| D::Error::custom(e.to_string()))? = handle;
1140                return Ok(());
1141            }
1142
1143            // Use custom asset load or deserialize implementation if present.
1144            if let Some(custom_loader) = self.ptr.schema().type_data.get::<SchemaMetaAssetLoader>()
1145            {
1146                return custom_loader
1147                    .load(self.ctx, self.ptr, deserializer)
1148                    .map_err(|e| D::Error::custom(e.to_string()));
1149            } else if let Some(schema_deserialize) =
1150                self.ptr.schema().type_data.get::<SchemaDeserialize>()
1151            {
1152                return schema_deserialize.deserialize(self.ptr, deserializer);
1153            }
1154
1155            match &self.ptr.schema().kind {
1156                SchemaKind::Struct(s) => {
1157                    // If this is a newtype struct
1158                    if s.fields.len() == 1 && s.fields[0].name.is_none() {
1159                        // Deserialize it as the inner type
1160                        // SOUND: it is safe to cast a struct with one field to it's field type
1161                        let ptr = unsafe {
1162                            SchemaRefMut::from_ptr_schema(self.ptr.as_ptr(), s.fields[0].schema)
1163                        };
1164                        SchemaPtrLoadCtx { ptr, ctx: self.ctx }.deserialize(deserializer)?
1165                    } else {
1166                        deserializer.deserialize_any(StructVisitor {
1167                            ptr: self.ptr,
1168                            ctx: self.ctx,
1169                        })?
1170                    }
1171                }
1172                SchemaKind::Vec(_) => deserializer.deserialize_seq(VecVisitor {
1173                    ptr: self.ptr,
1174                    ctx: self.ctx,
1175                })?,
1176                SchemaKind::Map { .. } => deserializer.deserialize_map(MapVisitor {
1177                    ptr: self.ptr,
1178                    ctx: self.ctx,
1179                })?,
1180                SchemaKind::Enum(_) => deserializer.deserialize_any(EnumVisitor {
1181                    ptr: self.ptr,
1182                    ctx: self.ctx,
1183                })?,
1184                SchemaKind::Box(_) => SchemaPtrLoadCtx {
1185                    ctx: self.ctx,
1186                    ptr: self.ptr.into_box().unwrap(),
1187                }
1188                .deserialize(deserializer)?,
1189                SchemaKind::Primitive(p) => {
1190                    match p {
1191                        Primitive::Bool => *self.ptr.cast_mut() = bool::deserialize(deserializer)?,
1192                        Primitive::U8 => *self.ptr.cast_mut() = u8::deserialize(deserializer)?,
1193                        Primitive::U16 => *self.ptr.cast_mut() = u16::deserialize(deserializer)?,
1194                        Primitive::U32 => *self.ptr.cast_mut() = u32::deserialize(deserializer)?,
1195                        Primitive::U64 => *self.ptr.cast_mut() = u64::deserialize(deserializer)?,
1196                        Primitive::U128 => *self.ptr.cast_mut() = u128::deserialize(deserializer)?,
1197                        Primitive::I8 => *self.ptr.cast_mut() = i8::deserialize(deserializer)?,
1198                        Primitive::I16 => *self.ptr.cast_mut() = i16::deserialize(deserializer)?,
1199                        Primitive::I32 => *self.ptr.cast_mut() = i32::deserialize(deserializer)?,
1200                        Primitive::I64 => *self.ptr.cast_mut() = i64::deserialize(deserializer)?,
1201                        Primitive::I128 => *self.ptr.cast_mut() = i128::deserialize(deserializer)?,
1202                        Primitive::F32 => *self.ptr.cast_mut() = f32::deserialize(deserializer)?,
1203                        Primitive::F64 => *self.ptr.cast_mut() = f64::deserialize(deserializer)?,
1204                        Primitive::String => {
1205                            *self.ptr.cast_mut() = String::deserialize(deserializer)?
1206                        }
1207                        Primitive::Opaque { .. } => {
1208                            return Err(D::Error::custom(
1209                                "Opaque types must be #[repr(C)] or have `SchemaDeserialize` type data in order \
1210                                to be loaded in a metadata asset.",
1211                            ));
1212                        }
1213                    };
1214                }
1215            };
1216
1217            Ok(())
1218        }
1219    }
1220
1221    struct StructVisitor<'a, 'srv, 'ptr> {
1222        ctx: &'a mut MetaAssetLoadCtx<'srv>,
1223        ptr: SchemaRefMut<'ptr>,
1224    }
1225
1226    impl<'a, 'srv, 'ptr, 'de> Visitor<'de> for StructVisitor<'a, 'srv, 'ptr> {
1227        type Value = ();
1228
1229        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1230            // TODO: Write very verbose error messages for metadata asset deserializers.
1231            // The schema describes the type that we are trying to deserialize, so we should
1232            // use that information to format a nice-to-read error message that documents
1233            // what data it's expecting.
1234            write!(
1235                formatter,
1236                "asset metadata matching the schema: {}",
1237                self.ptr.schema().full_name
1238            )
1239        }
1240
1241        fn visit_seq<A>(mut self, mut seq: A) -> Result<Self::Value, A::Error>
1242        where
1243            A: serde::de::SeqAccess<'de>,
1244        {
1245            let field_count = self.ptr.schema().kind.as_struct().unwrap().fields.len();
1246
1247            for i in 0..field_count {
1248                let field = self.ptr.access_mut().field(i).unwrap();
1249                if seq
1250                    .next_element_seed(SchemaPtrLoadCtx {
1251                        ctx: self.ctx,
1252                        ptr: field.into_schema_ref_mut(),
1253                    })?
1254                    .is_none()
1255                {
1256                    break;
1257                }
1258            }
1259
1260            Ok(())
1261        }
1262
1263        fn visit_map<A>(mut self, mut map: A) -> Result<Self::Value, A::Error>
1264        where
1265            A: serde::de::MapAccess<'de>,
1266        {
1267            while let Some(key) = map.next_key::<String>()? {
1268                match self.ptr.access_mut().field(&key) {
1269                    Ok(field) => {
1270                        map.next_value_seed(SchemaPtrLoadCtx {
1271                            ctx: self.ctx,
1272                            ptr: field.into_schema_ref_mut(),
1273                        })?;
1274                    }
1275                    Err(_) => {
1276                        let fields = &self.ptr.schema().kind.as_struct().unwrap().fields;
1277                        let mut msg = format!("unknown field `{key}`, ");
1278                        if !fields.is_empty() {
1279                            msg += "expected one of ";
1280                            for (i, field) in fields.iter().enumerate() {
1281                                msg += &field
1282                                    .name
1283                                    .as_ref()
1284                                    .map(|x| format!("`{x}`"))
1285                                    .unwrap_or_else(|| format!("`{i}`"));
1286                                if i < fields.len() - 1 {
1287                                    msg += ", "
1288                                }
1289                            }
1290                        } else {
1291                            msg += "there are no fields"
1292                        }
1293                        return Err(A::Error::custom(msg));
1294                    }
1295                }
1296            }
1297
1298            Ok(())
1299        }
1300    }
1301
1302    struct VecVisitor<'a, 'srv, 'ptr> {
1303        ctx: &'a mut MetaAssetLoadCtx<'srv>,
1304        ptr: SchemaRefMut<'ptr>,
1305    }
1306
1307    impl<'a, 'srv, 'ptr, 'de> Visitor<'de> for VecVisitor<'a, 'srv, 'ptr> {
1308        type Value = ();
1309
1310        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1311            // TODO: Write very verbose error messages for metadata asset deserializers.
1312            write!(
1313                formatter,
1314                "asset metadata matching the schema: {}",
1315                self.ptr.schema().full_name
1316            )
1317        }
1318
1319        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1320        where
1321            A: serde::de::SeqAccess<'de>,
1322        {
1323            // SOUND: schema asserts this is a SchemaVec.
1324            let v = unsafe { &mut *(self.ptr.as_ptr() as *mut SchemaVec) };
1325            loop {
1326                let item_schema = v.schema();
1327                let mut item = SchemaBox::default(item_schema);
1328                let item_ref = item.as_mut();
1329                if seq
1330                    .next_element_seed(SchemaPtrLoadCtx {
1331                        ctx: self.ctx,
1332                        ptr: item_ref,
1333                    })?
1334                    .is_none()
1335                {
1336                    break;
1337                }
1338                v.push_box(item);
1339            }
1340
1341            Ok(())
1342        }
1343    }
1344
1345    struct MapVisitor<'a, 'srv, 'ptr> {
1346        ctx: &'a mut MetaAssetLoadCtx<'srv>,
1347        ptr: SchemaRefMut<'ptr>,
1348    }
1349
1350    impl<'a, 'srv, 'ptr, 'de> Visitor<'de> for MapVisitor<'a, 'srv, 'ptr> {
1351        type Value = ();
1352
1353        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1354            // TODO: Write very verbose error messages for metadata asset deserializers.
1355            write!(
1356                formatter,
1357                "asset metadata matching the schema: {}",
1358                self.ptr.schema().full_name
1359            )
1360        }
1361
1362        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1363        where
1364            A: serde::de::MapAccess<'de>,
1365        {
1366            // SOUND: schema asserts this is a SchemaMap.
1367            let v = unsafe { &mut *(self.ptr.as_ptr() as *mut SchemaMap) };
1368            let is_ustr = v.key_schema() == Ustr::schema();
1369            if v.key_schema() != String::schema() && !is_ustr {
1370                return Err(A::Error::custom(
1371                    "Can only deserialize maps with `String` or `Ustr` keys.",
1372                ));
1373            }
1374            while let Some(key) = map.next_key::<String>()? {
1375                let key = if is_ustr {
1376                    SchemaBox::new(ustr(&key))
1377                } else {
1378                    SchemaBox::new(key)
1379                };
1380                let mut value = SchemaBox::default(v.value_schema());
1381                map.next_value_seed(SchemaPtrLoadCtx {
1382                    ctx: self.ctx,
1383                    ptr: value.as_mut(),
1384                })?;
1385
1386                v.insert_box(key, value);
1387            }
1388            Ok(())
1389        }
1390    }
1391
1392    struct EnumVisitor<'a, 'srv, 'ptr> {
1393        ctx: &'a mut MetaAssetLoadCtx<'srv>,
1394        ptr: SchemaRefMut<'ptr>,
1395    }
1396
1397    impl<'a, 'srv, 'ptr, 'de> Visitor<'de> for EnumVisitor<'a, 'srv, 'ptr> {
1398        type Value = ();
1399
1400        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1401            // TODO: Write very verbose error messages for metadata asset deserializers.
1402            write!(
1403                formatter,
1404                "asset metadata matching the schema: {}",
1405                self.ptr.schema().full_name
1406            )
1407        }
1408
1409        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1410        where
1411            E: Error,
1412        {
1413            let enum_info = self.ptr.schema().kind.as_enum().unwrap();
1414            let var_idx = enum_info
1415                .variants
1416                .iter()
1417                .position(|x| x.name == v)
1418                .ok_or_else(|| E::invalid_value(Unexpected::Str(v), &self))?;
1419
1420            if !enum_info.variants[var_idx]
1421                .schema
1422                .kind
1423                .as_struct()
1424                .unwrap()
1425                .fields
1426                .is_empty()
1427            {
1428                return Err(E::custom(format!(
1429                    "Cannot deserialize enum variant with fields from string: {v}"
1430                )));
1431            }
1432
1433            // SOUND: we match the cast with the enum tag type.
1434            unsafe {
1435                match enum_info.tag_type {
1436                    EnumTagType::U8 => self.ptr.as_ptr().cast::<u8>().write(var_idx as u8),
1437                    EnumTagType::U16 => self.ptr.as_ptr().cast::<u16>().write(var_idx as u16),
1438                    EnumTagType::U32 => self.ptr.as_ptr().cast::<u32>().write(var_idx as u32),
1439                }
1440            }
1441
1442            Ok(())
1443        }
1444
1445        fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
1446        where
1447            A: serde::de::EnumAccess<'de>,
1448        {
1449            let (value_ptr, var_access) = data.variant_seed(EnumPtrLoadCtx { ptr: self.ptr })?;
1450
1451            var_access.newtype_variant_seed(SchemaPtrLoadCtx {
1452                ctx: self.ctx,
1453                ptr: value_ptr,
1454            })?;
1455
1456            Ok(())
1457        }
1458    }
1459
1460    struct EnumPtrLoadCtx<'ptr> {
1461        ptr: SchemaRefMut<'ptr>,
1462    }
1463
1464    impl<'ptr, 'de> DeserializeSeed<'de> for EnumPtrLoadCtx<'ptr> {
1465        type Value = SchemaRefMut<'ptr>;
1466
1467        fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1468        where
1469            D: serde::Deserializer<'de>,
1470        {
1471            let var_name = String::deserialize(deserializer)?;
1472            let enum_info = self.ptr.schema().kind.as_enum().unwrap();
1473            let value_offset = self.ptr.schema().field_offsets()[0].1;
1474            let (var_idx, var_schema) = enum_info
1475                .variants
1476                .iter()
1477                .enumerate()
1478                .find_map(|(idx, info)| (info.name == var_name).then_some((idx, info.schema)))
1479                .ok_or_else(|| {
1480                    D::Error::custom(format!(
1481                        "Unknown enum variant `{var_name}`, expected one of: {}",
1482                        enum_info
1483                            .variants
1484                            .iter()
1485                            .map(|x| format!("`{}`", x.name))
1486                            .collect::<Vec<_>>()
1487                            .join(", ")
1488                    ))
1489                })?;
1490
1491            // Write the enum variant
1492            // SOUND: the schema asserts that the write to the enum discriminant is valid
1493            match enum_info.tag_type {
1494                EnumTagType::U8 => unsafe { self.ptr.as_ptr().cast::<u8>().write(var_idx as u8) },
1495                EnumTagType::U16 => unsafe {
1496                    self.ptr.as_ptr().cast::<u16>().write(var_idx as u16)
1497                },
1498                EnumTagType::U32 => unsafe {
1499                    self.ptr.as_ptr().cast::<u32>().write(var_idx as u32)
1500                },
1501            }
1502
1503            if var_schema.kind.as_struct().is_none() {
1504                return Err(D::Error::custom(
1505                    "All enum variant types must have a struct Schema",
1506                ));
1507            }
1508
1509            unsafe {
1510                Ok(SchemaRefMut::from_ptr_schema(
1511                    self.ptr.as_ptr().add(value_offset),
1512                    var_schema,
1513                ))
1514            }
1515        }
1516    }
1517}