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