1use crate::{
2 io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader},
3 loader_builders::NestedLoadBuilder,
4 meta::{AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfo, ProcessedInfoMinimal, Settings},
5 path::AssetPath,
6 Asset, AssetIndex, AssetLoadError, AssetServer, AssetServerMode, Assets, ErasedAssetIndex,
7 Handle, UntypedAssetId, UntypedHandle,
8};
9use alloc::{boxed::Box, string::ToString, vec::Vec};
10use atomicow::CowArc;
11use bevy_ecs::{error::BevyError, world::World};
12use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet};
13use bevy_reflect::TypePath;
14use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
15use core::{
16 any::{Any, TypeId},
17 convert::Infallible,
18};
19use downcast_rs::{impl_downcast, Downcast};
20use ron::error::SpannedError;
21use serde::{Deserialize, Serialize};
22use std::path::{Path, PathBuf};
23use thiserror::Error;
24use tracing::error;
25
26pub trait AssetLoader: TypePath + Send + Sync + 'static {
33 type Asset: Asset;
35 type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
37 type Error: Into<BevyError>;
39 fn load(
41 &self,
42 reader: &mut dyn Reader,
43 settings: &Self::Settings,
44 load_context: &mut LoadContext,
45 ) -> impl ConditionalSendFuture<Output = Result<Self::Asset, Self::Error>>;
46
47 fn extensions(&self) -> &[&str] {
50 &[]
51 }
52}
53
54pub trait ErasedAssetLoader: Send + Sync + 'static {
56 fn load<'a>(
58 &'a self,
59 reader: &'a mut dyn Reader,
60 settings: &'a dyn Settings,
61 load_context: LoadContext<'a>,
62 ) -> BoxedFuture<'a, Result<ErasedLoadedAsset, BevyError>>;
63
64 fn extensions(&self) -> &[&str];
66 fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError>;
68 fn default_meta(&self) -> Box<dyn AssetMetaDyn>;
70 fn type_path(&self) -> &'static str;
72 fn type_id(&self) -> TypeId;
74 fn asset_type_name(&self) -> &'static str;
76 fn asset_type_id(&self) -> TypeId;
78}
79
80impl<L> ErasedAssetLoader for L
81where
82 L: AssetLoader + Send + Sync,
83{
84 fn load<'a>(
86 &'a self,
87 reader: &'a mut dyn Reader,
88 settings: &'a dyn Settings,
89 mut load_context: LoadContext<'a>,
90 ) -> BoxedFuture<'a, Result<ErasedLoadedAsset, BevyError>> {
91 Box::pin(async move {
92 let settings = settings
93 .downcast_ref::<L::Settings>()
94 .expect("AssetLoader settings should match the loader type");
95 let asset = <L as AssetLoader>::load(self, reader, settings, &mut load_context)
96 .await
97 .map_err(Into::into)?;
98 Ok(load_context.finish(asset).into())
99 })
100 }
101
102 fn extensions(&self) -> &[&str] {
103 <L as AssetLoader>::extensions(self)
104 }
105
106 fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError> {
107 let meta = AssetMeta::<L, ()>::deserialize(meta)?;
108 Ok(Box::new(meta))
109 }
110
111 fn default_meta(&self) -> Box<dyn AssetMetaDyn> {
112 Box::new(AssetMeta::<L, ()>::new(crate::meta::AssetAction::Load {
113 loader: self.type_path().to_string(),
114 settings: L::Settings::default(),
115 }))
116 }
117
118 fn type_path(&self) -> &'static str {
119 L::type_path()
120 }
121
122 fn type_id(&self) -> TypeId {
123 TypeId::of::<L>()
124 }
125
126 fn asset_type_name(&self) -> &'static str {
127 core::any::type_name::<L::Asset>()
128 }
129
130 fn asset_type_id(&self) -> TypeId {
131 TypeId::of::<L::Asset>()
132 }
133}
134
135pub(crate) struct LabeledAsset {
136 pub(crate) asset: ErasedLoadedAsset,
137 pub(crate) handle: UntypedHandle,
138}
139
140pub struct LoadedAsset<A: Asset> {
145 pub(crate) value: A,
146 pub(crate) dependencies: HashSet<ErasedAssetIndex>,
147 pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
148 pub(crate) labeled_assets: Vec<LabeledAsset>,
150 pub(crate) label_to_asset_index: HashMap<CowArc<'static, str>, usize>,
152 pub(crate) asset_id_to_asset_index: HashMap<UntypedAssetId, usize>,
157}
158
159impl<A: Asset> LoadedAsset<A> {
160 pub fn new_with_dependencies(value: A) -> Self {
162 let mut dependencies = <HashSet<_>>::default();
163 value.visit_dependencies(&mut |id| {
164 let Ok(asset_index) = id.try_into() else {
165 return;
166 };
167 dependencies.insert(asset_index);
168 });
169 LoadedAsset {
170 value,
171 dependencies,
172 loader_dependencies: HashMap::default(),
173 labeled_assets: Default::default(),
174 label_to_asset_index: Default::default(),
175 asset_id_to_asset_index: Default::default(),
176 }
177 }
178
179 pub fn take(self) -> A {
181 self.value
182 }
183
184 pub fn get(&self) -> &A {
186 &self.value
187 }
188
189 pub fn get_labeled(&self, label: impl AsRef<str>) -> Option<&ErasedLoadedAsset> {
191 self.label_to_asset_index
192 .get(label.as_ref())
193 .map(|index| self.labeled_assets.get(*index).unwrap())
194 .map(|a| &a.asset)
195 }
196
197 pub fn get_labeled_by_id(&self, id: impl Into<UntypedAssetId>) -> Option<&ErasedLoadedAsset> {
202 let index = self.asset_id_to_asset_index.get(&id.into())?;
203 let labeled = &self.labeled_assets[*index];
204 Some(&labeled.asset)
205 }
206
207 pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
209 self.label_to_asset_index.keys().map(|s| &**s)
210 }
211}
212
213impl<A: Asset> From<A> for LoadedAsset<A> {
214 fn from(asset: A) -> Self {
215 LoadedAsset::new_with_dependencies(asset)
216 }
217}
218
219pub struct ErasedLoadedAsset {
221 pub(crate) value: Box<dyn AssetContainer>,
222 pub(crate) dependencies: HashSet<ErasedAssetIndex>,
223 pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
224 pub(crate) labeled_assets: Vec<LabeledAsset>,
226 pub(crate) label_to_asset_index: HashMap<CowArc<'static, str>, usize>,
228 pub(crate) asset_id_to_asset_index: HashMap<UntypedAssetId, usize>,
233}
234
235impl<A: Asset> From<LoadedAsset<A>> for ErasedLoadedAsset {
236 fn from(asset: LoadedAsset<A>) -> Self {
237 ErasedLoadedAsset {
238 value: Box::new(asset.value),
239 dependencies: asset.dependencies,
240 loader_dependencies: asset.loader_dependencies,
241 labeled_assets: asset.labeled_assets,
242 label_to_asset_index: asset.label_to_asset_index,
243 asset_id_to_asset_index: asset.asset_id_to_asset_index,
244 }
245 }
246}
247
248impl ErasedLoadedAsset {
249 pub fn take<A: Asset>(self) -> Option<A> {
252 self.value.downcast::<A>().map(|a| *a).ok()
253 }
254
255 pub fn get<A: Asset>(&self) -> Option<&A> {
257 self.value.downcast_ref::<A>()
258 }
259
260 pub fn asset_type_id(&self) -> TypeId {
262 (*self.value).type_id()
263 }
264
265 pub fn asset_type_name(&self) -> &'static str {
267 self.value.asset_type_name()
268 }
269
270 pub fn get_labeled(&self, label: impl AsRef<str>) -> Option<&ErasedLoadedAsset> {
272 self.label_to_asset_index
273 .get(label.as_ref())
274 .map(|index| self.labeled_assets.get(*index).unwrap())
275 .map(|a| &a.asset)
276 }
277
278 pub fn get_labeled_by_id(&self, id: impl Into<UntypedAssetId>) -> Option<&ErasedLoadedAsset> {
283 let index = self.asset_id_to_asset_index.get(&id.into())?;
284 let labeled = &self.labeled_assets[*index];
285 Some(&labeled.asset)
286 }
287
288 pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
290 self.label_to_asset_index.keys().map(|s| &**s)
291 }
292
293 #[cfg_attr(
296 not(target_arch = "wasm32"),
297 expect(
298 clippy::result_large_err,
299 reason = "Returning the passed in ErasedLoadedAsset"
300 )
301 )]
302 pub fn downcast<A: Asset>(mut self) -> Result<LoadedAsset<A>, ErasedLoadedAsset> {
303 match self.value.downcast::<A>() {
304 Ok(value) => Ok(LoadedAsset {
305 value: *value,
306 dependencies: self.dependencies,
307 loader_dependencies: self.loader_dependencies,
308 labeled_assets: self.labeled_assets,
309 label_to_asset_index: self.label_to_asset_index,
310 asset_id_to_asset_index: self.asset_id_to_asset_index,
311 }),
312 Err(value) => {
313 self.value = value;
314 Err(self)
315 }
316 }
317 }
318}
319
320pub(crate) trait AssetContainer: Downcast + Any + Send + Sync + 'static {
322 fn insert(self: Box<Self>, id: AssetIndex, world: &mut World);
323 fn asset_type_name(&self) -> &'static str;
324}
325
326impl_downcast!(AssetContainer);
327
328impl<A: Asset> AssetContainer for A {
329 fn insert(self: Box<Self>, index: AssetIndex, world: &mut World) {
330 world
332 .resource_mut::<Assets<A>>()
333 .insert(index, *self)
334 .expect("the AssetIndex is still valid");
335 }
336
337 fn asset_type_name(&self) -> &'static str {
338 core::any::type_name::<A>()
339 }
340}
341
342#[derive(Error, Debug)]
344pub enum LoadDirectError {
345 #[error("Attempted to load an asset with an empty path \"{0}\"")]
347 EmptyPath(AssetPath<'static>),
348 #[error("Requested to load an asset path ({0:?}) with a subasset, but this is unsupported. See issue #18291")]
352 RequestedSubasset(AssetPath<'static>),
353 #[error("Failed to load dependency {dependency:?} {error}")]
355 LoadError {
356 dependency: AssetPath<'static>,
358 error: Box<AssetLoadError>,
360 },
361}
362
363#[derive(Error, Debug, Clone, PartialEq, Eq)]
365pub enum DeserializeMetaError {
366 #[error("Failed to deserialize asset meta: {0:?}")]
368 DeserializeSettings(#[from] SpannedError),
369 #[error("Failed to deserialize minimal asset meta: {0:?}")]
371 DeserializeMinimal(SpannedError),
372}
373
374pub struct LoadContext<'a> {
378 pub(crate) asset_server: &'a AssetServer,
379 pub(crate) should_load_dependencies: bool,
384 populate_hashes: bool,
385 asset_path: AssetPath<'static>,
386 pub(crate) dependencies: HashSet<ErasedAssetIndex>,
387 pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
389 pub(crate) labeled_assets: Vec<LabeledAsset>,
391 pub(crate) label_to_asset_index: HashMap<CowArc<'static, str>, usize>,
393 pub(crate) asset_id_to_asset_index: HashMap<UntypedAssetId, usize>,
398}
399
400impl<'a> LoadContext<'a> {
401 pub(crate) fn new(
403 asset_server: &'a AssetServer,
404 asset_path: AssetPath<'static>,
405 should_load_dependencies: bool,
406 populate_hashes: bool,
407 ) -> Self {
408 Self {
409 asset_server,
410 asset_path,
411 populate_hashes,
412 should_load_dependencies,
413 dependencies: HashSet::default(),
414 loader_dependencies: HashMap::default(),
415 labeled_assets: Default::default(),
416 label_to_asset_index: Default::default(),
417 asset_id_to_asset_index: Default::default(),
418 }
419 }
420
421 pub fn begin_labeled_asset(&self) -> LoadContext<'_> {
451 LoadContext::new(
452 self.asset_server,
453 self.asset_path.clone(),
454 self.should_load_dependencies,
455 self.populate_hashes,
456 )
457 }
458
459 pub fn labeled_asset_scope<A: Asset, E>(
468 &mut self,
469 label: impl Into<CowArc<'static, str>>,
470 load: impl FnOnce(&mut LoadContext) -> Result<A, E>,
471 ) -> Result<Handle<A>, E> {
472 let mut context = self.begin_labeled_asset();
473 let asset = load(&mut context)?;
474 let loaded_asset = context.finish(asset);
475 Ok(self.add_loaded_labeled_asset(label, loaded_asset))
476 }
477
478 pub fn add_labeled_asset<A: Asset>(
489 &mut self,
490 label: impl Into<CowArc<'static, str>>,
491 asset: A,
492 ) -> Handle<A> {
493 let Ok(handle) = self.labeled_asset_scope(label, |_| Ok::<_, Infallible>(asset));
494 handle
495 }
496
497 pub fn add_loaded_labeled_asset<A: Asset>(
503 &mut self,
504 label: impl Into<CowArc<'static, str>>,
505 loaded_asset: LoadedAsset<A>,
506 ) -> Handle<A> {
507 let label = label.into();
508 let loaded_asset: ErasedLoadedAsset = loaded_asset.into();
509 let labeled_path = self.asset_path.clone().with_label(label.clone());
510 let handle = self
511 .asset_server
512 .get_or_create_path_handle(labeled_path, None);
513 let asset = LabeledAsset {
514 asset: loaded_asset,
515 handle: handle.clone().untyped(),
516 };
517 match self.label_to_asset_index.entry(label) {
518 Entry::Occupied(entry) => {
519 let index = *entry.get();
522 self.labeled_assets[index] = asset;
526 }
527 Entry::Vacant(entry) => {
528 entry.insert(self.labeled_assets.len());
529 self.asset_id_to_asset_index
530 .insert(handle.id().untyped(), self.labeled_assets.len());
531 self.labeled_assets.push(asset);
532 }
533 }
534 handle
535 }
536
537 pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
541 let path = self.asset_path.clone().with_label(label.into());
542 !self.asset_server.get_handles_untyped(&path).is_empty()
543 }
544
545 pub fn finish<A: Asset>(mut self, value: A) -> LoadedAsset<A> {
547 value.visit_dependencies(&mut |asset_id| {
554 let (type_id, index) = match asset_id {
555 UntypedAssetId::Index { type_id, index } => (type_id, index),
556 UntypedAssetId::Uuid { .. } => return,
558 };
559 self.dependencies
560 .insert(ErasedAssetIndex { index, type_id });
561 });
562 LoadedAsset {
563 value,
564 dependencies: self.dependencies,
565 loader_dependencies: self.loader_dependencies,
566 labeled_assets: self.labeled_assets,
567 label_to_asset_index: self.label_to_asset_index,
568 asset_id_to_asset_index: self.asset_id_to_asset_index,
569 }
570 }
571
572 pub fn path(&self) -> &AssetPath<'static> {
574 &self.asset_path
575 }
576
577 pub async fn read_asset_bytes<'b, 'c>(
579 &'b mut self,
580 path: impl Into<AssetPath<'c>>,
581 ) -> Result<Vec<u8>, ReadAssetBytesError> {
582 let path = path.into();
583 if path.path() == Path::new("") {
584 error!("Attempted to load an asset with an empty path \"{path}\"!");
585 return Err(ReadAssetBytesError::EmptyPath(path.into_owned()));
586 }
587
588 let source = self.asset_server.get_source(path.source())?;
589 let asset_reader = match self.asset_server.mode() {
590 AssetServerMode::Unprocessed => source.reader(),
591 AssetServerMode::Processed => source.processed_reader()?,
592 };
593 let mut reader = asset_reader.read(path.path()).await?;
594 let hash = if self.populate_hashes {
595 let meta_bytes = asset_reader.read_meta_bytes(path.path()).await?;
598 let minimal: ProcessedInfoMinimal = ron::de::from_bytes(&meta_bytes)
599 .map_err(DeserializeMetaError::DeserializeMinimal)?;
600 let processed_info = minimal
601 .processed_info
602 .ok_or(ReadAssetBytesError::MissingAssetHash)?;
603 processed_info.full_hash
604 } else {
605 Default::default()
606 };
607 let mut bytes = Vec::new();
608 reader
609 .read_to_end(&mut bytes)
610 .await
611 .map_err(|source| ReadAssetBytesError::Io {
612 path: path.path().to_path_buf(),
613 source,
614 })?;
615 self.loader_dependencies.insert(path.clone_owned(), hash);
616 Ok(bytes)
617 }
618
619 pub fn get_label_handle<'b, A: Asset>(
623 &mut self,
624 label: impl Into<CowArc<'b, str>>,
625 ) -> Handle<A> {
626 let path = self.asset_path.clone().with_label(label);
627 let handle = self.asset_server.get_or_create_path_handle(path, None);
628 let index = (&handle).try_into().unwrap();
630 self.dependencies.insert(index);
631 handle
632 }
633
634 pub fn get_labeled(&self, label: impl AsRef<str>) -> Option<&ErasedLoadedAsset> {
636 let index = self.label_to_asset_index.get(label.as_ref())?;
637 let labeled = &self.labeled_assets[*index];
638 Some(&labeled.asset)
639 }
640
641 pub fn get_labeled_by_id(&self, id: impl Into<UntypedAssetId>) -> Option<&ErasedLoadedAsset> {
646 let index = self.asset_id_to_asset_index.get(&id.into())?;
647 let labeled = &self.labeled_assets[*index];
648 Some(&labeled.asset)
649 }
650
651 pub(crate) async fn load_direct_internal(
652 &mut self,
653 path: AssetPath<'static>,
654 settings: &dyn Settings,
655 loader: &dyn ErasedAssetLoader,
656 reader: &mut dyn Reader,
657 processed_info: Option<&ProcessedInfo>,
658 ) -> Result<ErasedLoadedAsset, LoadDirectError> {
659 let loaded_asset = self
660 .asset_server
661 .load_with_settings_loader_and_reader(
662 &path,
663 settings,
664 loader,
665 reader,
666 self.should_load_dependencies,
667 self.populate_hashes,
668 )
669 .await
670 .map_err(|error| LoadDirectError::LoadError {
671 dependency: path.clone(),
672 error: Box::new(error),
673 })?;
674 let hash = processed_info.map(|i| i.full_hash).unwrap_or_default();
675 self.loader_dependencies.insert(path, hash);
676 Ok(loaded_asset)
677 }
678
679 #[must_use]
681 pub fn load_builder(&mut self) -> NestedLoadBuilder<'a, '_> {
682 NestedLoadBuilder::new(self)
683 }
684
685 pub fn load<'b, A: Asset>(&mut self, path: impl Into<AssetPath<'b>>) -> Handle<A> {
694 self.load_builder().load(path)
695 }
696}
697
698#[derive(Error, Debug)]
700pub enum ReadAssetBytesError {
701 #[error("Attempted to load an asset with an empty path \"{0}\"")]
702 EmptyPath(AssetPath<'static>),
703 #[error(transparent)]
704 DeserializeMetaError(#[from] DeserializeMetaError),
705 #[error(transparent)]
706 AssetReaderError(#[from] AssetReaderError),
707 #[error(transparent)]
708 MissingAssetSourceError(#[from] MissingAssetSourceError),
709 #[error(transparent)]
710 MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError),
711 #[error("Encountered an io error while loading asset at `{}`: {source}", path.display())]
713 Io {
714 path: PathBuf,
715 source: std::io::Error,
716 },
717 #[error("The LoadContext for this read_asset_bytes call requires hash metadata, but it was not provided. This is likely an internal implementation error.")]
718 MissingAssetHash,
719}