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