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