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}