bevy_asset/
loader.rs

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