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}\"")]
346 EmptyPath(AssetPath<'static>),
347 #[error("Requested to load an asset path ({0:?}) with a subasset, but this is unsupported. See issue #18291")]
348 RequestedSubasset(AssetPath<'static>),
349 #[error("Failed to load dependency {dependency:?} {error}")]
350 LoadError {
351 dependency: AssetPath<'static>,
352 error: AssetLoadError,
353 },
354}
355
356#[derive(Error, Debug, Clone, PartialEq, Eq)]
358pub enum DeserializeMetaError {
359 #[error("Failed to deserialize asset meta: {0:?}")]
360 DeserializeSettings(#[from] SpannedError),
361 #[error("Failed to deserialize minimal asset meta: {0:?}")]
362 DeserializeMinimal(SpannedError),
363}
364
365pub struct LoadContext<'a> {
369 pub(crate) asset_server: &'a AssetServer,
370 pub(crate) should_load_dependencies: bool,
375 populate_hashes: bool,
376 asset_path: AssetPath<'static>,
377 pub(crate) dependencies: HashSet<ErasedAssetIndex>,
378 pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
380 pub(crate) labeled_assets: Vec<LabeledAsset>,
382 pub(crate) label_to_asset_index: HashMap<CowArc<'static, str>, usize>,
384 pub(crate) asset_id_to_asset_index: HashMap<UntypedAssetId, usize>,
389}
390
391impl<'a> LoadContext<'a> {
392 pub(crate) fn new(
394 asset_server: &'a AssetServer,
395 asset_path: AssetPath<'static>,
396 should_load_dependencies: bool,
397 populate_hashes: bool,
398 ) -> Self {
399 Self {
400 asset_server,
401 asset_path,
402 populate_hashes,
403 should_load_dependencies,
404 dependencies: HashSet::default(),
405 loader_dependencies: HashMap::default(),
406 labeled_assets: Default::default(),
407 label_to_asset_index: Default::default(),
408 asset_id_to_asset_index: Default::default(),
409 }
410 }
411
412 pub fn begin_labeled_asset(&self) -> LoadContext<'_> {
442 LoadContext::new(
443 self.asset_server,
444 self.asset_path.clone(),
445 self.should_load_dependencies,
446 self.populate_hashes,
447 )
448 }
449
450 pub fn labeled_asset_scope<A: Asset, E>(
459 &mut self,
460 label: impl Into<CowArc<'static, str>>,
461 load: impl FnOnce(&mut LoadContext) -> Result<A, E>,
462 ) -> Result<Handle<A>, E> {
463 let mut context = self.begin_labeled_asset();
464 let asset = load(&mut context)?;
465 let loaded_asset = context.finish(asset);
466 Ok(self.add_loaded_labeled_asset(label, loaded_asset))
467 }
468
469 pub fn add_labeled_asset<A: Asset>(
480 &mut self,
481 label: impl Into<CowArc<'static, str>>,
482 asset: A,
483 ) -> Handle<A> {
484 let Ok(handle) = self.labeled_asset_scope(label, |_| Ok::<_, Infallible>(asset));
485 handle
486 }
487
488 pub fn add_loaded_labeled_asset<A: Asset>(
494 &mut self,
495 label: impl Into<CowArc<'static, str>>,
496 loaded_asset: LoadedAsset<A>,
497 ) -> Handle<A> {
498 let label = label.into();
499 let loaded_asset: ErasedLoadedAsset = loaded_asset.into();
500 let labeled_path = self.asset_path.clone().with_label(label.clone());
501 let handle = self
502 .asset_server
503 .get_or_create_path_handle(labeled_path, None);
504 let asset = LabeledAsset {
505 asset: loaded_asset,
506 handle: handle.clone().untyped(),
507 };
508 match self.label_to_asset_index.entry(label) {
509 Entry::Occupied(entry) => {
510 let index = *entry.get();
513 self.labeled_assets[index] = asset;
517 }
518 Entry::Vacant(entry) => {
519 entry.insert(self.labeled_assets.len());
520 self.asset_id_to_asset_index
521 .insert(handle.id().untyped(), self.labeled_assets.len());
522 self.labeled_assets.push(asset);
523 }
524 }
525 handle
526 }
527
528 pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
532 let path = self.asset_path.clone().with_label(label.into());
533 !self.asset_server.get_handles_untyped(&path).is_empty()
534 }
535
536 pub fn finish<A: Asset>(mut self, value: A) -> LoadedAsset<A> {
538 value.visit_dependencies(&mut |asset_id| {
545 let (type_id, index) = match asset_id {
546 UntypedAssetId::Index { type_id, index } => (type_id, index),
547 UntypedAssetId::Uuid { .. } => return,
549 };
550 self.dependencies
551 .insert(ErasedAssetIndex { index, type_id });
552 });
553 LoadedAsset {
554 value,
555 dependencies: self.dependencies,
556 loader_dependencies: self.loader_dependencies,
557 labeled_assets: self.labeled_assets,
558 label_to_asset_index: self.label_to_asset_index,
559 asset_id_to_asset_index: self.asset_id_to_asset_index,
560 }
561 }
562
563 pub fn path(&self) -> &AssetPath<'static> {
565 &self.asset_path
566 }
567
568 pub async fn read_asset_bytes<'b, 'c>(
570 &'b mut self,
571 path: impl Into<AssetPath<'c>>,
572 ) -> Result<Vec<u8>, ReadAssetBytesError> {
573 let path = path.into();
574 if path.path() == Path::new("") {
575 error!("Attempted to load an asset with an empty path \"{path}\"!");
576 return Err(ReadAssetBytesError::EmptyPath(path.into_owned()));
577 }
578
579 let source = self.asset_server.get_source(path.source())?;
580 let asset_reader = match self.asset_server.mode() {
581 AssetServerMode::Unprocessed => source.reader(),
582 AssetServerMode::Processed => source.processed_reader()?,
583 };
584 let mut reader = asset_reader.read(path.path()).await?;
585 let hash = if self.populate_hashes {
586 let meta_bytes = asset_reader.read_meta_bytes(path.path()).await?;
589 let minimal: ProcessedInfoMinimal = ron::de::from_bytes(&meta_bytes)
590 .map_err(DeserializeMetaError::DeserializeMinimal)?;
591 let processed_info = minimal
592 .processed_info
593 .ok_or(ReadAssetBytesError::MissingAssetHash)?;
594 processed_info.full_hash
595 } else {
596 Default::default()
597 };
598 let mut bytes = Vec::new();
599 reader
600 .read_to_end(&mut bytes)
601 .await
602 .map_err(|source| ReadAssetBytesError::Io {
603 path: path.path().to_path_buf(),
604 source,
605 })?;
606 self.loader_dependencies.insert(path.clone_owned(), hash);
607 Ok(bytes)
608 }
609
610 pub fn get_label_handle<'b, A: Asset>(
614 &mut self,
615 label: impl Into<CowArc<'b, str>>,
616 ) -> Handle<A> {
617 let path = self.asset_path.clone().with_label(label);
618 let handle = self.asset_server.get_or_create_path_handle(path, None);
619 let index = (&handle).try_into().unwrap();
621 self.dependencies.insert(index);
622 handle
623 }
624
625 pub fn get_labeled(&self, label: impl AsRef<str>) -> Option<&ErasedLoadedAsset> {
627 let index = self.label_to_asset_index.get(label.as_ref())?;
628 let labeled = &self.labeled_assets[*index];
629 Some(&labeled.asset)
630 }
631
632 pub fn get_labeled_by_id(&self, id: impl Into<UntypedAssetId>) -> Option<&ErasedLoadedAsset> {
637 let index = self.asset_id_to_asset_index.get(&id.into())?;
638 let labeled = &self.labeled_assets[*index];
639 Some(&labeled.asset)
640 }
641
642 pub(crate) async fn load_direct_internal(
643 &mut self,
644 path: AssetPath<'static>,
645 settings: &dyn Settings,
646 loader: &dyn ErasedAssetLoader,
647 reader: &mut dyn Reader,
648 processed_info: Option<&ProcessedInfo>,
649 ) -> Result<ErasedLoadedAsset, LoadDirectError> {
650 let loaded_asset = self
651 .asset_server
652 .load_with_settings_loader_and_reader(
653 &path,
654 settings,
655 loader,
656 reader,
657 self.should_load_dependencies,
658 self.populate_hashes,
659 )
660 .await
661 .map_err(|error| LoadDirectError::LoadError {
662 dependency: path.clone(),
663 error,
664 })?;
665 let hash = processed_info.map(|i| i.full_hash).unwrap_or_default();
666 self.loader_dependencies.insert(path, hash);
667 Ok(loaded_asset)
668 }
669
670 #[must_use]
672 pub fn load_builder(&mut self) -> NestedLoadBuilder<'a, '_> {
673 NestedLoadBuilder::new(self)
674 }
675
676 pub fn load<'b, A: Asset>(&mut self, path: impl Into<AssetPath<'b>>) -> Handle<A> {
685 self.load_builder().load(path)
686 }
687}
688
689#[derive(Error, Debug)]
691pub enum ReadAssetBytesError {
692 #[error("Attempted to load an asset with an empty path \"{0}\"")]
693 EmptyPath(AssetPath<'static>),
694 #[error(transparent)]
695 DeserializeMetaError(#[from] DeserializeMetaError),
696 #[error(transparent)]
697 AssetReaderError(#[from] AssetReaderError),
698 #[error(transparent)]
699 MissingAssetSourceError(#[from] MissingAssetSourceError),
700 #[error(transparent)]
701 MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError),
702 #[error("Encountered an io error while loading asset at `{}`: {source}", path.display())]
704 Io {
705 path: PathBuf,
706 source: std::io::Error,
707 },
708 #[error("The LoadContext for this read_asset_bytes call requires hash metadata, but it was not provided. This is likely an internal implementation error.")]
709 MissingAssetHash,
710}