1#![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")]
144#![cfg_attr(docsrs, feature(doc_auto_cfg))]
145#![doc(
146 html_logo_url = "https://bevyengine.org/assets/icon.png",
147 html_favicon_url = "https://bevyengine.org/assets/icon.png"
148)]
149
150extern crate alloc;
151
152pub mod io;
153pub mod meta;
154pub mod processor;
155pub mod saver;
156pub mod transformer;
157
158pub mod prelude {
162 #[doc(hidden)]
163 pub use crate::{
164 Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets,
165 DirectAssetAccessExt, Handle, UntypedHandle,
166 };
167}
168
169mod assets;
170mod direct_access_ext;
171mod event;
172mod folder;
173mod handle;
174mod id;
175mod loader;
176mod loader_builders;
177mod path;
178mod reflect;
179mod render_asset;
180mod server;
181
182pub use assets::*;
183pub use bevy_asset_macros::Asset;
184pub use direct_access_ext::DirectAssetAccessExt;
185pub use event::*;
186pub use folder::*;
187pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
188pub use handle::*;
189pub use id::*;
190pub use loader::*;
191pub use loader_builders::{
192 Deferred, DynamicTyped, Immediate, NestedLoader, StaticTyped, UnknownTyped,
193};
194pub use path::*;
195pub use reflect::*;
196pub use render_asset::*;
197pub use server::*;
198
199pub use ron;
201
202use crate::{
203 io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId},
204 processor::{AssetProcessor, Process},
205};
206use alloc::sync::Arc;
207use bevy_app::{App, Last, Plugin, PreUpdate};
208use bevy_ecs::{
209 reflect::AppTypeRegistry,
210 schedule::{IntoSystemConfigs, IntoSystemSetConfigs, SystemSet},
211 world::FromWorld,
212};
213use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath};
214use bevy_utils::{tracing::error, HashSet};
215use core::any::TypeId;
216
217#[cfg(all(feature = "file_watcher", not(feature = "multi_threaded")))]
218compile_error!(
219 "The \"file_watcher\" feature for hot reloading requires the \
220 \"multi_threaded\" feature to be functional.\n\
221 Consider either disabling the \"file_watcher\" feature or enabling \"multi_threaded\""
222);
223
224pub struct AssetPlugin {
232 pub file_path: String,
234 pub processed_file_path: String,
236 pub watch_for_changes_override: Option<bool>,
243 pub mode: AssetMode,
245 pub meta_check: AssetMetaCheck,
247}
248
249#[derive(Debug)]
256pub enum AssetMode {
257 Unprocessed,
262 Processed,
277}
278
279#[derive(Debug, Default, Clone)]
282pub enum AssetMetaCheck {
283 #[default]
285 Always,
286 Paths(HashSet<AssetPath<'static>>),
288 Never,
290}
291
292impl Default for AssetPlugin {
293 fn default() -> Self {
294 Self {
295 mode: AssetMode::Unprocessed,
296 file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(),
297 processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
298 watch_for_changes_override: None,
299 meta_check: AssetMetaCheck::default(),
300 }
301 }
302}
303
304impl AssetPlugin {
305 const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets";
306 const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default";
309}
310
311impl Plugin for AssetPlugin {
312 fn build(&self, app: &mut App) {
313 let embedded = EmbeddedAssetRegistry::default();
314 {
315 let mut sources = app
316 .world_mut()
317 .get_resource_or_init::<AssetSourceBuilders>();
318 sources.init_default_source(
319 &self.file_path,
320 (!matches!(self.mode, AssetMode::Unprocessed))
321 .then_some(self.processed_file_path.as_str()),
322 );
323 embedded.register_source(&mut sources);
324 }
325 {
326 let mut watch = cfg!(feature = "watch");
327 if let Some(watch_override) = self.watch_for_changes_override {
328 watch = watch_override;
329 }
330 match self.mode {
331 AssetMode::Unprocessed => {
332 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
333 let sources = builders.build_sources(watch, false);
334
335 app.insert_resource(AssetServer::new_with_meta_check(
336 sources,
337 AssetServerMode::Unprocessed,
338 self.meta_check.clone(),
339 watch,
340 ));
341 }
342 AssetMode::Processed => {
343 #[cfg(feature = "asset_processor")]
344 {
345 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
346 let processor = AssetProcessor::new(&mut builders);
347 let mut sources = builders.build_sources(false, watch);
348 sources.gate_on_processor(processor.data.clone());
349 app.insert_resource(AssetServer::new_with_loaders(
351 sources,
352 processor.server().data.loaders.clone(),
353 AssetServerMode::Processed,
354 AssetMetaCheck::Always,
355 watch,
356 ))
357 .insert_resource(processor)
358 .add_systems(bevy_app::Startup, AssetProcessor::start);
359 }
360 #[cfg(not(feature = "asset_processor"))]
361 {
362 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
363 let sources = builders.build_sources(false, watch);
364 app.insert_resource(AssetServer::new_with_meta_check(
365 sources,
366 AssetServerMode::Processed,
367 AssetMetaCheck::Always,
368 watch,
369 ));
370 }
371 }
372 }
373 }
374 app.insert_resource(embedded)
375 .init_asset::<LoadedFolder>()
376 .init_asset::<LoadedUntypedAsset>()
377 .init_asset::<()>()
378 .add_event::<UntypedAssetLoadFailedEvent>()
379 .configure_sets(PreUpdate, TrackAssets.after(handle_internal_asset_events))
380 .add_systems(PreUpdate, handle_internal_asset_events.ambiguous_with_all())
385 .register_type::<AssetPath>();
386 }
387}
388
389#[diagnostic::on_unimplemented(
397 message = "`{Self}` is not an `Asset`",
398 label = "invalid `Asset`",
399 note = "consider annotating `{Self}` with `#[derive(Asset)]`"
400)]
401pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {}
402
403pub trait VisitAssetDependencies {
408 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId));
409}
410
411impl<A: Asset> VisitAssetDependencies for Handle<A> {
412 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
413 visit(self.id().untyped());
414 }
415}
416
417impl<A: Asset> VisitAssetDependencies for Option<Handle<A>> {
418 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
419 if let Some(handle) = self {
420 visit(handle.id().untyped());
421 }
422 }
423}
424
425impl VisitAssetDependencies for UntypedHandle {
426 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
427 visit(self.id());
428 }
429}
430
431impl VisitAssetDependencies for Option<UntypedHandle> {
432 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
433 if let Some(handle) = self {
434 visit(handle.id());
435 }
436 }
437}
438
439impl<A: Asset> VisitAssetDependencies for Vec<Handle<A>> {
440 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
441 for dependency in self {
442 visit(dependency.id().untyped());
443 }
444 }
445}
446
447impl VisitAssetDependencies for Vec<UntypedHandle> {
448 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
449 for dependency in self {
450 visit(dependency.id());
451 }
452 }
453}
454
455pub trait AssetApp {
457 fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self;
459 fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self;
461 fn register_asset_source(
466 &mut self,
467 id: impl Into<AssetSourceId<'static>>,
468 source: AssetSourceBuilder,
469 ) -> &mut Self;
470 fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self;
472 fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self;
474 fn init_asset<A: Asset>(&mut self) -> &mut Self;
482 fn register_asset_reflect<A>(&mut self) -> &mut Self
487 where
488 A: Asset + Reflect + FromReflect + GetTypeRegistration;
489 fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self;
492}
493
494impl AssetApp for App {
495 fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
496 self.world()
497 .resource::<AssetServer>()
498 .register_loader(loader);
499 self
500 }
501
502 fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self {
503 if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
504 asset_processor.register_processor(processor);
505 }
506 self
507 }
508
509 fn register_asset_source(
510 &mut self,
511 id: impl Into<AssetSourceId<'static>>,
512 source: AssetSourceBuilder,
513 ) -> &mut Self {
514 let id = AssetSourceId::from_static(id);
515 if self.world().get_resource::<AssetServer>().is_some() {
516 error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id);
517 }
518
519 {
520 let mut sources = self
521 .world_mut()
522 .get_resource_or_init::<AssetSourceBuilders>();
523 sources.insert(id, source);
524 }
525
526 self
527 }
528
529 fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self {
530 if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
531 asset_processor.set_default_processor::<P>(extension);
532 }
533 self
534 }
535
536 fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self {
537 let loader = L::from_world(self.world_mut());
538 self.register_asset_loader(loader)
539 }
540
541 fn init_asset<A: Asset>(&mut self) -> &mut Self {
542 let assets = Assets::<A>::default();
543 self.world()
544 .resource::<AssetServer>()
545 .register_asset(&assets);
546 if self.world().contains_resource::<AssetProcessor>() {
547 let processor = self.world().resource::<AssetProcessor>();
548 processor
552 .server()
553 .register_handle_provider(AssetHandleProvider::new(
554 TypeId::of::<A>(),
555 Arc::new(AssetIndexAllocator::default()),
556 ));
557 }
558 self.insert_resource(assets)
559 .allow_ambiguous_resource::<Assets<A>>()
560 .add_event::<AssetEvent<A>>()
561 .add_event::<AssetLoadFailedEvent<A>>()
562 .register_type::<Handle<A>>()
563 .add_systems(
564 Last,
565 Assets::<A>::asset_events
566 .run_if(Assets::<A>::asset_events_condition)
567 .in_set(AssetEvents),
568 )
569 .add_systems(PreUpdate, Assets::<A>::track_assets.in_set(TrackAssets))
570 }
571
572 fn register_asset_reflect<A>(&mut self) -> &mut Self
573 where
574 A: Asset + Reflect + FromReflect + GetTypeRegistration,
575 {
576 let type_registry = self.world().resource::<AppTypeRegistry>();
577 {
578 let mut type_registry = type_registry.write();
579
580 type_registry.register::<A>();
581 type_registry.register::<Handle<A>>();
582 type_registry.register_type_data::<A, ReflectAsset>();
583 type_registry.register_type_data::<Handle<A>, ReflectHandle>();
584 }
585
586 self
587 }
588
589 fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self {
590 self.world_mut()
591 .resource_mut::<AssetServer>()
592 .preregister_loader::<L>(extensions);
593 self
594 }
595}
596
597#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
599pub struct TrackAssets;
600
601#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
605pub struct AssetEvents;
606
607#[cfg(test)]
608mod tests {
609 use crate::{
610 self as bevy_asset,
611 folder::LoadedFolder,
612 handle::Handle,
613 io::{
614 gated::{GateOpener, GatedReader},
615 memory::{Dir, MemoryAssetReader},
616 AssetReader, AssetReaderError, AssetSource, AssetSourceId, Reader,
617 },
618 loader::{AssetLoader, LoadContext},
619 Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
620 AssetPlugin, AssetServer, Assets,
621 };
622 use alloc::sync::Arc;
623 use bevy_app::{App, Update};
624 use bevy_core::TaskPoolPlugin;
625 use bevy_ecs::{
626 event::EventCursor,
627 prelude::*,
628 schedule::{LogLevel, ScheduleBuildSettings},
629 };
630 use bevy_log::LogPlugin;
631 use bevy_reflect::TypePath;
632 use bevy_utils::{Duration, HashMap};
633 use derive_more::derive::{Display, Error, From};
634 use serde::{Deserialize, Serialize};
635 use std::path::Path;
636
637 #[derive(Asset, TypePath, Debug, Default)]
638 pub struct CoolText {
639 pub text: String,
640 pub embedded: String,
641 #[dependency]
642 pub dependencies: Vec<Handle<CoolText>>,
643 #[dependency]
644 pub sub_texts: Vec<Handle<SubText>>,
645 }
646
647 #[derive(Asset, TypePath, Debug)]
648 pub struct SubText {
649 text: String,
650 }
651
652 #[derive(Serialize, Deserialize)]
653 pub struct CoolTextRon {
654 text: String,
655 dependencies: Vec<String>,
656 embedded_dependencies: Vec<String>,
657 sub_texts: Vec<String>,
658 }
659
660 #[derive(Default)]
661 pub struct CoolTextLoader;
662
663 #[derive(Error, Display, Debug, From)]
664 pub enum CoolTextLoaderError {
665 #[display("Could not load dependency: {dependency}")]
666 CannotLoadDependency { dependency: AssetPath<'static> },
667 #[display("A RON error occurred during loading")]
668 RonSpannedError(ron::error::SpannedError),
669 #[display("An IO error occurred during loading")]
670 Io(std::io::Error),
671 }
672
673 impl AssetLoader for CoolTextLoader {
674 type Asset = CoolText;
675
676 type Settings = ();
677
678 type Error = CoolTextLoaderError;
679
680 async fn load(
681 &self,
682 reader: &mut dyn Reader,
683 _settings: &Self::Settings,
684 load_context: &mut LoadContext<'_>,
685 ) -> Result<Self::Asset, Self::Error> {
686 let mut bytes = Vec::new();
687 reader.read_to_end(&mut bytes).await?;
688 let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
689 let mut embedded = String::new();
690 for dep in ron.embedded_dependencies {
691 let loaded = load_context
692 .loader()
693 .immediate()
694 .load::<CoolText>(&dep)
695 .await
696 .map_err(|_| Self::Error::CannotLoadDependency {
697 dependency: dep.into(),
698 })?;
699 let cool = loaded.get();
700 embedded.push_str(&cool.text);
701 }
702 Ok(CoolText {
703 text: ron.text,
704 embedded,
705 dependencies: ron
706 .dependencies
707 .iter()
708 .map(|p| load_context.load(p))
709 .collect(),
710 sub_texts: ron
711 .sub_texts
712 .drain(..)
713 .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text }))
714 .collect(),
715 })
716 }
717
718 fn extensions(&self) -> &[&str] {
719 &["cool.ron"]
720 }
721 }
722
723 #[derive(Default, Clone)]
725 pub struct UnstableMemoryAssetReader {
726 pub attempt_counters: Arc<std::sync::Mutex<HashMap<Box<Path>, usize>>>,
727 pub load_delay: Duration,
728 memory_reader: MemoryAssetReader,
729 failure_count: usize,
730 }
731
732 impl UnstableMemoryAssetReader {
733 pub fn new(root: Dir, failure_count: usize) -> Self {
734 Self {
735 load_delay: Duration::from_millis(10),
736 memory_reader: MemoryAssetReader { root },
737 attempt_counters: Default::default(),
738 failure_count,
739 }
740 }
741 }
742
743 impl AssetReader for UnstableMemoryAssetReader {
744 async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
745 self.memory_reader.is_directory(path).await
746 }
747 async fn read_directory<'a>(
748 &'a self,
749 path: &'a Path,
750 ) -> Result<Box<bevy_asset::io::PathStream>, AssetReaderError> {
751 self.memory_reader.read_directory(path).await
752 }
753 async fn read_meta<'a>(
754 &'a self,
755 path: &'a Path,
756 ) -> Result<impl Reader + 'a, AssetReaderError> {
757 self.memory_reader.read_meta(path).await
758 }
759 async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
760 let attempt_number = {
761 let mut attempt_counters = self.attempt_counters.lock().unwrap();
762 if let Some(existing) = attempt_counters.get_mut(path) {
763 *existing += 1;
764 *existing
765 } else {
766 attempt_counters.insert(path.into(), 1);
767 1
768 }
769 };
770
771 if attempt_number <= self.failure_count {
772 let io_error = std::io::Error::new(
773 std::io::ErrorKind::ConnectionRefused,
774 format!(
775 "Simulated failure {attempt_number} of {}",
776 self.failure_count
777 ),
778 );
779 let wait = self.load_delay;
780 return async move {
781 std::thread::sleep(wait);
782 Err(AssetReaderError::Io(io_error.into()))
783 }
784 .await;
785 }
786
787 self.memory_reader.read(path).await
788 }
789 }
790
791 fn test_app(dir: Dir) -> (App, GateOpener) {
792 let mut app = App::new();
793 let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir });
794 app.register_asset_source(
795 AssetSourceId::Default,
796 AssetSource::build().with_reader(move || Box::new(gated_memory_reader.clone())),
797 )
798 .add_plugins((
799 TaskPoolPlugin::default(),
800 LogPlugin::default(),
801 AssetPlugin::default(),
802 ));
803 (app, gate_opener)
804 }
805
806 pub fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) {
807 for _ in 0..LARGE_ITERATION_COUNT {
808 app.update();
809 if predicate(app.world_mut()).is_some() {
810 return;
811 }
812 }
813
814 panic!("Ran out of loops to return `Some` from `predicate`");
815 }
816
817 const LARGE_ITERATION_COUNT: usize = 10000;
818
819 fn get<A: Asset>(world: &World, id: AssetId<A>) -> Option<&A> {
820 world.resource::<Assets<A>>().get(id)
821 }
822
823 #[derive(Resource, Default)]
824 struct StoredEvents(Vec<AssetEvent<CoolText>>);
825
826 fn store_asset_events(
827 mut reader: EventReader<AssetEvent<CoolText>>,
828 mut storage: ResMut<StoredEvents>,
829 ) {
830 storage.0.extend(reader.read().cloned());
831 }
832
833 #[test]
834 fn load_dependencies() {
835 #[cfg(not(feature = "multi_threaded"))]
837 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
838
839 let dir = Dir::default();
840
841 let a_path = "a.cool.ron";
842 let a_ron = r#"
843(
844 text: "a",
845 dependencies: [
846 "foo/b.cool.ron",
847 "c.cool.ron",
848 ],
849 embedded_dependencies: [],
850 sub_texts: [],
851)"#;
852 let b_path = "foo/b.cool.ron";
853 let b_ron = r#"
854(
855 text: "b",
856 dependencies: [],
857 embedded_dependencies: [],
858 sub_texts: [],
859)"#;
860
861 let c_path = "c.cool.ron";
862 let c_ron = r#"
863(
864 text: "c",
865 dependencies: [
866 "d.cool.ron",
867 ],
868 embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"],
869 sub_texts: ["hello"],
870)"#;
871
872 let d_path = "d.cool.ron";
873 let d_ron = r#"
874(
875 text: "d",
876 dependencies: [],
877 embedded_dependencies: [],
878 sub_texts: [],
879)"#;
880
881 dir.insert_asset_text(Path::new(a_path), a_ron);
882 dir.insert_asset_text(Path::new(b_path), b_ron);
883 dir.insert_asset_text(Path::new(c_path), c_ron);
884 dir.insert_asset_text(Path::new(d_path), d_ron);
885
886 #[derive(Resource)]
887 struct IdResults {
888 b_id: AssetId<CoolText>,
889 c_id: AssetId<CoolText>,
890 d_id: AssetId<CoolText>,
891 }
892
893 let (mut app, gate_opener) = test_app(dir);
894 app.init_asset::<CoolText>()
895 .init_asset::<SubText>()
896 .init_resource::<StoredEvents>()
897 .register_asset_loader(CoolTextLoader)
898 .add_systems(Update, store_asset_events);
899 let asset_server = app.world().resource::<AssetServer>().clone();
900 let handle: Handle<CoolText> = asset_server.load(a_path);
901 let a_id = handle.id();
902 app.update();
903 {
904 let a_text = get::<CoolText>(app.world(), a_id);
905 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
906 assert!(a_text.is_none(), "a's asset should not exist yet");
907 assert!(a_load.is_loading());
908 assert!(a_deps.is_loading());
909 assert!(a_rec_deps.is_loading());
910 }
911
912 gate_opener.open(a_path);
915 run_app_until(&mut app, |world| {
916 let a_text = get::<CoolText>(world, a_id)?;
917 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
918 assert_eq!(a_text.text, "a");
919 assert_eq!(a_text.dependencies.len(), 2);
920 assert!(a_load.is_loaded());
921 assert!(a_deps.is_loading());
922 assert!(a_rec_deps.is_loading());
923
924 let b_id = a_text.dependencies[0].id();
925 let b_text = get::<CoolText>(world, b_id);
926 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
927 assert!(b_text.is_none(), "b component should not exist yet");
928 assert!(b_load.is_loading());
929 assert!(b_deps.is_loading());
930 assert!(b_rec_deps.is_loading());
931
932 let c_id = a_text.dependencies[1].id();
933 let c_text = get::<CoolText>(world, c_id);
934 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
935 assert!(c_text.is_none(), "c component should not exist yet");
936 assert!(c_load.is_loading());
937 assert!(c_deps.is_loading());
938 assert!(c_rec_deps.is_loading());
939 Some(())
940 });
941
942 gate_opener.open(b_path);
945 run_app_until(&mut app, |world| {
946 let a_text = get::<CoolText>(world, a_id)?;
947 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
948 assert_eq!(a_text.text, "a");
949 assert_eq!(a_text.dependencies.len(), 2);
950 assert!(a_load.is_loaded());
951 assert!(a_deps.is_loading());
952 assert!(a_rec_deps.is_loading());
953
954 let b_id = a_text.dependencies[0].id();
955 let b_text = get::<CoolText>(world, b_id)?;
956 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
957 assert_eq!(b_text.text, "b");
958 assert!(b_load.is_loaded());
959 assert!(b_deps.is_loaded());
960 assert!(b_rec_deps.is_loaded());
961
962 let c_id = a_text.dependencies[1].id();
963 let c_text = get::<CoolText>(world, c_id);
964 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
965 assert!(c_text.is_none(), "c component should not exist yet");
966 assert!(c_load.is_loading());
967 assert!(c_deps.is_loading());
968 assert!(c_rec_deps.is_loading());
969 Some(())
970 });
971
972 gate_opener.open(c_path);
975
976 gate_opener.open(a_path);
978 gate_opener.open(b_path);
979 run_app_until(&mut app, |world| {
980 let a_text = get::<CoolText>(world, a_id)?;
981 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
982 assert_eq!(a_text.text, "a");
983 assert_eq!(a_text.embedded, "");
984 assert_eq!(a_text.dependencies.len(), 2);
985 assert!(a_load.is_loaded());
986
987 let b_id = a_text.dependencies[0].id();
988 let b_text = get::<CoolText>(world, b_id)?;
989 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
990 assert_eq!(b_text.text, "b");
991 assert_eq!(b_text.embedded, "");
992 assert!(b_load.is_loaded());
993 assert!(b_deps.is_loaded());
994 assert!(b_rec_deps.is_loaded());
995
996 let c_id = a_text.dependencies[1].id();
997 let c_text = get::<CoolText>(world, c_id)?;
998 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
999 assert_eq!(c_text.text, "c");
1000 assert_eq!(c_text.embedded, "ab");
1001 assert!(c_load.is_loaded());
1002 assert!(
1003 c_deps.is_loading(),
1004 "c deps should not be loaded yet because d has not loaded"
1005 );
1006 assert!(
1007 c_rec_deps.is_loading(),
1008 "c rec deps should not be loaded yet because d has not loaded"
1009 );
1010
1011 let sub_text_id = c_text.sub_texts[0].id();
1012 let sub_text = get::<SubText>(world, sub_text_id)
1013 .expect("subtext should exist if c exists. it came from the same loader");
1014 assert_eq!(sub_text.text, "hello");
1015 let (sub_text_load, sub_text_deps, sub_text_rec_deps) =
1016 asset_server.get_load_states(sub_text_id).unwrap();
1017 assert!(sub_text_load.is_loaded());
1018 assert!(sub_text_deps.is_loaded());
1019 assert!(sub_text_rec_deps.is_loaded());
1020
1021 let d_id = c_text.dependencies[0].id();
1022 let d_text = get::<CoolText>(world, d_id);
1023 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1024 assert!(d_text.is_none(), "d component should not exist yet");
1025 assert!(d_load.is_loading());
1026 assert!(d_deps.is_loading());
1027 assert!(d_rec_deps.is_loading());
1028
1029 assert!(
1030 a_deps.is_loaded(),
1031 "If c has been loaded, the a deps should all be considered loaded"
1032 );
1033 assert!(
1034 a_rec_deps.is_loading(),
1035 "d is not loaded, so a's recursive deps should still be loading"
1036 );
1037 world.insert_resource(IdResults { b_id, c_id, d_id });
1038 Some(())
1039 });
1040
1041 gate_opener.open(d_path);
1042 run_app_until(&mut app, |world| {
1043 let a_text = get::<CoolText>(world, a_id)?;
1044 let (_a_load, _a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1045 let c_id = a_text.dependencies[1].id();
1046 let c_text = get::<CoolText>(world, c_id)?;
1047 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1048 assert_eq!(c_text.text, "c");
1049 assert_eq!(c_text.embedded, "ab");
1050
1051 let d_id = c_text.dependencies[0].id();
1052 let d_text = get::<CoolText>(world, d_id)?;
1053 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1054 assert_eq!(d_text.text, "d");
1055 assert_eq!(d_text.embedded, "");
1056
1057 assert!(c_load.is_loaded());
1058 assert!(c_deps.is_loaded());
1059 assert!(c_rec_deps.is_loaded());
1060
1061 assert!(d_load.is_loaded());
1062 assert!(d_deps.is_loaded());
1063 assert!(d_rec_deps.is_loaded());
1064
1065 assert!(
1066 a_rec_deps.is_loaded(),
1067 "d is loaded, so a's recursive deps should be loaded"
1068 );
1069 Some(())
1070 });
1071
1072 {
1073 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1074 let a = texts.get_mut(a_id).unwrap();
1075 a.text = "Changed".to_string();
1076 }
1077
1078 drop(handle);
1079
1080 app.update();
1081 assert_eq!(
1082 app.world().resource::<Assets<CoolText>>().len(),
1083 0,
1084 "CoolText asset entities should be despawned when no more handles exist"
1085 );
1086 app.update();
1087 assert_eq!(
1089 app.world().resource::<Assets<SubText>>().len(),
1090 0,
1091 "SubText asset entities should be despawned when no more handles exist"
1092 );
1093 let events = app.world_mut().remove_resource::<StoredEvents>().unwrap();
1094 let id_results = app.world_mut().remove_resource::<IdResults>().unwrap();
1095 let expected_events = vec![
1096 AssetEvent::Added { id: a_id },
1097 AssetEvent::LoadedWithDependencies {
1098 id: id_results.b_id,
1099 },
1100 AssetEvent::Added {
1101 id: id_results.b_id,
1102 },
1103 AssetEvent::Added {
1104 id: id_results.c_id,
1105 },
1106 AssetEvent::LoadedWithDependencies {
1107 id: id_results.d_id,
1108 },
1109 AssetEvent::LoadedWithDependencies {
1110 id: id_results.c_id,
1111 },
1112 AssetEvent::LoadedWithDependencies { id: a_id },
1113 AssetEvent::Added {
1114 id: id_results.d_id,
1115 },
1116 AssetEvent::Modified { id: a_id },
1117 AssetEvent::Unused { id: a_id },
1118 AssetEvent::Removed { id: a_id },
1119 AssetEvent::Unused {
1120 id: id_results.b_id,
1121 },
1122 AssetEvent::Removed {
1123 id: id_results.b_id,
1124 },
1125 AssetEvent::Unused {
1126 id: id_results.c_id,
1127 },
1128 AssetEvent::Removed {
1129 id: id_results.c_id,
1130 },
1131 AssetEvent::Unused {
1132 id: id_results.d_id,
1133 },
1134 AssetEvent::Removed {
1135 id: id_results.d_id,
1136 },
1137 ];
1138 assert_eq!(events.0, expected_events);
1139 }
1140
1141 #[test]
1142 fn failure_load_states() {
1143 #[cfg(not(feature = "multi_threaded"))]
1145 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1146
1147 let dir = Dir::default();
1148
1149 let a_path = "a.cool.ron";
1150 let a_ron = r#"
1151(
1152 text: "a",
1153 dependencies: [
1154 "b.cool.ron",
1155 "c.cool.ron",
1156 ],
1157 embedded_dependencies: [],
1158 sub_texts: []
1159)"#;
1160 let b_path = "b.cool.ron";
1161 let b_ron = r#"
1162(
1163 text: "b",
1164 dependencies: [],
1165 embedded_dependencies: [],
1166 sub_texts: []
1167)"#;
1168
1169 let c_path = "c.cool.ron";
1170 let c_ron = r#"
1171(
1172 text: "c",
1173 dependencies: [
1174 "d.cool.ron",
1175 ],
1176 embedded_dependencies: [],
1177 sub_texts: []
1178)"#;
1179
1180 let d_path = "d.cool.ron";
1181 let d_ron = r#"
1182(
1183 text: "d",
1184 dependencies: [],
1185 OH NO THIS ASSET IS MALFORMED
1186 embedded_dependencies: [],
1187 sub_texts: []
1188)"#;
1189
1190 dir.insert_asset_text(Path::new(a_path), a_ron);
1191 dir.insert_asset_text(Path::new(b_path), b_ron);
1192 dir.insert_asset_text(Path::new(c_path), c_ron);
1193 dir.insert_asset_text(Path::new(d_path), d_ron);
1194
1195 let (mut app, gate_opener) = test_app(dir);
1196 app.init_asset::<CoolText>()
1197 .register_asset_loader(CoolTextLoader);
1198 let asset_server = app.world().resource::<AssetServer>().clone();
1199 let handle: Handle<CoolText> = asset_server.load(a_path);
1200 let a_id = handle.id();
1201 {
1202 let other_handle: Handle<CoolText> = asset_server.load(a_path);
1203 assert_eq!(
1204 other_handle, handle,
1205 "handles from consecutive load calls should be equal"
1206 );
1207 assert_eq!(
1208 other_handle.id(),
1209 handle.id(),
1210 "handle ids from consecutive load calls should be equal"
1211 );
1212 }
1213
1214 gate_opener.open(a_path);
1215 gate_opener.open(b_path);
1216 gate_opener.open(c_path);
1217 gate_opener.open(d_path);
1218
1219 run_app_until(&mut app, |world| {
1220 let a_text = get::<CoolText>(world, a_id)?;
1221 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1222
1223 let b_id = a_text.dependencies[0].id();
1224 let b_text = get::<CoolText>(world, b_id)?;
1225 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1226
1227 let c_id = a_text.dependencies[1].id();
1228 let c_text = get::<CoolText>(world, c_id)?;
1229 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1230
1231 let d_id = c_text.dependencies[0].id();
1232 let d_text = get::<CoolText>(world, d_id);
1233 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1234
1235 if !d_load.is_failed() {
1236 return None;
1238 }
1239
1240 assert!(d_text.is_none());
1241 assert!(d_load.is_failed());
1242 assert!(d_deps.is_failed());
1243 assert!(d_rec_deps.is_failed());
1244
1245 assert_eq!(a_text.text, "a");
1246 assert!(a_load.is_loaded());
1247 assert!(a_deps.is_loaded());
1248 assert!(a_rec_deps.is_failed());
1249
1250 assert_eq!(b_text.text, "b");
1251 assert!(b_load.is_loaded());
1252 assert!(b_deps.is_loaded());
1253 assert!(b_rec_deps.is_loaded());
1254
1255 assert_eq!(c_text.text, "c");
1256 assert!(c_load.is_loaded());
1257 assert!(c_deps.is_failed());
1258 assert!(c_rec_deps.is_failed());
1259
1260 assert!(asset_server.load_state(a_id).is_loaded());
1261 assert!(asset_server.dependency_load_state(a_id).is_loaded());
1262 assert!(asset_server
1263 .recursive_dependency_load_state(a_id)
1264 .is_failed());
1265
1266 assert!(asset_server.is_loaded(a_id));
1267 assert!(asset_server.is_loaded_with_direct_dependencies(a_id));
1268 assert!(!asset_server.is_loaded_with_dependencies(a_id));
1269
1270 Some(())
1271 });
1272 }
1273
1274 #[test]
1275 fn dependency_load_states() {
1276 #[cfg(not(feature = "multi_threaded"))]
1278 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1279
1280 let a_path = "a.cool.ron";
1281 let a_ron = r#"
1282(
1283 text: "a",
1284 dependencies: [
1285 "b.cool.ron",
1286 "c.cool.ron",
1287 ],
1288 embedded_dependencies: [],
1289 sub_texts: []
1290)"#;
1291 let b_path = "b.cool.ron";
1292 let b_ron = r#"
1293(
1294 text: "b",
1295 dependencies: [],
1296 MALFORMED
1297 embedded_dependencies: [],
1298 sub_texts: []
1299)"#;
1300
1301 let c_path = "c.cool.ron";
1302 let c_ron = r#"
1303(
1304 text: "c",
1305 dependencies: [],
1306 embedded_dependencies: [],
1307 sub_texts: []
1308)"#;
1309
1310 let dir = Dir::default();
1311 dir.insert_asset_text(Path::new(a_path), a_ron);
1312 dir.insert_asset_text(Path::new(b_path), b_ron);
1313 dir.insert_asset_text(Path::new(c_path), c_ron);
1314
1315 let (mut app, gate_opener) = test_app(dir);
1316 app.init_asset::<CoolText>()
1317 .register_asset_loader(CoolTextLoader);
1318 let asset_server = app.world().resource::<AssetServer>().clone();
1319 let handle: Handle<CoolText> = asset_server.load(a_path);
1320 let a_id = handle.id();
1321
1322 gate_opener.open(a_path);
1323 run_app_until(&mut app, |world| {
1324 let _a_text = get::<CoolText>(world, a_id)?;
1325 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1326 assert!(a_load.is_loaded());
1327 assert!(a_deps.is_loading());
1328 assert!(a_rec_deps.is_loading());
1329 Some(())
1330 });
1331
1332 gate_opener.open(b_path);
1333 run_app_until(&mut app, |world| {
1334 let a_text = get::<CoolText>(world, a_id)?;
1335 let b_id = a_text.dependencies[0].id();
1336
1337 let (b_load, _b_deps, _b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1338 if !b_load.is_failed() {
1339 return None;
1341 }
1342
1343 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1344 assert!(a_load.is_loaded());
1345 assert!(a_deps.is_failed());
1346 assert!(a_rec_deps.is_failed());
1347 Some(())
1348 });
1349
1350 gate_opener.open(c_path);
1351 run_app_until(&mut app, |world| {
1352 let a_text = get::<CoolText>(world, a_id)?;
1353 let c_id = a_text.dependencies[1].id();
1354 let _c_text = get::<CoolText>(world, c_id)?;
1356
1357 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1358 assert!(a_load.is_loaded());
1359 assert!(
1360 a_deps.is_failed(),
1361 "Successful dependency load should not overwrite a previous failure"
1362 );
1363 assert!(
1364 a_rec_deps.is_failed(),
1365 "Successful dependency load should not overwrite a previous failure"
1366 );
1367 Some(())
1368 });
1369 }
1370
1371 const SIMPLE_TEXT: &str = r#"
1372(
1373 text: "dep",
1374 dependencies: [],
1375 embedded_dependencies: [],
1376 sub_texts: [],
1377)"#;
1378 #[test]
1379 fn keep_gotten_strong_handles() {
1380 let dir = Dir::default();
1381 dir.insert_asset_text(Path::new("dep.cool.ron"), SIMPLE_TEXT);
1382
1383 let (mut app, _) = test_app(dir);
1384 app.init_asset::<CoolText>()
1385 .init_asset::<SubText>()
1386 .init_resource::<StoredEvents>()
1387 .register_asset_loader(CoolTextLoader)
1388 .add_systems(Update, store_asset_events);
1389
1390 let id = {
1391 let handle = {
1392 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1393 let handle = texts.add(CoolText::default());
1394 texts.get_strong_handle(handle.id()).unwrap()
1395 };
1396
1397 app.update();
1398
1399 {
1400 let text = app.world().resource::<Assets<CoolText>>().get(&handle);
1401 assert!(text.is_some());
1402 }
1403 handle.id()
1404 };
1405 app.update();
1407 assert!(
1408 app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1409 "asset has no handles, so it should have been dropped last update"
1410 );
1411 }
1412
1413 #[test]
1414 fn manual_asset_management() {
1415 #[cfg(not(feature = "multi_threaded"))]
1417 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1418
1419 let dir = Dir::default();
1420 let dep_path = "dep.cool.ron";
1421
1422 dir.insert_asset_text(Path::new(dep_path), SIMPLE_TEXT);
1423
1424 let (mut app, gate_opener) = test_app(dir);
1425 app.init_asset::<CoolText>()
1426 .init_asset::<SubText>()
1427 .init_resource::<StoredEvents>()
1428 .register_asset_loader(CoolTextLoader)
1429 .add_systems(Update, store_asset_events);
1430
1431 let hello = "hello".to_string();
1432 let empty = "".to_string();
1433
1434 let id = {
1435 let handle = {
1436 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1437 texts.add(CoolText {
1438 text: hello.clone(),
1439 embedded: empty.clone(),
1440 dependencies: vec![],
1441 sub_texts: Vec::new(),
1442 })
1443 };
1444
1445 app.update();
1446
1447 {
1448 let text = app
1449 .world()
1450 .resource::<Assets<CoolText>>()
1451 .get(&handle)
1452 .unwrap();
1453 assert_eq!(text.text, hello);
1454 }
1455 handle.id()
1456 };
1457 app.update();
1459 assert!(
1460 app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1461 "asset has no handles, so it should have been dropped last update"
1462 );
1463 app.update();
1465 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1466 let expected_events = vec![
1467 AssetEvent::Added { id },
1468 AssetEvent::Unused { id },
1469 AssetEvent::Removed { id },
1470 ];
1471 assert_eq!(events, expected_events);
1472
1473 let dep_handle = app.world().resource::<AssetServer>().load(dep_path);
1474 let a = CoolText {
1475 text: "a".to_string(),
1476 embedded: empty,
1477 dependencies: vec![dep_handle.clone()],
1479 sub_texts: Vec::new(),
1480 };
1481 let a_handle = app.world().resource::<AssetServer>().load_asset(a);
1482 app.update();
1483 app.update();
1485
1486 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1487 let expected_events = vec![AssetEvent::Added { id: a_handle.id() }];
1488 assert_eq!(events, expected_events);
1489
1490 gate_opener.open(dep_path);
1491 loop {
1492 app.update();
1493 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1494 if events.is_empty() {
1495 continue;
1496 }
1497 let expected_events = vec![
1498 AssetEvent::LoadedWithDependencies {
1499 id: dep_handle.id(),
1500 },
1501 AssetEvent::LoadedWithDependencies { id: a_handle.id() },
1502 ];
1503 assert_eq!(events, expected_events);
1504 break;
1505 }
1506 app.update();
1507 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1508 let expected_events = vec![AssetEvent::Added {
1509 id: dep_handle.id(),
1510 }];
1511 assert_eq!(events, expected_events);
1512 }
1513
1514 #[test]
1515 fn load_folder() {
1516 #[cfg(not(feature = "multi_threaded"))]
1518 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1519
1520 let dir = Dir::default();
1521
1522 let a_path = "text/a.cool.ron";
1523 let a_ron = r#"
1524(
1525 text: "a",
1526 dependencies: [
1527 "b.cool.ron",
1528 ],
1529 embedded_dependencies: [],
1530 sub_texts: [],
1531)"#;
1532 let b_path = "b.cool.ron";
1533 let b_ron = r#"
1534(
1535 text: "b",
1536 dependencies: [],
1537 embedded_dependencies: [],
1538 sub_texts: [],
1539)"#;
1540
1541 let c_path = "text/c.cool.ron";
1542 let c_ron = r#"
1543(
1544 text: "c",
1545 dependencies: [
1546 ],
1547 embedded_dependencies: [],
1548 sub_texts: [],
1549)"#;
1550 dir.insert_asset_text(Path::new(a_path), a_ron);
1551 dir.insert_asset_text(Path::new(b_path), b_ron);
1552 dir.insert_asset_text(Path::new(c_path), c_ron);
1553
1554 let (mut app, gate_opener) = test_app(dir);
1555 app.init_asset::<CoolText>()
1556 .init_asset::<SubText>()
1557 .register_asset_loader(CoolTextLoader);
1558 let asset_server = app.world().resource::<AssetServer>().clone();
1559 let handle: Handle<LoadedFolder> = asset_server.load_folder("text");
1560 gate_opener.open(a_path);
1561 gate_opener.open(b_path);
1562 gate_opener.open(c_path);
1563
1564 let mut reader = EventCursor::default();
1565 run_app_until(&mut app, |world| {
1566 let events = world.resource::<Events<AssetEvent<LoadedFolder>>>();
1567 let asset_server = world.resource::<AssetServer>();
1568 let loaded_folders = world.resource::<Assets<LoadedFolder>>();
1569 let cool_texts = world.resource::<Assets<CoolText>>();
1570 for event in reader.read(events) {
1571 if let AssetEvent::LoadedWithDependencies { id } = event {
1572 if *id == handle.id() {
1573 let loaded_folder = loaded_folders.get(&handle).unwrap();
1574 let a_handle: Handle<CoolText> =
1575 asset_server.get_handle("text/a.cool.ron").unwrap();
1576 let c_handle: Handle<CoolText> =
1577 asset_server.get_handle("text/c.cool.ron").unwrap();
1578
1579 let mut found_a = false;
1580 let mut found_c = false;
1581 for asset_handle in &loaded_folder.handles {
1582 if asset_handle.id() == a_handle.id().untyped() {
1583 found_a = true;
1584 } else if asset_handle.id() == c_handle.id().untyped() {
1585 found_c = true;
1586 }
1587 }
1588 assert!(found_a);
1589 assert!(found_c);
1590 assert_eq!(loaded_folder.handles.len(), 2);
1591
1592 let a_text = cool_texts.get(&a_handle).unwrap();
1593 let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();
1594 let c_text = cool_texts.get(&c_handle).unwrap();
1595
1596 assert_eq!("a", a_text.text);
1597 assert_eq!("b", b_text.text);
1598 assert_eq!("c", c_text.text);
1599
1600 return Some(());
1601 }
1602 }
1603 }
1604 None
1605 });
1606 }
1607
1608 #[test]
1610 fn load_error_events() {
1611 #[derive(Resource, Default)]
1612 struct ErrorTracker {
1613 tick: u64,
1614 failures: usize,
1615 queued_retries: Vec<(AssetPath<'static>, AssetId<CoolText>, u64)>,
1616 finished_asset: Option<AssetId<CoolText>>,
1617 }
1618
1619 fn asset_event_handler(
1620 mut events: EventReader<AssetEvent<CoolText>>,
1621 mut tracker: ResMut<ErrorTracker>,
1622 ) {
1623 for event in events.read() {
1624 if let AssetEvent::LoadedWithDependencies { id } = event {
1625 tracker.finished_asset = Some(*id);
1626 }
1627 }
1628 }
1629
1630 fn asset_load_error_event_handler(
1631 server: Res<AssetServer>,
1632 mut errors: EventReader<AssetLoadFailedEvent<CoolText>>,
1633 mut tracker: ResMut<ErrorTracker>,
1634 ) {
1635 tracker.tick += 1;
1637
1638 let now = tracker.tick;
1640 tracker
1641 .queued_retries
1642 .retain(|(path, old_id, retry_after)| {
1643 if now > *retry_after {
1644 let new_handle = server.load::<CoolText>(path);
1645 assert_eq!(&new_handle.id(), old_id);
1646 false
1647 } else {
1648 true
1649 }
1650 });
1651
1652 for error in errors.read() {
1654 let (load_state, _, _) = server.get_load_states(error.id).unwrap();
1655 assert!(load_state.is_failed());
1656 assert_eq!(*error.path.source(), AssetSourceId::Name("unstable".into()));
1657 match &error.error {
1658 AssetLoadError::AssetReaderError(read_error) => match read_error {
1659 AssetReaderError::Io(_) => {
1660 tracker.failures += 1;
1661 if tracker.failures <= 2 {
1662 tracker.queued_retries.push((
1664 error.path.clone(),
1665 error.id,
1666 now + 10,
1667 ));
1668 } else {
1669 panic!(
1670 "Unexpected failure #{} (expected only 2)",
1671 tracker.failures
1672 );
1673 }
1674 }
1675 _ => panic!("Unexpected error type {:?}", read_error),
1676 },
1677 _ => panic!("Unexpected error type {:?}", error.error),
1678 }
1679 }
1680 }
1681
1682 let a_path = "text/a.cool.ron";
1683 let a_ron = r#"
1684(
1685 text: "a",
1686 dependencies: [],
1687 embedded_dependencies: [],
1688 sub_texts: [],
1689)"#;
1690
1691 let dir = Dir::default();
1692 dir.insert_asset_text(Path::new(a_path), a_ron);
1693 let unstable_reader = UnstableMemoryAssetReader::new(dir, 2);
1694
1695 let mut app = App::new();
1696 app.register_asset_source(
1697 "unstable",
1698 AssetSource::build().with_reader(move || Box::new(unstable_reader.clone())),
1699 )
1700 .add_plugins((
1701 TaskPoolPlugin::default(),
1702 LogPlugin::default(),
1703 AssetPlugin::default(),
1704 ))
1705 .init_asset::<CoolText>()
1706 .register_asset_loader(CoolTextLoader)
1707 .init_resource::<ErrorTracker>()
1708 .add_systems(
1709 Update,
1710 (asset_event_handler, asset_load_error_event_handler).chain(),
1711 );
1712
1713 let asset_server = app.world().resource::<AssetServer>().clone();
1714 let a_path = format!("unstable://{a_path}");
1715 let a_handle: Handle<CoolText> = asset_server.load(a_path);
1716 let a_id = a_handle.id();
1717
1718 run_app_until(&mut app, |world| {
1719 let tracker = world.resource::<ErrorTracker>();
1720 match tracker.finished_asset {
1721 Some(asset_id) => {
1722 assert_eq!(asset_id, a_id);
1723 let assets = world.resource::<Assets<CoolText>>();
1724 let result = assets.get(asset_id).unwrap();
1725 assert_eq!(result.text, "a");
1726 Some(())
1727 }
1728 None => None,
1729 }
1730 });
1731 }
1732
1733 #[test]
1734 fn ignore_system_ambiguities_on_assets() {
1735 let mut app = App::new();
1736 app.add_plugins(AssetPlugin::default())
1737 .init_asset::<CoolText>();
1738
1739 fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1740 app.add_systems(Update, (uses_assets, uses_assets));
1741 app.edit_schedule(Update, |s| {
1742 s.set_build_settings(ScheduleBuildSettings {
1743 ambiguity_detection: LogLevel::Error,
1744 ..Default::default()
1745 });
1746 });
1747
1748 app.world_mut().run_schedule(Update);
1750 }
1751
1752 #[derive(Asset, TypePath)]
1754 pub struct TestAsset;
1755
1756 #[allow(dead_code)]
1757 #[derive(Asset, TypePath)]
1758 pub enum EnumTestAsset {
1759 Unnamed(#[dependency] Handle<TestAsset>),
1760 Named {
1761 #[dependency]
1762 handle: Handle<TestAsset>,
1763 #[dependency]
1764 vec_handles: Vec<Handle<TestAsset>>,
1765 #[dependency]
1766 embedded: TestAsset,
1767 },
1768 StructStyle(#[dependency] TestAsset),
1769 Empty,
1770 }
1771
1772 #[allow(dead_code)]
1773 #[derive(Asset, TypePath)]
1774 pub struct StructTestAsset {
1775 #[dependency]
1776 handle: Handle<TestAsset>,
1777 #[dependency]
1778 embedded: TestAsset,
1779 }
1780
1781 #[allow(dead_code)]
1782 #[derive(Asset, TypePath)]
1783 pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);
1784}