bevy_asset/
loader.rs

1use crate::{
2    io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader},
3    loader_builders::{Deferred, NestedLoader, StaticTyped},
4    meta::{AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal, Settings},
5    path::AssetPath,
6    Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, UntypedAssetId,
7    UntypedHandle,
8};
9use alloc::{
10    boxed::Box,
11    string::{String, ToString},
12    vec::Vec,
13};
14use atomicow::CowArc;
15use bevy_ecs::world::World;
16use bevy_platform::collections::{HashMap, HashSet};
17use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
18use core::any::{Any, TypeId};
19use downcast_rs::{impl_downcast, Downcast};
20use ron::error::SpannedError;
21use serde::{Deserialize, Serialize};
22use std::path::{Path, PathBuf};
23use thiserror::Error;
24
25/// Loads an [`Asset`] from a given byte [`Reader`]. This can accept [`AssetLoader::Settings`], which configure how the [`Asset`]
26/// should be loaded.
27///
28/// This trait is generally used in concert with [`AssetReader`](crate::io::AssetReader) to load assets from a byte source.
29///
30/// For a complementary version of this trait that can save assets, see [`AssetSaver`](crate::saver::AssetSaver).
31pub trait AssetLoader: Send + Sync + 'static {
32    /// The top level [`Asset`] loaded by this [`AssetLoader`].
33    type Asset: Asset;
34    /// The settings type used by this [`AssetLoader`].
35    type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
36    /// The type of [error](`std::error::Error`) which could be encountered by this loader.
37    type Error: Into<Box<dyn core::error::Error + Send + Sync + 'static>>;
38    /// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`].
39    fn load(
40        &self,
41        reader: &mut dyn Reader,
42        settings: &Self::Settings,
43        load_context: &mut LoadContext,
44    ) -> impl ConditionalSendFuture<Output = Result<Self::Asset, Self::Error>>;
45
46    /// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot.
47    /// Note that users of this [`AssetLoader`] may choose to load files with a non-matching extension.
48    fn extensions(&self) -> &[&str] {
49        &[]
50    }
51}
52
53/// Provides type-erased access to an [`AssetLoader`].
54pub trait ErasedAssetLoader: Send + Sync + 'static {
55    /// Asynchronously loads the asset(s) from the bytes provided by [`Reader`].
56    fn load<'a>(
57        &'a self,
58        reader: &'a mut dyn Reader,
59        meta: &'a dyn AssetMetaDyn,
60        load_context: LoadContext<'a>,
61    ) -> BoxedFuture<
62        'a,
63        Result<ErasedLoadedAsset, Box<dyn core::error::Error + Send + Sync + 'static>>,
64    >;
65
66    /// Returns a list of extensions supported by this asset loader, without the preceding dot.
67    fn extensions(&self) -> &[&str];
68    /// Deserializes metadata from the input `meta` bytes into the appropriate type (erased as [`Box<dyn AssetMetaDyn>`]).
69    fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError>;
70    /// Returns the default meta value for the [`AssetLoader`] (erased as [`Box<dyn AssetMetaDyn>`]).
71    fn default_meta(&self) -> Box<dyn AssetMetaDyn>;
72    /// Returns the type name of the [`AssetLoader`].
73    fn type_name(&self) -> &'static str;
74    /// Returns the [`TypeId`] of the [`AssetLoader`].
75    fn type_id(&self) -> TypeId;
76    /// Returns the type name of the top-level [`Asset`] loaded by the [`AssetLoader`].
77    fn asset_type_name(&self) -> &'static str;
78    /// Returns the [`TypeId`] of the top-level [`Asset`] loaded by the [`AssetLoader`].
79    fn asset_type_id(&self) -> TypeId;
80}
81
82impl<L> ErasedAssetLoader for L
83where
84    L: AssetLoader + Send + Sync,
85{
86    /// Processes the asset in an asynchronous closure.
87    fn load<'a>(
88        &'a self,
89        reader: &'a mut dyn Reader,
90        meta: &'a dyn AssetMetaDyn,
91        mut load_context: LoadContext<'a>,
92    ) -> BoxedFuture<
93        'a,
94        Result<ErasedLoadedAsset, Box<dyn core::error::Error + Send + Sync + 'static>>,
95    > {
96        Box::pin(async move {
97            let settings = meta
98                .loader_settings()
99                .expect("Loader settings should exist")
100                .downcast_ref::<L::Settings>()
101                .expect("AssetLoader settings should match the loader type");
102            let asset = <L as AssetLoader>::load(self, reader, settings, &mut load_context)
103                .await
104                .map_err(Into::into)?;
105            Ok(load_context.finish(asset).into())
106        })
107    }
108
109    fn extensions(&self) -> &[&str] {
110        <L as AssetLoader>::extensions(self)
111    }
112
113    fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError> {
114        let meta = AssetMeta::<L, ()>::deserialize(meta)?;
115        Ok(Box::new(meta))
116    }
117
118    fn default_meta(&self) -> Box<dyn AssetMetaDyn> {
119        Box::new(AssetMeta::<L, ()>::new(crate::meta::AssetAction::Load {
120            loader: self.type_name().to_string(),
121            settings: L::Settings::default(),
122        }))
123    }
124
125    fn type_name(&self) -> &'static str {
126        core::any::type_name::<L>()
127    }
128
129    fn type_id(&self) -> TypeId {
130        TypeId::of::<L>()
131    }
132
133    fn asset_type_name(&self) -> &'static str {
134        core::any::type_name::<L::Asset>()
135    }
136
137    fn asset_type_id(&self) -> TypeId {
138        TypeId::of::<L::Asset>()
139    }
140}
141
142pub(crate) struct LabeledAsset {
143    pub(crate) asset: ErasedLoadedAsset,
144    pub(crate) handle: UntypedHandle,
145}
146
147/// The successful result of an [`AssetLoader::load`] call. This contains the loaded "root" asset and any other "labeled" assets produced
148/// by the loader. It also holds the input [`AssetMeta`] (if it exists) and tracks dependencies:
149/// * normal dependencies: dependencies that must be loaded as part of this asset load (ex: assets a given asset has handles to).
150/// * Loader dependencies: dependencies whose actual asset values are used during the load process
151pub struct LoadedAsset<A: Asset> {
152    pub(crate) value: A,
153    pub(crate) dependencies: HashSet<UntypedAssetId>,
154    pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
155    pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
156}
157
158impl<A: Asset> LoadedAsset<A> {
159    /// Create a new loaded asset. This will use [`VisitAssetDependencies`](crate::VisitAssetDependencies) to populate `dependencies`.
160    pub fn new_with_dependencies(value: A) -> Self {
161        let mut dependencies = <HashSet<_>>::default();
162        value.visit_dependencies(&mut |id| {
163            dependencies.insert(id);
164        });
165        LoadedAsset {
166            value,
167            dependencies,
168            loader_dependencies: HashMap::default(),
169            labeled_assets: HashMap::default(),
170        }
171    }
172
173    /// Cast (and take ownership) of the [`Asset`] value of the given type.
174    pub fn take(self) -> A {
175        self.value
176    }
177
178    /// Retrieves a reference to the internal [`Asset`] type.
179    pub fn get(&self) -> &A {
180        &self.value
181    }
182
183    /// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
184    pub fn get_labeled(
185        &self,
186        label: impl Into<CowArc<'static, str>>,
187    ) -> Option<&ErasedLoadedAsset> {
188        self.labeled_assets.get(&label.into()).map(|a| &a.asset)
189    }
190
191    /// Iterate over all labels for "labeled assets" in the loaded asset
192    pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
193        self.labeled_assets.keys().map(|s| &**s)
194    }
195}
196
197impl<A: Asset> From<A> for LoadedAsset<A> {
198    fn from(asset: A) -> Self {
199        LoadedAsset::new_with_dependencies(asset)
200    }
201}
202
203/// A "type erased / boxed" counterpart to [`LoadedAsset`]. This is used in places where the loaded type is not statically known.
204pub struct ErasedLoadedAsset {
205    pub(crate) value: Box<dyn AssetContainer>,
206    pub(crate) dependencies: HashSet<UntypedAssetId>,
207    pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
208    pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
209}
210
211impl<A: Asset> From<LoadedAsset<A>> for ErasedLoadedAsset {
212    fn from(asset: LoadedAsset<A>) -> Self {
213        ErasedLoadedAsset {
214            value: Box::new(asset.value),
215            dependencies: asset.dependencies,
216            loader_dependencies: asset.loader_dependencies,
217            labeled_assets: asset.labeled_assets,
218        }
219    }
220}
221
222impl ErasedLoadedAsset {
223    /// Cast (and take ownership) of the [`Asset`] value of the given type. This will return [`Some`] if
224    /// the stored type matches `A` and [`None`] if it does not.
225    pub fn take<A: Asset>(self) -> Option<A> {
226        self.value.downcast::<A>().map(|a| *a).ok()
227    }
228
229    /// Retrieves a reference to the internal [`Asset`] type, if it matches the type `A`. Otherwise returns [`None`].
230    pub fn get<A: Asset>(&self) -> Option<&A> {
231        self.value.downcast_ref::<A>()
232    }
233
234    /// Retrieves the [`TypeId`] of the stored [`Asset`] type.
235    pub fn asset_type_id(&self) -> TypeId {
236        (*self.value).type_id()
237    }
238
239    /// Retrieves the `type_name` of the stored [`Asset`] type.
240    pub fn asset_type_name(&self) -> &'static str {
241        self.value.asset_type_name()
242    }
243
244    /// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
245    pub fn get_labeled(
246        &self,
247        label: impl Into<CowArc<'static, str>>,
248    ) -> Option<&ErasedLoadedAsset> {
249        self.labeled_assets.get(&label.into()).map(|a| &a.asset)
250    }
251
252    /// Iterate over all labels for "labeled assets" in the loaded asset
253    pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
254        self.labeled_assets.keys().map(|s| &**s)
255    }
256
257    /// Cast this loaded asset as the given type. If the type does not match,
258    /// the original type-erased asset is returned.
259    pub fn downcast<A: Asset>(mut self) -> Result<LoadedAsset<A>, ErasedLoadedAsset> {
260        match self.value.downcast::<A>() {
261            Ok(value) => Ok(LoadedAsset {
262                value: *value,
263                dependencies: self.dependencies,
264                loader_dependencies: self.loader_dependencies,
265                labeled_assets: self.labeled_assets,
266            }),
267            Err(value) => {
268                self.value = value;
269                Err(self)
270            }
271        }
272    }
273}
274
275/// A type erased container for an [`Asset`] value that is capable of inserting the [`Asset`] into a [`World`]'s [`Assets`] collection.
276pub trait AssetContainer: Downcast + Any + Send + Sync + 'static {
277    fn insert(self: Box<Self>, id: UntypedAssetId, world: &mut World);
278    fn asset_type_name(&self) -> &'static str;
279}
280
281impl_downcast!(AssetContainer);
282
283impl<A: Asset> AssetContainer for A {
284    fn insert(self: Box<Self>, id: UntypedAssetId, world: &mut World) {
285        world.resource_mut::<Assets<A>>().insert(id.typed(), *self);
286    }
287
288    fn asset_type_name(&self) -> &'static str {
289        core::any::type_name::<A>()
290    }
291}
292
293/// An error that occurs when attempting to call [`NestedLoader::load`] which
294/// is configured to work [immediately].
295///
296/// [`NestedLoader::load`]: crate::NestedLoader::load
297/// [immediately]: crate::Immediate
298#[derive(Error, Debug)]
299pub enum LoadDirectError {
300    #[error("Requested to load an asset path ({0:?}) with a subasset, but this is unsupported. See issue #18291")]
301    RequestedSubasset(AssetPath<'static>),
302    #[error("Failed to load dependency {dependency:?} {error}")]
303    LoadError {
304        dependency: AssetPath<'static>,
305        error: AssetLoadError,
306    },
307}
308
309/// An error that occurs while deserializing [`AssetMeta`].
310#[derive(Error, Debug, Clone, PartialEq, Eq)]
311pub enum DeserializeMetaError {
312    #[error("Failed to deserialize asset meta: {0:?}")]
313    DeserializeSettings(#[from] SpannedError),
314    #[error("Failed to deserialize minimal asset meta: {0:?}")]
315    DeserializeMinimal(SpannedError),
316}
317
318/// A context that provides access to assets in [`AssetLoader`]s, tracks dependencies, and collects asset load state.
319///
320/// Any asset state accessed by [`LoadContext`] will be tracked and stored for use in dependency events and asset preprocessing.
321pub struct LoadContext<'a> {
322    pub(crate) asset_server: &'a AssetServer,
323    pub(crate) should_load_dependencies: bool,
324    populate_hashes: bool,
325    asset_path: AssetPath<'static>,
326    pub(crate) dependencies: HashSet<UntypedAssetId>,
327    /// Direct dependencies used by this loader.
328    pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
329    pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
330}
331
332impl<'a> LoadContext<'a> {
333    /// Creates a new [`LoadContext`] instance.
334    pub(crate) fn new(
335        asset_server: &'a AssetServer,
336        asset_path: AssetPath<'static>,
337        should_load_dependencies: bool,
338        populate_hashes: bool,
339    ) -> Self {
340        Self {
341            asset_server,
342            asset_path,
343            populate_hashes,
344            should_load_dependencies,
345            dependencies: HashSet::default(),
346            loader_dependencies: HashMap::default(),
347            labeled_assets: HashMap::default(),
348        }
349    }
350
351    /// Begins a new labeled asset load. Use the returned [`LoadContext`] to load
352    /// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load.
353    /// When finished, make sure you call [`LoadContext::add_labeled_asset`] to add the results back to the parent
354    /// context.
355    /// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add
356    /// the labeled [`LoadContext`] back to the parent context.
357    /// [`LoadContext::begin_labeled_asset`] exists largely to enable parallel asset loading.
358    ///
359    /// See [`AssetPath`] for more on labeled assets.
360    ///
361    /// ```no_run
362    /// # use bevy_asset::{Asset, LoadContext};
363    /// # use bevy_reflect::TypePath;
364    /// # #[derive(Asset, TypePath, Default)]
365    /// # struct Image;
366    /// # let load_context: LoadContext = panic!();
367    /// let mut handles = Vec::new();
368    /// for i in 0..2 {
369    ///     let mut labeled = load_context.begin_labeled_asset();
370    ///     handles.push(std::thread::spawn(move || {
371    ///         (i.to_string(), labeled.finish(Image::default()))
372    ///     }));
373    /// }
374    ///
375    /// for handle in handles {
376    ///     let (label, loaded_asset) = handle.join().unwrap();
377    ///     load_context.add_loaded_labeled_asset(label, loaded_asset);
378    /// }
379    /// ```
380    pub fn begin_labeled_asset(&self) -> LoadContext {
381        LoadContext::new(
382            self.asset_server,
383            self.asset_path.clone(),
384            self.should_load_dependencies,
385            self.populate_hashes,
386        )
387    }
388
389    /// Creates a new [`LoadContext`] for the given `label`. The `load` function is responsible for loading an [`Asset`] of
390    /// type `A`. `load` will be called immediately and the result will be used to finalize the [`LoadContext`], resulting in a new
391    /// [`LoadedAsset`], which is registered under the `label` label.
392    ///
393    /// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the
394    /// result with [`LoadContext::add_labeled_asset`].
395    ///
396    /// See [`AssetPath`] for more on labeled assets.
397    pub fn labeled_asset_scope<A: Asset>(
398        &mut self,
399        label: String,
400        load: impl FnOnce(&mut LoadContext) -> A,
401    ) -> Handle<A> {
402        let mut context = self.begin_labeled_asset();
403        let asset = load(&mut context);
404        let loaded_asset = context.finish(asset);
405        self.add_loaded_labeled_asset(label, loaded_asset)
406    }
407
408    /// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label.
409    ///
410    /// # Warning
411    ///
412    /// This will not assign dependencies to the given `asset`. If adding an asset
413    /// with dependencies generated from calls such as [`LoadContext::load`], use
414    /// [`LoadContext::labeled_asset_scope`] or [`LoadContext::begin_labeled_asset`] to generate a
415    /// new [`LoadContext`] to track the dependencies for the labeled asset.
416    ///
417    /// See [`AssetPath`] for more on labeled assets.
418    pub fn add_labeled_asset<A: Asset>(&mut self, label: String, asset: A) -> Handle<A> {
419        self.labeled_asset_scope(label, |_| asset)
420    }
421
422    /// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context.
423    /// This can be used in combination with [`LoadContext::begin_labeled_asset`] to parallelize
424    /// sub asset loading.
425    ///
426    /// See [`AssetPath`] for more on labeled assets.
427    pub fn add_loaded_labeled_asset<A: Asset>(
428        &mut self,
429        label: impl Into<CowArc<'static, str>>,
430        loaded_asset: LoadedAsset<A>,
431    ) -> Handle<A> {
432        let label = label.into();
433        let loaded_asset: ErasedLoadedAsset = loaded_asset.into();
434        let labeled_path = self.asset_path.clone().with_label(label.clone());
435        let handle = self
436            .asset_server
437            .get_or_create_path_handle(labeled_path, None);
438        self.labeled_assets.insert(
439            label,
440            LabeledAsset {
441                asset: loaded_asset,
442                handle: handle.clone().untyped(),
443            },
444        );
445        handle
446    }
447
448    /// Returns `true` if an asset with the label `label` exists in this context.
449    ///
450    /// See [`AssetPath`] for more on labeled assets.
451    pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
452        let path = self.asset_path.clone().with_label(label.into());
453        !self.asset_server.get_handles_untyped(&path).is_empty()
454    }
455
456    /// "Finishes" this context by populating the final [`Asset`] value.
457    pub fn finish<A: Asset>(self, value: A) -> LoadedAsset<A> {
458        LoadedAsset {
459            value,
460            dependencies: self.dependencies,
461            loader_dependencies: self.loader_dependencies,
462            labeled_assets: self.labeled_assets,
463        }
464    }
465
466    /// Gets the source path for this load context.
467    pub fn path(&self) -> &Path {
468        self.asset_path.path()
469    }
470
471    /// Gets the source asset path for this load context.
472    pub fn asset_path(&self) -> &AssetPath<'static> {
473        &self.asset_path
474    }
475
476    /// Reads the asset at the given path and returns its bytes
477    pub async fn read_asset_bytes<'b, 'c>(
478        &'b mut self,
479        path: impl Into<AssetPath<'c>>,
480    ) -> Result<Vec<u8>, ReadAssetBytesError> {
481        let path = path.into();
482        let source = self.asset_server.get_source(path.source())?;
483        let asset_reader = match self.asset_server.mode() {
484            AssetServerMode::Unprocessed => source.reader(),
485            AssetServerMode::Processed => source.processed_reader()?,
486        };
487        let mut reader = asset_reader.read(path.path()).await?;
488        let hash = if self.populate_hashes {
489            // NOTE: ensure meta is read while the asset bytes reader is still active to ensure transactionality
490            // See `ProcessorGatedReader` for more info
491            let meta_bytes = asset_reader.read_meta_bytes(path.path()).await?;
492            let minimal: ProcessedInfoMinimal = ron::de::from_bytes(&meta_bytes)
493                .map_err(DeserializeMetaError::DeserializeMinimal)?;
494            let processed_info = minimal
495                .processed_info
496                .ok_or(ReadAssetBytesError::MissingAssetHash)?;
497            processed_info.full_hash
498        } else {
499            Default::default()
500        };
501        let mut bytes = Vec::new();
502        reader
503            .read_to_end(&mut bytes)
504            .await
505            .map_err(|source| ReadAssetBytesError::Io {
506                path: path.path().to_path_buf(),
507                source,
508            })?;
509        self.loader_dependencies.insert(path.clone_owned(), hash);
510        Ok(bytes)
511    }
512
513    /// Returns a handle to an asset of type `A` with the label `label`. This [`LoadContext`] must produce an asset of the
514    /// given type and the given label or the dependencies of this asset will never be considered "fully loaded". However you
515    /// can call this method before _or_ after adding the labeled asset.
516    pub fn get_label_handle<'b, A: Asset>(
517        &mut self,
518        label: impl Into<CowArc<'b, str>>,
519    ) -> Handle<A> {
520        let path = self.asset_path.clone().with_label(label);
521        let handle = self.asset_server.get_or_create_path_handle::<A>(path, None);
522        self.dependencies.insert(handle.id().untyped());
523        handle
524    }
525
526    pub(crate) async fn load_direct_internal(
527        &mut self,
528        path: AssetPath<'static>,
529        meta: &dyn AssetMetaDyn,
530        loader: &dyn ErasedAssetLoader,
531        reader: &mut dyn Reader,
532    ) -> Result<ErasedLoadedAsset, LoadDirectError> {
533        let loaded_asset = self
534            .asset_server
535            .load_with_meta_loader_and_reader(
536                &path,
537                meta,
538                loader,
539                reader,
540                false,
541                self.populate_hashes,
542            )
543            .await
544            .map_err(|error| LoadDirectError::LoadError {
545                dependency: path.clone(),
546                error,
547            })?;
548        let info = meta.processed_info().as_ref();
549        let hash = info.map(|i| i.full_hash).unwrap_or_default();
550        self.loader_dependencies.insert(path, hash);
551        Ok(loaded_asset)
552    }
553
554    /// Create a builder for loading nested assets in this context.
555    #[must_use]
556    pub fn loader(&mut self) -> NestedLoader<'a, '_, StaticTyped, Deferred> {
557        NestedLoader::new(self)
558    }
559
560    /// Retrieves a handle for the asset at the given path and adds that path as a dependency of the asset.
561    /// If the current context is a normal [`AssetServer::load`], an actual asset load will be kicked off immediately, which ensures the load happens
562    /// as soon as possible.
563    /// "Normal loads" kicked from within a normal Bevy App will generally configure the context to kick off loads immediately.
564    /// If the current context is configured to not load dependencies automatically (ex: [`AssetProcessor`](crate::processor::AssetProcessor)),
565    /// a load will not be kicked off automatically. It is then the calling context's responsibility to begin a load if necessary.
566    ///
567    /// If you need to override asset settings, asset type, or load directly, please see [`LoadContext::loader`].
568    pub fn load<'b, A: Asset>(&mut self, path: impl Into<AssetPath<'b>>) -> Handle<A> {
569        self.loader().load(path)
570    }
571}
572
573/// An error produced when calling [`LoadContext::read_asset_bytes`]
574#[derive(Error, Debug)]
575pub enum ReadAssetBytesError {
576    #[error(transparent)]
577    DeserializeMetaError(#[from] DeserializeMetaError),
578    #[error(transparent)]
579    AssetReaderError(#[from] AssetReaderError),
580    #[error(transparent)]
581    MissingAssetSourceError(#[from] MissingAssetSourceError),
582    #[error(transparent)]
583    MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError),
584    /// Encountered an I/O error while loading an asset.
585    #[error("Encountered an io error while loading asset at `{}`: {source}", path.display())]
586    Io {
587        path: PathBuf,
588        source: std::io::Error,
589    },
590    #[error("The LoadContext for this read_asset_bytes call requires hash metadata, but it was not provided. This is likely an internal implementation error.")]
591    MissingAssetHash,
592}