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;
191use bevy_diagnostic::{Diagnostic, DiagnosticsStore, RegisterDiagnostic};
192pub use direct_access_ext::DirectAssetAccessExt;
193pub use event::*;
194pub use folder::*;
195pub use futures_lite::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
196pub use handle::*;
197pub use id::*;
198pub use loader::*;
199pub use loader_builders::{
200 Deferred, DynamicTyped, Immediate, NestedLoader, StaticTyped, UnknownTyped,
201};
202pub use path::*;
203pub use reflect::*;
204pub use render_asset::*;
205pub use server::*;
206
207pub use uuid;
208
209use crate::{
210 io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId},
211 processor::{AssetProcessor, Process},
212};
213use alloc::{
214 string::{String, ToString},
215 sync::Arc,
216 vec::Vec,
217};
218use bevy_app::{App, Plugin, PostUpdate, PreUpdate};
219use bevy_ecs::{prelude::Component, schedule::common_conditions::resource_exists};
220use bevy_ecs::{
221 reflect::AppTypeRegistry,
222 schedule::{IntoScheduleConfigs, SystemSet},
223 world::FromWorld,
224};
225use bevy_platform::collections::HashSet;
226use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath};
227use core::any::TypeId;
228use tracing::error;
229
230pub struct AssetPlugin {
238 pub file_path: String,
240 pub processed_file_path: String,
242 pub watch_for_changes_override: Option<bool>,
249 pub use_asset_processor_override: Option<bool>,
255 pub mode: AssetMode,
257 pub meta_check: AssetMetaCheck,
259 pub unapproved_path_mode: UnapprovedPathMode,
264}
265
266#[derive(Clone, Default)]
279pub enum UnapprovedPathMode {
280 Allow,
282 Deny,
285 #[default]
287 Forbid,
288}
289
290#[derive(Debug)]
297pub enum AssetMode {
298 Unprocessed,
303 Processed,
318}
319
320#[derive(Debug, Default, Clone)]
323pub enum AssetMetaCheck {
324 #[default]
326 Always,
327 Paths(HashSet<AssetPath<'static>>),
329 Never,
331}
332
333impl Default for AssetPlugin {
334 fn default() -> Self {
335 Self {
336 mode: AssetMode::Unprocessed,
337 file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(),
338 processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
339 watch_for_changes_override: None,
340 use_asset_processor_override: None,
341 meta_check: AssetMetaCheck::default(),
342 unapproved_path_mode: UnapprovedPathMode::default(),
343 }
344 }
345}
346
347impl AssetPlugin {
348 const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets";
349 const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default";
352}
353
354impl Plugin for AssetPlugin {
355 fn build(&self, app: &mut App) {
356 let embedded = EmbeddedAssetRegistry::default();
357 {
358 let mut sources = app
359 .world_mut()
360 .get_resource_or_init::<AssetSourceBuilders>();
361 sources.init_default_source(
362 &self.file_path,
363 (!matches!(self.mode, AssetMode::Unprocessed))
364 .then_some(self.processed_file_path.as_str()),
365 );
366 embedded.register_source(&mut sources);
367 }
368 {
369 let watch = self
370 .watch_for_changes_override
371 .unwrap_or(cfg!(feature = "watch"));
372 match self.mode {
373 AssetMode::Unprocessed => {
374 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
375 let sources = builders.build_sources(watch, false);
376
377 app.insert_resource(AssetServer::new_with_meta_check(
378 Arc::new(sources),
379 AssetServerMode::Unprocessed,
380 self.meta_check.clone(),
381 watch,
382 self.unapproved_path_mode.clone(),
383 ));
384 }
385 AssetMode::Processed => {
386 let use_asset_processor = self
387 .use_asset_processor_override
388 .unwrap_or(cfg!(feature = "asset_processor"));
389 if use_asset_processor {
390 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
391 let (processor, sources) = AssetProcessor::new(&mut builders, watch);
392 app.insert_resource(AssetServer::new_with_loaders(
394 sources,
395 processor.server().data.loaders.clone(),
396 AssetServerMode::Processed,
397 AssetMetaCheck::Always,
398 watch,
399 self.unapproved_path_mode.clone(),
400 ))
401 .insert_resource(processor)
402 .add_systems(bevy_app::Startup, AssetProcessor::start);
403 } else {
404 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
405 let sources = builders.build_sources(false, watch);
406 app.insert_resource(AssetServer::new_with_meta_check(
407 Arc::new(sources),
408 AssetServerMode::Processed,
409 AssetMetaCheck::Always,
410 watch,
411 self.unapproved_path_mode.clone(),
412 ));
413 }
414 }
415 }
416 }
417 app.insert_resource(embedded)
418 .init_asset::<LoadedFolder>()
419 .init_asset::<LoadedUntypedAsset>()
420 .init_asset::<()>()
421 .add_message::<UntypedAssetLoadFailedEvent>()
422 .configure_sets(
423 PreUpdate,
424 AssetTrackingSystems.after(handle_internal_asset_events),
425 )
426 .add_systems(
431 PreUpdate,
432 (
433 handle_internal_asset_events.ambiguous_with_all(),
434 publish_asset_server_diagnostics.run_if(resource_exists::<DiagnosticsStore>),
437 )
438 .chain(),
439 )
440 .register_diagnostic(Diagnostic::new(AssetServer::STARTED_LOAD_COUNT));
441 }
442}
443
444#[diagnostic::on_unimplemented(
452 message = "`{Self}` is not an `Asset`",
453 label = "invalid `Asset`",
454 note = "consider annotating `{Self}` with `#[derive(Asset)]`"
455)]
456pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {}
457
458pub trait AsAssetId: Component {
460 type Asset: Asset;
462
463 fn as_asset_id(&self) -> AssetId<Self::Asset>;
465}
466
467pub trait VisitAssetDependencies {
472 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId));
473}
474
475impl<A: Asset> VisitAssetDependencies for Handle<A> {
476 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
477 visit(self.id().untyped());
478 }
479}
480
481impl<A: Asset> VisitAssetDependencies for Option<Handle<A>> {
482 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
483 if let Some(handle) = self {
484 visit(handle.id().untyped());
485 }
486 }
487}
488
489impl VisitAssetDependencies for UntypedHandle {
490 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
491 visit(self.id());
492 }
493}
494
495impl VisitAssetDependencies for Option<UntypedHandle> {
496 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
497 if let Some(handle) = self {
498 visit(handle.id());
499 }
500 }
501}
502
503impl<A: Asset, const N: usize> VisitAssetDependencies for [Handle<A>; N] {
504 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
505 for dependency in self {
506 visit(dependency.id().untyped());
507 }
508 }
509}
510
511impl<const N: usize> VisitAssetDependencies for [UntypedHandle; N] {
512 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
513 for dependency in self {
514 visit(dependency.id());
515 }
516 }
517}
518
519impl<A: Asset> VisitAssetDependencies for Vec<Handle<A>> {
520 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
521 for dependency in self {
522 visit(dependency.id().untyped());
523 }
524 }
525}
526
527impl VisitAssetDependencies for Vec<UntypedHandle> {
528 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
529 for dependency in self {
530 visit(dependency.id());
531 }
532 }
533}
534
535impl<A: Asset> VisitAssetDependencies for HashSet<Handle<A>> {
536 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
537 for dependency in self {
538 visit(dependency.id().untyped());
539 }
540 }
541}
542
543impl VisitAssetDependencies for HashSet<UntypedHandle> {
544 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
545 for dependency in self {
546 visit(dependency.id());
547 }
548 }
549}
550
551pub trait AssetApp {
553 fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self;
555 fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self;
557 fn register_asset_source(
562 &mut self,
563 id: impl Into<AssetSourceId<'static>>,
564 source: AssetSourceBuilder,
565 ) -> &mut Self;
566 fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self;
568 fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self;
570 fn init_asset<A: Asset>(&mut self) -> &mut Self;
578 fn register_asset_reflect<A>(&mut self) -> &mut Self
583 where
584 A: Asset + Reflect + FromReflect + GetTypeRegistration;
585 fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self;
588}
589
590impl AssetApp for App {
591 fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
592 self.world()
593 .resource::<AssetServer>()
594 .register_loader(loader);
595 self
596 }
597
598 fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self {
599 if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
600 asset_processor.register_processor(processor);
601 }
602 self
603 }
604
605 fn register_asset_source(
606 &mut self,
607 id: impl Into<AssetSourceId<'static>>,
608 source: AssetSourceBuilder,
609 ) -> &mut Self {
610 let id = id.into();
611 if self.world().get_resource::<AssetServer>().is_some() {
612 error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id);
613 }
614
615 {
616 let mut sources = self
617 .world_mut()
618 .get_resource_or_init::<AssetSourceBuilders>();
619 sources.insert(id, source);
620 }
621
622 self
623 }
624
625 fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self {
626 if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
627 asset_processor.set_default_processor::<P>(extension);
628 }
629 self
630 }
631
632 fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self {
633 let loader = L::from_world(self.world_mut());
634 self.register_asset_loader(loader)
635 }
636
637 fn init_asset<A: Asset>(&mut self) -> &mut Self {
638 let assets = Assets::<A>::default();
639 self.world()
640 .resource::<AssetServer>()
641 .register_asset(&assets);
642 if self.world().contains_resource::<AssetProcessor>() {
643 let processor = self.world().resource::<AssetProcessor>();
644 processor
648 .server()
649 .register_handle_provider(AssetHandleProvider::new(
650 TypeId::of::<A>(),
651 Arc::new(AssetIndexAllocator::default()),
652 ));
653 }
654 self.insert_resource(assets)
655 .allow_ambiguous_resource::<Assets<A>>()
656 .add_message::<AssetEvent<A>>()
657 .add_message::<AssetLoadFailedEvent<A>>()
658 .register_type::<Handle<A>>()
659 .add_systems(
660 PostUpdate,
661 Assets::<A>::asset_events
662 .run_if(Assets::<A>::asset_events_condition)
663 .in_set(AssetEventSystems),
664 )
665 .add_systems(
666 PreUpdate,
667 Assets::<A>::track_assets.in_set(AssetTrackingSystems),
668 )
669 }
670
671 fn register_asset_reflect<A>(&mut self) -> &mut Self
672 where
673 A: Asset + Reflect + FromReflect + GetTypeRegistration,
674 {
675 let type_registry = self.world().resource::<AppTypeRegistry>();
676 {
677 let mut type_registry = type_registry.write();
678
679 type_registry.register::<A>();
680 type_registry.register::<Handle<A>>();
681 type_registry.register_type_data::<A, ReflectAsset>();
682 type_registry.register_type_data::<Handle<A>, ReflectHandle>();
683 }
684
685 self
686 }
687
688 fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self {
689 self.world_mut()
690 .resource_mut::<AssetServer>()
691 .preregister_loader::<L>(extensions);
692 self
693 }
694}
695
696#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
698pub struct AssetTrackingSystems;
699
700#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
704pub struct AssetEventSystems;
705
706#[cfg(test)]
707mod tests {
708 use crate::{
709 folder::LoadedFolder,
710 handle::Handle,
711 io::{
712 gated::{GateOpener, GatedReader},
713 memory::{Dir, MemoryAssetReader, MemoryAssetWriter},
714 AssetReader, AssetReaderError, AssetSourceBuilder, AssetSourceEvent, AssetSourceId,
715 AssetWatcher, Reader,
716 },
717 loader::{AssetLoader, LoadContext},
718 Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
719 AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, LoadedAsset,
720 UnapprovedPathMode, UntypedHandle, WriteDefaultMetaError,
721 };
722 use alloc::{
723 boxed::Box,
724 format,
725 string::{String, ToString},
726 sync::Arc,
727 vec,
728 vec::Vec,
729 };
730 use async_channel::{Receiver, Sender};
731 use bevy_app::{App, TaskPoolPlugin, Update};
732 use bevy_diagnostic::{DiagnosticsPlugin, DiagnosticsStore};
733 use bevy_ecs::{
734 message::MessageCursor,
735 prelude::*,
736 schedule::{LogLevel, ScheduleBuildSettings},
737 };
738 use bevy_platform::{
739 collections::{HashMap, HashSet},
740 sync::Mutex,
741 };
742 use bevy_reflect::TypePath;
743 use core::time::Duration;
744 use futures_lite::AsyncReadExt;
745 use serde::{Deserialize, Serialize};
746 use std::path::{Path, PathBuf};
747 use thiserror::Error;
748
749 #[derive(Asset, TypePath, Debug, Default)]
750 pub struct CoolText {
751 pub text: String,
752 pub embedded: String,
753 #[dependency]
754 pub dependencies: Vec<Handle<CoolText>>,
755 #[dependency]
756 pub sub_texts: Vec<Handle<SubText>>,
757 }
758
759 #[derive(Asset, TypePath, Debug)]
760 pub struct SubText {
761 pub text: String,
762 }
763
764 #[derive(Serialize, Deserialize, Default)]
765 pub struct CoolTextRon {
766 pub text: String,
767 pub dependencies: Vec<String>,
768 pub embedded_dependencies: Vec<String>,
769 pub sub_texts: Vec<String>,
770 }
771
772 #[derive(Default, TypePath)]
773 pub struct CoolTextLoader;
774
775 #[derive(Error, Debug)]
776 pub enum CoolTextLoaderError {
777 #[error("Could not load dependency: {dependency}")]
778 CannotLoadDependency { dependency: AssetPath<'static> },
779 #[error("A RON error occurred during loading")]
780 RonSpannedError(#[from] ron::error::SpannedError),
781 #[error("An IO error occurred during loading")]
782 Io(#[from] std::io::Error),
783 }
784
785 impl AssetLoader for CoolTextLoader {
786 type Asset = CoolText;
787
788 type Settings = ();
789
790 type Error = CoolTextLoaderError;
791
792 async fn load(
793 &self,
794 reader: &mut dyn Reader,
795 _settings: &Self::Settings,
796 load_context: &mut LoadContext<'_>,
797 ) -> Result<Self::Asset, Self::Error> {
798 let mut bytes = Vec::new();
799 reader.read_to_end(&mut bytes).await?;
800 let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
801 let mut embedded = String::new();
802 for dep in ron.embedded_dependencies {
803 let loaded = load_context
804 .loader()
805 .immediate()
806 .load::<CoolText>(&dep)
807 .await
808 .map_err(|_| Self::Error::CannotLoadDependency {
809 dependency: dep.into(),
810 })?;
811 let cool = loaded.get();
812 embedded.push_str(&cool.text);
813 }
814 Ok(CoolText {
815 text: ron.text,
816 embedded,
817 dependencies: ron
818 .dependencies
819 .iter()
820 .map(|p| load_context.load(p))
821 .collect(),
822 sub_texts: ron
823 .sub_texts
824 .drain(..)
825 .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text }))
826 .collect(),
827 })
828 }
829
830 fn extensions(&self) -> &[&str] {
831 &["cool.ron"]
832 }
833 }
834
835 #[derive(Default, Clone)]
837 pub struct UnstableMemoryAssetReader {
838 pub attempt_counters: Arc<Mutex<HashMap<Box<Path>, usize>>>,
839 pub load_delay: Duration,
840 memory_reader: MemoryAssetReader,
841 failure_count: usize,
842 }
843
844 impl UnstableMemoryAssetReader {
845 pub fn new(root: Dir, failure_count: usize) -> Self {
846 Self {
847 load_delay: Duration::from_millis(10),
848 memory_reader: MemoryAssetReader { root },
849 attempt_counters: Default::default(),
850 failure_count,
851 }
852 }
853 }
854
855 impl AssetReader for UnstableMemoryAssetReader {
856 async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
857 self.memory_reader.is_directory(path).await
858 }
859 async fn read_directory<'a>(
860 &'a self,
861 path: &'a Path,
862 ) -> Result<Box<bevy_asset::io::PathStream>, AssetReaderError> {
863 self.memory_reader.read_directory(path).await
864 }
865 async fn read_meta<'a>(
866 &'a self,
867 path: &'a Path,
868 ) -> Result<impl Reader + 'a, AssetReaderError> {
869 self.memory_reader.read_meta(path).await
870 }
871 async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
872 let attempt_number = {
873 let mut attempt_counters = self.attempt_counters.lock().unwrap();
874 if let Some(existing) = attempt_counters.get_mut(path) {
875 *existing += 1;
876 *existing
877 } else {
878 attempt_counters.insert(path.into(), 1);
879 1
880 }
881 };
882
883 if attempt_number <= self.failure_count {
884 let io_error = std::io::Error::new(
885 std::io::ErrorKind::ConnectionRefused,
886 format!(
887 "Simulated failure {attempt_number} of {}",
888 self.failure_count
889 ),
890 );
891 let wait = self.load_delay;
892 return async move {
893 std::thread::sleep(wait);
894 Err(AssetReaderError::Io(io_error.into()))
895 }
896 .await;
897 }
898
899 self.memory_reader.read(path).await
900 }
901 }
902
903 fn create_app() -> (App, Dir) {
905 let mut app = App::new();
906 let dir = Dir::default();
907 let dir_clone = dir.clone();
908 let dir_clone2 = dir.clone();
909 app.register_asset_source(
910 AssetSourceId::Default,
911 AssetSourceBuilder::new(move || {
912 Box::new(MemoryAssetReader {
913 root: dir_clone.clone(),
914 })
915 })
916 .with_writer(move |_| {
917 Some(Box::new(MemoryAssetWriter {
918 root: dir_clone2.clone(),
919 }))
920 }),
921 )
922 .add_plugins((
923 TaskPoolPlugin::default(),
924 AssetPlugin {
925 watch_for_changes_override: Some(false),
926 use_asset_processor_override: Some(false),
927 ..Default::default()
928 },
929 DiagnosticsPlugin,
930 ));
931 (app, dir)
932 }
933
934 fn create_app_with_gate(dir: Dir) -> (App, GateOpener) {
935 let mut app = App::new();
936 let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir });
937 app.register_asset_source(
938 AssetSourceId::Default,
939 AssetSourceBuilder::new(move || Box::new(gated_memory_reader.clone())),
940 )
941 .add_plugins((
942 TaskPoolPlugin::default(),
943 AssetPlugin::default(),
944 DiagnosticsPlugin,
945 ));
946 (app, gate_opener)
947 }
948
949 pub fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) {
950 for _ in 0..LARGE_ITERATION_COUNT {
951 app.update();
952 if predicate(app.world_mut()).is_some() {
953 return;
954 }
955 }
956
957 panic!("Ran out of loops to return `Some` from `predicate`");
958 }
959
960 const LARGE_ITERATION_COUNT: usize = 10000;
961
962 fn get<A: Asset>(world: &World, id: AssetId<A>) -> Option<&A> {
963 world.resource::<Assets<A>>().get(id)
964 }
965
966 fn get_started_load_count(world: &World) -> usize {
967 world
968 .resource::<DiagnosticsStore>()
969 .get_measurement(&AssetServer::STARTED_LOAD_COUNT)
970 .map(|measurement| measurement.value as _)
971 .unwrap_or_default()
972 }
973
974 #[derive(Resource, Default)]
975 struct StoredEvents(Vec<AssetEvent<CoolText>>);
976
977 fn store_asset_events(
978 mut reader: MessageReader<AssetEvent<CoolText>>,
979 mut storage: ResMut<StoredEvents>,
980 ) {
981 storage.0.extend(reader.read().cloned());
982 }
983
984 #[test]
985 fn load_dependencies() {
986 let dir = Dir::default();
987
988 let a_path = "a.cool.ron";
989 let a_ron = r#"
990(
991 text: "a",
992 dependencies: [
993 "foo/b.cool.ron",
994 "c.cool.ron",
995 ],
996 embedded_dependencies: [],
997 sub_texts: [],
998)"#;
999 let b_path = "foo/b.cool.ron";
1000 let b_ron = r#"
1001(
1002 text: "b",
1003 dependencies: [],
1004 embedded_dependencies: [],
1005 sub_texts: [],
1006)"#;
1007
1008 let c_path = "c.cool.ron";
1009 let c_ron = r#"
1010(
1011 text: "c",
1012 dependencies: [
1013 "d.cool.ron",
1014 ],
1015 embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"],
1016 sub_texts: ["hello"],
1017)"#;
1018
1019 let d_path = "d.cool.ron";
1020 let d_ron = r#"
1021(
1022 text: "d",
1023 dependencies: [],
1024 embedded_dependencies: [],
1025 sub_texts: [],
1026)"#;
1027
1028 dir.insert_asset_text(Path::new(a_path), a_ron);
1029 dir.insert_asset_text(Path::new(b_path), b_ron);
1030 dir.insert_asset_text(Path::new(c_path), c_ron);
1031 dir.insert_asset_text(Path::new(d_path), d_ron);
1032
1033 #[derive(Resource)]
1034 struct IdResults {
1035 b_id: AssetId<CoolText>,
1036 c_id: AssetId<CoolText>,
1037 d_id: AssetId<CoolText>,
1038 }
1039
1040 let (mut app, gate_opener) = create_app_with_gate(dir);
1041 app.init_asset::<CoolText>()
1042 .init_asset::<SubText>()
1043 .init_resource::<StoredEvents>()
1044 .register_asset_loader(CoolTextLoader)
1045 .add_systems(Update, store_asset_events);
1046 let asset_server = app.world().resource::<AssetServer>().clone();
1047 let handle: Handle<CoolText> = asset_server.load(a_path);
1048 let a_id = handle.id();
1049 app.update();
1050 assert_eq!(get_started_load_count(app.world()), 1);
1051
1052 {
1053 let a_text = get::<CoolText>(app.world(), a_id);
1054 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1055 assert!(a_text.is_none(), "a's asset should not exist yet");
1056 assert!(a_load.is_loading());
1057 assert!(a_deps.is_loading());
1058 assert!(a_rec_deps.is_loading());
1059 }
1060
1061 gate_opener.open(a_path);
1064 run_app_until(&mut app, |world| {
1065 let a_text = get::<CoolText>(world, a_id)?;
1066 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1067 assert_eq!(a_text.text, "a");
1068 assert_eq!(a_text.dependencies.len(), 2);
1069 assert!(a_load.is_loaded());
1070 assert!(a_deps.is_loading());
1071 assert!(a_rec_deps.is_loading());
1072
1073 let b_id = a_text.dependencies[0].id();
1074 let b_text = get::<CoolText>(world, b_id);
1075 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1076 assert!(b_text.is_none(), "b component should not exist yet");
1077 assert!(b_load.is_loading());
1078 assert!(b_deps.is_loading());
1079 assert!(b_rec_deps.is_loading());
1080
1081 let c_id = a_text.dependencies[1].id();
1082 let c_text = get::<CoolText>(world, c_id);
1083 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1084 assert!(c_text.is_none(), "c component should not exist yet");
1085 assert!(c_load.is_loading());
1086 assert!(c_deps.is_loading());
1087 assert!(c_rec_deps.is_loading());
1088 Some(())
1089 });
1090 assert_eq!(get_started_load_count(app.world()), 3);
1091
1092 gate_opener.open(b_path);
1095 run_app_until(&mut app, |world| {
1096 let a_text = get::<CoolText>(world, a_id)?;
1097 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1098 assert_eq!(a_text.text, "a");
1099 assert_eq!(a_text.dependencies.len(), 2);
1100 assert!(a_load.is_loaded());
1101 assert!(a_deps.is_loading());
1102 assert!(a_rec_deps.is_loading());
1103
1104 let b_id = a_text.dependencies[0].id();
1105 let b_text = get::<CoolText>(world, b_id)?;
1106 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1107 assert_eq!(b_text.text, "b");
1108 assert!(b_load.is_loaded());
1109 assert!(b_deps.is_loaded());
1110 assert!(b_rec_deps.is_loaded());
1111
1112 let c_id = a_text.dependencies[1].id();
1113 let c_text = get::<CoolText>(world, c_id);
1114 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1115 assert!(c_text.is_none(), "c component should not exist yet");
1116 assert!(c_load.is_loading());
1117 assert!(c_deps.is_loading());
1118 assert!(c_rec_deps.is_loading());
1119 Some(())
1120 });
1121 assert_eq!(get_started_load_count(app.world()), 3);
1122
1123 gate_opener.open(c_path);
1126
1127 gate_opener.open(a_path);
1129 gate_opener.open(b_path);
1130 run_app_until(&mut app, |world| {
1131 let a_text = get::<CoolText>(world, a_id)?;
1132 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1133 assert_eq!(a_text.text, "a");
1134 assert_eq!(a_text.embedded, "");
1135 assert_eq!(a_text.dependencies.len(), 2);
1136 assert!(a_load.is_loaded());
1137
1138 let b_id = a_text.dependencies[0].id();
1139 let b_text = get::<CoolText>(world, b_id)?;
1140 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1141 assert_eq!(b_text.text, "b");
1142 assert_eq!(b_text.embedded, "");
1143 assert!(b_load.is_loaded());
1144 assert!(b_deps.is_loaded());
1145 assert!(b_rec_deps.is_loaded());
1146
1147 let c_id = a_text.dependencies[1].id();
1148 let c_text = get::<CoolText>(world, c_id)?;
1149 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1150 assert_eq!(c_text.text, "c");
1151 assert_eq!(c_text.embedded, "ab");
1152 assert!(c_load.is_loaded());
1153 assert!(
1154 c_deps.is_loading(),
1155 "c deps should not be loaded yet because d has not loaded"
1156 );
1157 assert!(
1158 c_rec_deps.is_loading(),
1159 "c rec deps should not be loaded yet because d has not loaded"
1160 );
1161
1162 let sub_text_id = c_text.sub_texts[0].id();
1163 let sub_text = get::<SubText>(world, sub_text_id)
1164 .expect("subtext should exist if c exists. it came from the same loader");
1165 assert_eq!(sub_text.text, "hello");
1166 let (sub_text_load, sub_text_deps, sub_text_rec_deps) =
1167 asset_server.get_load_states(sub_text_id).unwrap();
1168 assert!(sub_text_load.is_loaded());
1169 assert!(sub_text_deps.is_loaded());
1170 assert!(sub_text_rec_deps.is_loaded());
1171
1172 let d_id = c_text.dependencies[0].id();
1173 let d_text = get::<CoolText>(world, d_id);
1174 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1175 assert!(d_text.is_none(), "d component should not exist yet");
1176 assert!(d_load.is_loading());
1177 assert!(d_deps.is_loading());
1178 assert!(d_rec_deps.is_loading());
1179
1180 assert!(
1181 a_deps.is_loaded(),
1182 "If c has been loaded, the a deps should all be considered loaded"
1183 );
1184 assert!(
1185 a_rec_deps.is_loading(),
1186 "d is not loaded, so a's recursive deps should still be loading"
1187 );
1188 world.insert_resource(IdResults { b_id, c_id, d_id });
1189 Some(())
1190 });
1191 assert_eq!(get_started_load_count(app.world()), 6);
1192
1193 gate_opener.open(d_path);
1194 run_app_until(&mut app, |world| {
1195 let a_text = get::<CoolText>(world, a_id)?;
1196 let (_a_load, _a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1197 let c_id = a_text.dependencies[1].id();
1198 let c_text = get::<CoolText>(world, c_id)?;
1199 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1200 assert_eq!(c_text.text, "c");
1201 assert_eq!(c_text.embedded, "ab");
1202
1203 let d_id = c_text.dependencies[0].id();
1204 let d_text = get::<CoolText>(world, d_id)?;
1205 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1206 assert_eq!(d_text.text, "d");
1207 assert_eq!(d_text.embedded, "");
1208
1209 assert!(c_load.is_loaded());
1210 assert!(c_deps.is_loaded());
1211 assert!(c_rec_deps.is_loaded());
1212
1213 assert!(d_load.is_loaded());
1214 assert!(d_deps.is_loaded());
1215 assert!(d_rec_deps.is_loaded());
1216
1217 assert!(
1218 a_rec_deps.is_loaded(),
1219 "d is loaded, so a's recursive deps should be loaded"
1220 );
1221 Some(())
1222 });
1223
1224 assert_eq!(get_started_load_count(app.world()), 6);
1225
1226 {
1227 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1228 let a = texts.get_mut(a_id).unwrap();
1229 a.text = "Changed".to_string();
1230 }
1231
1232 drop(handle);
1233
1234 app.update();
1235 assert_eq!(
1236 app.world().resource::<Assets<CoolText>>().len(),
1237 0,
1238 "CoolText asset entities should be despawned when no more handles exist"
1239 );
1240 app.update();
1241 assert_eq!(
1243 app.world().resource::<Assets<SubText>>().len(),
1244 0,
1245 "SubText asset entities should be despawned when no more handles exist"
1246 );
1247 let events = app.world_mut().remove_resource::<StoredEvents>().unwrap();
1248 let id_results = app.world_mut().remove_resource::<IdResults>().unwrap();
1249 let expected_events = vec![
1250 AssetEvent::Added { id: a_id },
1251 AssetEvent::LoadedWithDependencies {
1252 id: id_results.b_id,
1253 },
1254 AssetEvent::Added {
1255 id: id_results.b_id,
1256 },
1257 AssetEvent::Added {
1258 id: id_results.c_id,
1259 },
1260 AssetEvent::LoadedWithDependencies {
1261 id: id_results.d_id,
1262 },
1263 AssetEvent::LoadedWithDependencies {
1264 id: id_results.c_id,
1265 },
1266 AssetEvent::LoadedWithDependencies { id: a_id },
1267 AssetEvent::Added {
1268 id: id_results.d_id,
1269 },
1270 AssetEvent::Modified { id: a_id },
1271 AssetEvent::Unused { id: a_id },
1272 AssetEvent::Removed { id: a_id },
1273 AssetEvent::Unused {
1274 id: id_results.b_id,
1275 },
1276 AssetEvent::Removed {
1277 id: id_results.b_id,
1278 },
1279 AssetEvent::Unused {
1280 id: id_results.c_id,
1281 },
1282 AssetEvent::Removed {
1283 id: id_results.c_id,
1284 },
1285 AssetEvent::Unused {
1286 id: id_results.d_id,
1287 },
1288 AssetEvent::Removed {
1289 id: id_results.d_id,
1290 },
1291 ];
1292 assert_eq!(events.0, expected_events);
1293 }
1294
1295 #[test]
1296 fn failure_load_states() {
1297 let dir = Dir::default();
1298
1299 let a_path = "a.cool.ron";
1300 let a_ron = r#"
1301(
1302 text: "a",
1303 dependencies: [
1304 "b.cool.ron",
1305 "c.cool.ron",
1306 ],
1307 embedded_dependencies: [],
1308 sub_texts: []
1309)"#;
1310 let b_path = "b.cool.ron";
1311 let b_ron = r#"
1312(
1313 text: "b",
1314 dependencies: [],
1315 embedded_dependencies: [],
1316 sub_texts: []
1317)"#;
1318
1319 let c_path = "c.cool.ron";
1320 let c_ron = r#"
1321(
1322 text: "c",
1323 dependencies: [
1324 "d.cool.ron",
1325 ],
1326 embedded_dependencies: [],
1327 sub_texts: []
1328)"#;
1329
1330 let d_path = "d.cool.ron";
1331 let d_ron = r#"
1332(
1333 text: "d",
1334 dependencies: [],
1335 OH NO THIS ASSET IS MALFORMED
1336 embedded_dependencies: [],
1337 sub_texts: []
1338)"#;
1339
1340 dir.insert_asset_text(Path::new(a_path), a_ron);
1341 dir.insert_asset_text(Path::new(b_path), b_ron);
1342 dir.insert_asset_text(Path::new(c_path), c_ron);
1343 dir.insert_asset_text(Path::new(d_path), d_ron);
1344
1345 let (mut app, gate_opener) = create_app_with_gate(dir);
1346 app.init_asset::<CoolText>()
1347 .register_asset_loader(CoolTextLoader);
1348 let asset_server = app.world().resource::<AssetServer>().clone();
1349 let handle: Handle<CoolText> = asset_server.load(a_path);
1350 let a_id = handle.id();
1351
1352 app.update();
1353 assert_eq!(get_started_load_count(app.world()), 1);
1354 {
1355 let other_handle: Handle<CoolText> = asset_server.load(a_path);
1356 assert_eq!(
1357 other_handle, handle,
1358 "handles from consecutive load calls should be equal"
1359 );
1360 assert_eq!(
1361 other_handle.id(),
1362 handle.id(),
1363 "handle ids from consecutive load calls should be equal"
1364 );
1365
1366 app.update();
1367 assert_eq!(get_started_load_count(app.world()), 1);
1369 }
1370
1371 gate_opener.open(a_path);
1372 gate_opener.open(b_path);
1373 gate_opener.open(c_path);
1374 gate_opener.open(d_path);
1375
1376 run_app_until(&mut app, |world| {
1377 let a_text = get::<CoolText>(world, a_id)?;
1378 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1379
1380 let b_id = a_text.dependencies[0].id();
1381 let b_text = get::<CoolText>(world, b_id)?;
1382 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1383
1384 let c_id = a_text.dependencies[1].id();
1385 let c_text = get::<CoolText>(world, c_id)?;
1386 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1387
1388 let d_id = c_text.dependencies[0].id();
1389 let d_text = get::<CoolText>(world, d_id);
1390 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1391
1392 if !d_load.is_failed() {
1393 return None;
1395 }
1396
1397 assert!(d_text.is_none());
1398 assert!(d_load.is_failed());
1399 assert!(d_deps.is_failed());
1400 assert!(d_rec_deps.is_failed());
1401
1402 assert_eq!(a_text.text, "a");
1403 assert!(a_load.is_loaded());
1404 assert!(a_deps.is_loaded());
1405 assert!(a_rec_deps.is_failed());
1406
1407 assert_eq!(b_text.text, "b");
1408 assert!(b_load.is_loaded());
1409 assert!(b_deps.is_loaded());
1410 assert!(b_rec_deps.is_loaded());
1411
1412 assert_eq!(c_text.text, "c");
1413 assert!(c_load.is_loaded());
1414 assert!(c_deps.is_failed());
1415 assert!(c_rec_deps.is_failed());
1416
1417 assert!(asset_server.load_state(a_id).is_loaded());
1418 assert!(asset_server.dependency_load_state(a_id).is_loaded());
1419 assert!(asset_server
1420 .recursive_dependency_load_state(a_id)
1421 .is_failed());
1422
1423 assert!(asset_server.is_loaded(a_id));
1424 assert!(asset_server.is_loaded_with_direct_dependencies(a_id));
1425 assert!(!asset_server.is_loaded_with_dependencies(a_id));
1426
1427 Some(())
1428 });
1429
1430 assert_eq!(get_started_load_count(app.world()), 4);
1431 }
1432
1433 #[test]
1434 fn dependency_load_states() {
1435 let a_path = "a.cool.ron";
1436 let a_ron = r#"
1437(
1438 text: "a",
1439 dependencies: [
1440 "b.cool.ron",
1441 "c.cool.ron",
1442 ],
1443 embedded_dependencies: [],
1444 sub_texts: []
1445)"#;
1446 let b_path = "b.cool.ron";
1447 let b_ron = r#"
1448(
1449 text: "b",
1450 dependencies: [],
1451 MALFORMED
1452 embedded_dependencies: [],
1453 sub_texts: []
1454)"#;
1455
1456 let c_path = "c.cool.ron";
1457 let c_ron = r#"
1458(
1459 text: "c",
1460 dependencies: [],
1461 embedded_dependencies: [],
1462 sub_texts: []
1463)"#;
1464
1465 let dir = Dir::default();
1466 dir.insert_asset_text(Path::new(a_path), a_ron);
1467 dir.insert_asset_text(Path::new(b_path), b_ron);
1468 dir.insert_asset_text(Path::new(c_path), c_ron);
1469
1470 let (mut app, gate_opener) = create_app_with_gate(dir);
1471 app.init_asset::<CoolText>()
1472 .register_asset_loader(CoolTextLoader);
1473 let asset_server = app.world().resource::<AssetServer>().clone();
1474 let handle: Handle<CoolText> = asset_server.load(a_path);
1475 let a_id = handle.id();
1476
1477 app.update();
1478 assert_eq!(get_started_load_count(app.world()), 1);
1479
1480 gate_opener.open(a_path);
1481 run_app_until(&mut app, |world| {
1482 let _a_text = get::<CoolText>(world, a_id)?;
1483 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1484 assert!(a_load.is_loaded());
1485 assert!(a_deps.is_loading());
1486 assert!(a_rec_deps.is_loading());
1487 Some(())
1488 });
1489
1490 assert_eq!(get_started_load_count(app.world()), 3);
1491
1492 gate_opener.open(b_path);
1493 run_app_until(&mut app, |world| {
1494 let a_text = get::<CoolText>(world, a_id)?;
1495 let b_id = a_text.dependencies[0].id();
1496
1497 let (b_load, _b_deps, _b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1498 if !b_load.is_failed() {
1499 return None;
1501 }
1502
1503 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1504 assert!(a_load.is_loaded());
1505 assert!(a_deps.is_failed());
1506 assert!(a_rec_deps.is_failed());
1507 Some(())
1508 });
1509
1510 assert_eq!(get_started_load_count(app.world()), 3);
1511
1512 gate_opener.open(c_path);
1513 run_app_until(&mut app, |world| {
1514 let a_text = get::<CoolText>(world, a_id)?;
1515 let c_id = a_text.dependencies[1].id();
1516 let _c_text = get::<CoolText>(world, c_id)?;
1518
1519 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1520 assert!(a_load.is_loaded());
1521 assert!(
1522 a_deps.is_failed(),
1523 "Successful dependency load should not overwrite a previous failure"
1524 );
1525 assert!(
1526 a_rec_deps.is_failed(),
1527 "Successful dependency load should not overwrite a previous failure"
1528 );
1529 Some(())
1530 });
1531
1532 assert_eq!(get_started_load_count(app.world()), 3);
1533 }
1534
1535 const SIMPLE_TEXT: &str = r#"
1536(
1537 text: "dep",
1538 dependencies: [],
1539 embedded_dependencies: [],
1540 sub_texts: [],
1541)"#;
1542 #[test]
1543 fn keep_gotten_strong_handles() {
1544 let dir = Dir::default();
1545 dir.insert_asset_text(Path::new("dep.cool.ron"), SIMPLE_TEXT);
1546
1547 let (mut app, _) = create_app_with_gate(dir);
1548 app.init_asset::<CoolText>()
1549 .init_asset::<SubText>()
1550 .init_resource::<StoredEvents>()
1551 .register_asset_loader(CoolTextLoader)
1552 .add_systems(Update, store_asset_events);
1553
1554 let id = {
1555 let handle = {
1556 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1557 let handle = texts.add(CoolText::default());
1558 texts.get_strong_handle(handle.id()).unwrap()
1559 };
1560
1561 app.update();
1562
1563 {
1564 let text = app.world().resource::<Assets<CoolText>>().get(&handle);
1565 assert!(text.is_some());
1566 }
1567 handle.id()
1568 };
1569 app.update();
1571 assert!(
1572 app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1573 "asset has no handles, so it should have been dropped last update"
1574 );
1575 }
1576
1577 #[test]
1578 fn manual_asset_management() {
1579 let dir = Dir::default();
1580 let dep_path = "dep.cool.ron";
1581
1582 dir.insert_asset_text(Path::new(dep_path), SIMPLE_TEXT);
1583
1584 let (mut app, gate_opener) = create_app_with_gate(dir);
1585 app.init_asset::<CoolText>()
1586 .init_asset::<SubText>()
1587 .init_resource::<StoredEvents>()
1588 .register_asset_loader(CoolTextLoader)
1589 .add_systems(Update, store_asset_events);
1590
1591 let hello = "hello".to_string();
1592 let empty = "".to_string();
1593
1594 let id = {
1595 let handle = {
1596 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1597 texts.add(CoolText {
1598 text: hello.clone(),
1599 embedded: empty.clone(),
1600 dependencies: vec![],
1601 sub_texts: Vec::new(),
1602 })
1603 };
1604
1605 app.update();
1606
1607 {
1608 let text = app
1609 .world()
1610 .resource::<Assets<CoolText>>()
1611 .get(&handle)
1612 .unwrap();
1613 assert_eq!(text.text, hello);
1614 }
1615 handle.id()
1616 };
1617 app.update();
1619 assert!(
1620 app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1621 "asset has no handles, so it should have been dropped last update"
1622 );
1623 app.update();
1625 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1626 let expected_events = vec![
1627 AssetEvent::Added { id },
1628 AssetEvent::Unused { id },
1629 AssetEvent::Removed { id },
1630 ];
1631
1632 assert_eq!(get_started_load_count(app.world()), 0);
1634
1635 assert_eq!(events, expected_events);
1636
1637 let dep_handle = app.world().resource::<AssetServer>().load(dep_path);
1638
1639 app.update();
1640 assert_eq!(get_started_load_count(app.world()), 1);
1641
1642 let a = CoolText {
1643 text: "a".to_string(),
1644 embedded: empty,
1645 dependencies: vec![dep_handle.clone()],
1647 sub_texts: Vec::new(),
1648 };
1649 let a_handle = app.world().resource::<AssetServer>().load_asset(a);
1650
1651 assert_eq!(get_started_load_count(app.world()), 1);
1653
1654 app.update();
1655 app.update();
1657
1658 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1659 let expected_events = vec![AssetEvent::Added { id: a_handle.id() }];
1660 assert_eq!(events, expected_events);
1661
1662 gate_opener.open(dep_path);
1663 loop {
1664 app.update();
1665 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1666 if events.is_empty() {
1667 continue;
1668 }
1669 let expected_events = vec![
1670 AssetEvent::LoadedWithDependencies {
1671 id: dep_handle.id(),
1672 },
1673 AssetEvent::LoadedWithDependencies { id: a_handle.id() },
1674 ];
1675 assert_eq!(events, expected_events);
1676 break;
1677 }
1678
1679 assert_eq!(get_started_load_count(app.world()), 1);
1680
1681 app.update();
1682 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1683 let expected_events = vec![AssetEvent::Added {
1684 id: dep_handle.id(),
1685 }];
1686 assert_eq!(events, expected_events);
1687 }
1688
1689 #[test]
1690 fn load_folder() {
1691 let dir = Dir::default();
1692
1693 let a_path = "text/a.cool.ron";
1694 let a_ron = r#"
1695(
1696 text: "a",
1697 dependencies: [
1698 "b.cool.ron",
1699 ],
1700 embedded_dependencies: [],
1701 sub_texts: [],
1702)"#;
1703 let b_path = "b.cool.ron";
1704 let b_ron = r#"
1705(
1706 text: "b",
1707 dependencies: [],
1708 embedded_dependencies: [],
1709 sub_texts: [],
1710)"#;
1711
1712 let c_path = "text/c.cool.ron";
1713 let c_ron = r#"
1714(
1715 text: "c",
1716 dependencies: [
1717 ],
1718 embedded_dependencies: [],
1719 sub_texts: [],
1720)"#;
1721 dir.insert_asset_text(Path::new(a_path), a_ron);
1722 dir.insert_asset_text(Path::new(b_path), b_ron);
1723 dir.insert_asset_text(Path::new(c_path), c_ron);
1724
1725 let (mut app, gate_opener) = create_app_with_gate(dir);
1726 app.init_asset::<CoolText>()
1727 .init_asset::<SubText>()
1728 .register_asset_loader(CoolTextLoader);
1729 let asset_server = app.world().resource::<AssetServer>().clone();
1730 let handle: Handle<LoadedFolder> = asset_server.load_folder("text");
1731
1732 app.update();
1736 let started_load_tasks = get_started_load_count(app.world());
1737 assert!((1..=2).contains(&started_load_tasks));
1738
1739 gate_opener.open(a_path);
1740 gate_opener.open(b_path);
1741 gate_opener.open(c_path);
1742
1743 let mut cursor = MessageCursor::default();
1744 run_app_until(&mut app, |world| {
1745 let events = world.resource::<Messages<AssetEvent<LoadedFolder>>>();
1746 let asset_server = world.resource::<AssetServer>();
1747 let loaded_folders = world.resource::<Assets<LoadedFolder>>();
1748 let cool_texts = world.resource::<Assets<CoolText>>();
1749 for event in cursor.read(events) {
1750 if let AssetEvent::LoadedWithDependencies { id } = event
1751 && *id == handle.id()
1752 {
1753 let loaded_folder = loaded_folders.get(&handle).unwrap();
1754 let a_handle: Handle<CoolText> =
1755 asset_server.get_handle("text/a.cool.ron").unwrap();
1756 let c_handle: Handle<CoolText> =
1757 asset_server.get_handle("text/c.cool.ron").unwrap();
1758
1759 let mut found_a = false;
1760 let mut found_c = false;
1761 for asset_handle in &loaded_folder.handles {
1762 if asset_handle.id() == a_handle.id().untyped() {
1763 found_a = true;
1764 } else if asset_handle.id() == c_handle.id().untyped() {
1765 found_c = true;
1766 }
1767 }
1768 assert!(found_a);
1769 assert!(found_c);
1770 assert_eq!(loaded_folder.handles.len(), 2);
1771
1772 let a_text = cool_texts.get(&a_handle).unwrap();
1773 let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();
1774 let c_text = cool_texts.get(&c_handle).unwrap();
1775
1776 assert_eq!("a", a_text.text);
1777 assert_eq!("b", b_text.text);
1778 assert_eq!("c", c_text.text);
1779
1780 return Some(());
1781 }
1782 }
1783 None
1784 });
1785 assert_eq!(get_started_load_count(app.world()), 4);
1786 }
1787
1788 #[test]
1790 fn load_error_events() {
1791 #[derive(Resource, Default)]
1792 struct ErrorTracker {
1793 tick: u64,
1794 failures: usize,
1795 queued_retries: Vec<(AssetPath<'static>, AssetId<CoolText>, u64)>,
1796 finished_asset: Option<AssetId<CoolText>>,
1797 }
1798
1799 fn asset_event_handler(
1800 mut events: MessageReader<AssetEvent<CoolText>>,
1801 mut tracker: ResMut<ErrorTracker>,
1802 ) {
1803 for event in events.read() {
1804 if let AssetEvent::LoadedWithDependencies { id } = event {
1805 tracker.finished_asset = Some(*id);
1806 }
1807 }
1808 }
1809
1810 fn asset_load_error_event_handler(
1811 server: Res<AssetServer>,
1812 mut errors: MessageReader<AssetLoadFailedEvent<CoolText>>,
1813 mut tracker: ResMut<ErrorTracker>,
1814 ) {
1815 tracker.tick += 1;
1817
1818 let now = tracker.tick;
1820 tracker
1821 .queued_retries
1822 .retain(|(path, old_id, retry_after)| {
1823 if now > *retry_after {
1824 let new_handle = server.load::<CoolText>(path);
1825 assert_eq!(&new_handle.id(), old_id);
1826 false
1827 } else {
1828 true
1829 }
1830 });
1831
1832 for error in errors.read() {
1834 let (load_state, _, _) = server.get_load_states(error.id).unwrap();
1835 assert!(load_state.is_failed());
1836 assert_eq!(*error.path.source(), AssetSourceId::Name("unstable".into()));
1837 match &error.error {
1838 AssetLoadError::AssetReaderError(read_error) => match read_error {
1839 AssetReaderError::Io(_) => {
1840 tracker.failures += 1;
1841 if tracker.failures <= 2 {
1842 tracker.queued_retries.push((
1844 error.path.clone(),
1845 error.id,
1846 now + 10,
1847 ));
1848 } else {
1849 panic!(
1850 "Unexpected failure #{} (expected only 2)",
1851 tracker.failures
1852 );
1853 }
1854 }
1855 _ => panic!("Unexpected error type {}", read_error),
1856 },
1857 _ => panic!("Unexpected error type {}", error.error),
1858 }
1859 }
1860 }
1861
1862 let a_path = "text/a.cool.ron";
1863 let a_ron = r#"
1864(
1865 text: "a",
1866 dependencies: [],
1867 embedded_dependencies: [],
1868 sub_texts: [],
1869)"#;
1870
1871 let dir = Dir::default();
1872 dir.insert_asset_text(Path::new(a_path), a_ron);
1873 let unstable_reader = UnstableMemoryAssetReader::new(dir, 2);
1874
1875 let mut app = App::new();
1876 app.register_asset_source(
1877 "unstable",
1878 AssetSourceBuilder::new(move || Box::new(unstable_reader.clone())),
1879 )
1880 .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
1881 .init_asset::<CoolText>()
1882 .register_asset_loader(CoolTextLoader)
1883 .init_resource::<ErrorTracker>()
1884 .add_systems(
1885 Update,
1886 (asset_event_handler, asset_load_error_event_handler).chain(),
1887 );
1888
1889 let asset_server = app.world().resource::<AssetServer>().clone();
1890 let a_path = format!("unstable://{a_path}");
1891 let a_handle: Handle<CoolText> = asset_server.load(a_path);
1892 let a_id = a_handle.id();
1893
1894 run_app_until(&mut app, |world| {
1895 let tracker = world.resource::<ErrorTracker>();
1896 match tracker.finished_asset {
1897 Some(asset_id) => {
1898 assert_eq!(asset_id, a_id);
1899 let assets = world.resource::<Assets<CoolText>>();
1900 let result = assets.get(asset_id).unwrap();
1901 assert_eq!(result.text, "a");
1902 Some(())
1903 }
1904 None => None,
1905 }
1906 });
1907 }
1908
1909 #[test]
1910 fn ignore_system_ambiguities_on_assets() {
1911 let mut app = create_app().0;
1912 app.init_asset::<CoolText>();
1913
1914 fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1915 app.add_systems(Update, (uses_assets, uses_assets));
1916 app.edit_schedule(Update, |s| {
1917 s.set_build_settings(ScheduleBuildSettings {
1918 ambiguity_detection: LogLevel::Error,
1919 ..Default::default()
1920 });
1921 });
1922
1923 app.world_mut().run_schedule(Update);
1925 }
1926
1927 #[test]
1930 fn error_on_nested_immediate_load_of_subasset() {
1931 let (mut app, dir) = create_app();
1932 dir.insert_asset_text(
1933 Path::new("a.cool.ron"),
1934 r#"(
1935 text: "b",
1936 dependencies: [],
1937 embedded_dependencies: [],
1938 sub_texts: ["A"],
1939)"#,
1940 );
1941 dir.insert_asset_text(Path::new("empty.txt"), "");
1942
1943 app.init_asset::<CoolText>()
1944 .init_asset::<SubText>()
1945 .register_asset_loader(CoolTextLoader);
1946
1947 #[derive(TypePath)]
1948 struct NestedLoadOfSubassetLoader;
1949
1950 impl AssetLoader for NestedLoadOfSubassetLoader {
1951 type Asset = TestAsset;
1952 type Error = crate::loader::LoadDirectError;
1953 type Settings = ();
1954
1955 async fn load(
1956 &self,
1957 _: &mut dyn Reader,
1958 _: &Self::Settings,
1959 load_context: &mut LoadContext<'_>,
1960 ) -> Result<Self::Asset, Self::Error> {
1961 load_context
1963 .loader()
1964 .immediate()
1965 .load::<SubText>("a.cool.ron#A")
1966 .await?;
1967 Ok(TestAsset)
1968 }
1969
1970 fn extensions(&self) -> &[&str] {
1971 &["txt"]
1972 }
1973 }
1974
1975 app.init_asset::<TestAsset>()
1976 .register_asset_loader(NestedLoadOfSubassetLoader);
1977
1978 let asset_server = app.world().resource::<AssetServer>().clone();
1979 let handle = asset_server.load::<TestAsset>("empty.txt");
1980
1981 run_app_until(&mut app, |_world| match asset_server.load_state(&handle) {
1982 LoadState::Loading => None,
1983 LoadState::Failed(err) => {
1984 let error_message = format!("{err}");
1985 assert!(error_message.contains("Requested to load an asset path (a.cool.ron#A) with a subasset, but this is unsupported"), "what? \"{error_message}\"");
1986 Some(())
1987 }
1988 state => panic!("Unexpected asset state: {state:?}"),
1989 });
1990 }
1991
1992 #[derive(Asset, TypePath)]
1994 pub struct TestAsset;
1995
1996 #[derive(Asset, TypePath)]
1997 #[expect(
1998 dead_code,
1999 reason = "This exists to ensure that `#[derive(Asset)]` works on enums. The inner variants are known not to be used."
2000 )]
2001 pub enum EnumTestAsset {
2002 Unnamed(#[dependency] Handle<TestAsset>),
2003 Named {
2004 #[dependency]
2005 handle: Handle<TestAsset>,
2006 #[dependency]
2007 vec_handles: Vec<Handle<TestAsset>>,
2008 #[dependency]
2009 embedded: TestAsset,
2010 #[dependency]
2011 set_handles: HashSet<Handle<TestAsset>>,
2012 #[dependency]
2013 untyped_set_handles: HashSet<UntypedHandle>,
2014 },
2015 StructStyle(#[dependency] TestAsset),
2016 Empty,
2017 }
2018
2019 #[expect(
2020 dead_code,
2021 reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed."
2022 )]
2023 #[derive(Asset, TypePath)]
2024 pub struct StructTestAsset {
2025 #[dependency]
2026 handle: Handle<TestAsset>,
2027 #[dependency]
2028 embedded: TestAsset,
2029 #[dependency]
2030 array_handles: [Handle<TestAsset>; 5],
2031 #[dependency]
2032 untyped_array_handles: [UntypedHandle; 5],
2033 #[dependency]
2034 set_handles: HashSet<Handle<TestAsset>>,
2035 #[dependency]
2036 untyped_set_handles: HashSet<UntypedHandle>,
2037 }
2038
2039 #[expect(
2040 dead_code,
2041 reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed."
2042 )]
2043 #[derive(Asset, TypePath)]
2044 pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);
2045
2046 fn unapproved_path_setup(mode: UnapprovedPathMode) -> App {
2047 let dir = Dir::default();
2048 let a_path = "../a.cool.ron";
2049 let a_ron = r#"
2050(
2051 text: "a",
2052 dependencies: [],
2053 embedded_dependencies: [],
2054 sub_texts: [],
2055)"#;
2056
2057 dir.insert_asset_text(Path::new(a_path), a_ron);
2058
2059 let mut app = App::new();
2060 let memory_reader = MemoryAssetReader { root: dir };
2061 app.register_asset_source(
2062 AssetSourceId::Default,
2063 AssetSourceBuilder::new(move || Box::new(memory_reader.clone())),
2064 )
2065 .add_plugins((
2066 TaskPoolPlugin::default(),
2067 AssetPlugin {
2068 unapproved_path_mode: mode,
2069 ..Default::default()
2070 },
2071 ));
2072 app.init_asset::<CoolText>();
2073
2074 app
2075 }
2076
2077 fn load_a_asset(assets: Res<AssetServer>) {
2078 let a = assets.load::<CoolText>("../a.cool.ron");
2079 if a == Handle::default() {
2080 panic!()
2081 }
2082 }
2083
2084 fn load_a_asset_override(assets: Res<AssetServer>) {
2085 let a = assets.load_override::<CoolText>("../a.cool.ron");
2086 if a == Handle::default() {
2087 panic!()
2088 }
2089 }
2090
2091 #[test]
2092 #[should_panic]
2093 fn unapproved_path_forbid_should_panic() {
2094 let mut app = unapproved_path_setup(UnapprovedPathMode::Forbid);
2095
2096 fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
2097 app.add_systems(Update, (uses_assets, load_a_asset_override));
2098
2099 app.world_mut().run_schedule(Update);
2100 }
2101
2102 #[test]
2103 #[should_panic]
2104 fn unapproved_path_deny_should_panic() {
2105 let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);
2106
2107 fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
2108 app.add_systems(Update, (uses_assets, load_a_asset));
2109
2110 app.world_mut().run_schedule(Update);
2111 }
2112
2113 #[test]
2114 fn unapproved_path_deny_should_finish() {
2115 let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);
2116
2117 fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
2118 app.add_systems(Update, (uses_assets, load_a_asset_override));
2119
2120 app.world_mut().run_schedule(Update);
2121 }
2122
2123 #[test]
2124 fn unapproved_path_allow_should_finish() {
2125 let mut app = unapproved_path_setup(UnapprovedPathMode::Allow);
2126
2127 fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
2128 app.add_systems(Update, (uses_assets, load_a_asset));
2129
2130 app.world_mut().run_schedule(Update);
2131 }
2132
2133 #[test]
2134 fn insert_dropped_handle_returns_error() {
2135 let mut app = create_app().0;
2136
2137 app.init_asset::<TestAsset>();
2138
2139 let handle = app.world().resource::<Assets<TestAsset>>().reserve_handle();
2140 let asset_id = handle.id();
2142 drop(handle);
2143
2144 app.world_mut()
2146 .run_system_cached(Assets::<TestAsset>::track_assets)
2147 .unwrap();
2148
2149 let AssetId::Index { index, .. } = asset_id else {
2150 unreachable!("Reserving a handle always produces an index");
2151 };
2152
2153 assert_eq!(
2155 app.world_mut()
2156 .resource_mut::<Assets<TestAsset>>()
2157 .insert(asset_id, TestAsset),
2158 Err(InvalidGenerationError::Removed { index })
2159 );
2160 }
2161
2162 #[derive(TypePath)]
2168 struct GatedLoader {
2169 in_loader_sender: Sender<()>,
2170 gate_receiver: Receiver<()>,
2171 }
2172
2173 impl AssetLoader for GatedLoader {
2174 type Asset = TestAsset;
2175 type Error = std::io::Error;
2176 type Settings = ();
2177
2178 async fn load(
2179 &self,
2180 _reader: &mut dyn Reader,
2181 _settings: &Self::Settings,
2182 _load_context: &mut LoadContext<'_>,
2183 ) -> Result<Self::Asset, Self::Error> {
2184 self.in_loader_sender.send_blocking(()).unwrap();
2185 let _ = self.gate_receiver.recv().await;
2186 Ok(TestAsset)
2187 }
2188
2189 fn extensions(&self) -> &[&str] {
2190 &["ron"]
2191 }
2192 }
2193
2194 #[test]
2195 fn dropping_handle_while_loading_cancels_load() {
2196 let (mut app, dir) = create_app();
2197
2198 let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2199 let (gate_sender, gate_receiver) = async_channel::bounded(1);
2200
2201 app.init_asset::<TestAsset>()
2202 .register_asset_loader(GatedLoader {
2203 in_loader_sender,
2204 gate_receiver,
2205 });
2206
2207 let path = Path::new("abc.ron");
2208 dir.insert_asset_text(path, "blah");
2209
2210 let asset_server = app.world().resource::<AssetServer>().clone();
2211
2212 let handle = asset_server.load::<TestAsset>(path);
2214 assert!(asset_server.get_load_state(&handle).unwrap().is_loading());
2215 app.update();
2216
2217 in_loader_receiver.recv_blocking().unwrap();
2219
2220 let asset_id = handle.id();
2221 drop(handle);
2223 app.update();
2224 assert!(asset_server.get_load_state(asset_id).is_none());
2225
2226 gate_sender.send_blocking(()).unwrap();
2228 for _ in 0..10 {
2229 app.update();
2230 for message in app
2231 .world()
2232 .resource::<Messages<AssetEvent<TestAsset>>>()
2233 .iter_current_update_messages()
2234 {
2235 match message {
2236 AssetEvent::Unused { .. } => {}
2237 message => panic!("No asset events are allowed: {message:?}"),
2238 }
2239 }
2240 }
2241 }
2242
2243 #[test]
2244 fn dropping_subasset_handle_while_loading_cancels_load() {
2245 let (mut app, dir) = create_app();
2246
2247 let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2248 let (gate_sender, gate_receiver) = async_channel::bounded(1);
2249
2250 app.init_asset::<TestAsset>()
2251 .register_asset_loader(GatedLoader {
2252 in_loader_sender,
2253 gate_receiver,
2254 });
2255
2256 let path = Path::new("abc.ron");
2257 dir.insert_asset_text(path, "blah");
2258
2259 let asset_server = app.world().resource::<AssetServer>().clone();
2260
2261 let handle = asset_server.load::<TestAsset>("abc.ron#sub");
2265 assert!(asset_server.get_load_state(&handle).unwrap().is_loading());
2266 app.update();
2267
2268 in_loader_receiver.recv_blocking().unwrap();
2270
2271 let asset_id = handle.id();
2272 drop(handle);
2274 app.update();
2275 assert!(asset_server.get_load_state(asset_id).is_none());
2276
2277 gate_sender.send_blocking(()).unwrap();
2279 for _ in 0..10 {
2280 app.update();
2281 for message in app
2282 .world()
2283 .resource::<Messages<AssetEvent<TestAsset>>>()
2284 .iter_current_update_messages()
2285 {
2286 match message {
2287 AssetEvent::Unused { .. } => {}
2288 message => panic!("No asset events are allowed: {message:?}"),
2289 }
2290 }
2291 }
2292 }
2293
2294 fn create_app_with_source_event_sender() -> (App, Dir, Sender<AssetSourceEvent>) {
2297 let mut app = App::new();
2298 let dir = Dir::default();
2299 let memory_reader = MemoryAssetReader { root: dir.clone() };
2300
2301 let (sender_sender, sender_receiver) = crossbeam_channel::bounded(1);
2303
2304 struct FakeWatcher;
2305 impl AssetWatcher for FakeWatcher {}
2306
2307 app.register_asset_source(
2308 AssetSourceId::Default,
2309 AssetSourceBuilder::new(move || Box::new(memory_reader.clone())).with_watcher(
2310 move |sender| {
2311 sender_sender.send(sender).unwrap();
2312 Some(Box::new(FakeWatcher))
2313 },
2314 ),
2315 )
2316 .add_plugins((
2317 TaskPoolPlugin::default(),
2318 AssetPlugin {
2319 watch_for_changes_override: Some(true),
2320 ..Default::default()
2321 },
2322 ));
2323
2324 let sender = sender_receiver.try_recv().unwrap();
2325
2326 (app, dir, sender)
2327 }
2328
2329 fn collect_asset_events<A: Asset>(world: &mut World) -> Vec<AssetEvent<A>> {
2330 world
2331 .resource_mut::<Messages<AssetEvent<A>>>()
2332 .drain()
2333 .collect()
2334 }
2335
2336 fn collect_asset_load_failed_events<A: Asset>(
2337 world: &mut World,
2338 ) -> Vec<AssetLoadFailedEvent<A>> {
2339 world
2340 .resource_mut::<Messages<AssetLoadFailedEvent<A>>>()
2341 .drain()
2342 .collect()
2343 }
2344
2345 #[test]
2346 fn reloads_asset_after_source_event() {
2347 let (mut app, dir, source_events) = create_app_with_source_event_sender();
2348 let asset_server = app.world().resource::<AssetServer>().clone();
2349
2350 dir.insert_asset_text(
2351 Path::new("abc.cool.ron"),
2352 r#"(
2353 text: "a",
2354 dependencies: [],
2355 embedded_dependencies: [],
2356 sub_texts: [],
2357)"#,
2358 );
2359
2360 app.init_asset::<CoolText>()
2361 .init_asset::<SubText>()
2362 .register_asset_loader(CoolTextLoader);
2363
2364 let handle: Handle<CoolText> = asset_server.load("abc.cool.ron");
2365 run_app_until(&mut app, |world| {
2366 let messages = collect_asset_events(world);
2367 if messages.is_empty() {
2368 return None;
2369 }
2370 assert_eq!(
2371 messages,
2372 [
2373 AssetEvent::LoadedWithDependencies { id: handle.id() },
2374 AssetEvent::Added { id: handle.id() },
2375 ]
2376 );
2377 Some(())
2378 });
2379
2380 source_events
2383 .send_blocking(AssetSourceEvent::ModifiedAsset(PathBuf::from(
2384 "abc.cool.ron",
2385 )))
2386 .unwrap();
2387
2388 run_app_until(&mut app, |world| {
2389 let messages = collect_asset_events(world);
2390 if messages.is_empty() {
2391 return None;
2392 }
2393 assert_eq!(
2394 messages,
2395 [
2396 AssetEvent::LoadedWithDependencies { id: handle.id() },
2397 AssetEvent::Modified { id: handle.id() }
2398 ]
2399 );
2400 Some(())
2401 });
2402 }
2403
2404 #[test]
2405 fn added_asset_reloads_previously_missing_asset() {
2406 let (mut app, dir, source_events) = create_app_with_source_event_sender();
2407 let asset_server = app.world().resource::<AssetServer>().clone();
2408
2409 app.init_asset::<CoolText>()
2410 .init_asset::<SubText>()
2411 .register_asset_loader(CoolTextLoader);
2412
2413 let handle: Handle<CoolText> = asset_server.load("abc.cool.ron");
2414 run_app_until(&mut app, |world| {
2415 let failed_ids = collect_asset_load_failed_events(world)
2416 .drain(..)
2417 .map(|event| event.id)
2418 .collect::<Vec<_>>();
2419 if failed_ids.is_empty() {
2420 return None;
2421 }
2422 assert_eq!(failed_ids, [handle.id()]);
2423 Some(())
2424 });
2425
2426 dir.insert_asset_text(
2429 Path::new("abc.cool.ron"),
2430 r#"(
2431 text: "a",
2432 dependencies: [],
2433 embedded_dependencies: [],
2434 sub_texts: [],
2435)"#,
2436 );
2437 source_events
2438 .send_blocking(AssetSourceEvent::AddedAsset(PathBuf::from("abc.cool.ron")))
2439 .unwrap();
2440
2441 run_app_until(&mut app, |world| {
2442 let messages = collect_asset_events(world);
2443 if messages.is_empty() {
2444 return None;
2445 }
2446 assert_eq!(
2447 messages,
2448 [
2449 AssetEvent::LoadedWithDependencies { id: handle.id() },
2450 AssetEvent::Added { id: handle.id() }
2451 ]
2452 );
2453 Some(())
2454 });
2455 }
2456
2457 #[test]
2458 fn same_asset_different_settings() {
2459 #[derive(Asset, TypePath)]
2466 struct U8Asset(u8);
2467
2468 #[derive(Serialize, Deserialize, Default)]
2469 struct U8LoaderSettings(u8);
2470
2471 #[derive(TypePath)]
2472 struct U8Loader;
2473
2474 impl AssetLoader for U8Loader {
2475 type Asset = U8Asset;
2476 type Settings = U8LoaderSettings;
2477 type Error = crate::loader::LoadDirectError;
2478
2479 async fn load(
2480 &self,
2481 _: &mut dyn Reader,
2482 settings: &Self::Settings,
2483 _: &mut LoadContext<'_>,
2484 ) -> Result<Self::Asset, Self::Error> {
2485 Ok(U8Asset(settings.0))
2486 }
2487
2488 fn extensions(&self) -> &[&str] {
2489 &["u8"]
2490 }
2491 }
2492
2493 let (mut app, dir) = create_app();
2496 dir.insert_asset(Path::new("test.u8"), &[]);
2497
2498 app.init_asset::<U8Asset>().register_asset_loader(U8Loader);
2499
2500 let asset_server = app.world().resource::<AssetServer>();
2501
2502 fn load(asset_server: &AssetServer, path: &'static str, value: u8) -> Handle<U8Asset> {
2505 asset_server.load_with_settings::<U8Asset, U8LoaderSettings>(
2506 path,
2507 move |s: &mut U8LoaderSettings| s.0 = value,
2508 )
2509 }
2510
2511 let handle_1 = load(asset_server, "test.u8", 1);
2512 let handle_2 = load(asset_server, "test.u8", 2);
2513
2514 assert_eq!(handle_1, handle_2);
2522
2523 run_app_until(&mut app, |world| {
2524 let (Some(asset_1), Some(asset_2)) = (
2525 world.resource::<Assets<U8Asset>>().get(&handle_1),
2526 world.resource::<Assets<U8Asset>>().get(&handle_2),
2527 ) else {
2528 return None;
2529 };
2530
2531 assert_eq!(asset_1.0, asset_2.0);
2540
2541 Some(())
2542 });
2543 }
2544
2545 #[test]
2546 fn loading_two_subassets_does_not_start_two_loads() {
2547 let (mut app, dir) = create_app();
2548 dir.insert_asset(Path::new("test.txt"), &[]);
2549
2550 #[derive(TypePath)]
2551 struct TwoSubassetLoader;
2552
2553 impl AssetLoader for TwoSubassetLoader {
2554 type Asset = TestAsset;
2555 type Settings = ();
2556 type Error = std::io::Error;
2557
2558 async fn load(
2559 &self,
2560 _reader: &mut dyn Reader,
2561 _settings: &Self::Settings,
2562 load_context: &mut LoadContext<'_>,
2563 ) -> Result<Self::Asset, Self::Error> {
2564 load_context.add_labeled_asset("A".into(), TestAsset);
2565 load_context.add_labeled_asset("B".into(), TestAsset);
2566 Ok(TestAsset)
2567 }
2568
2569 fn extensions(&self) -> &[&str] {
2570 &["txt"]
2571 }
2572 }
2573
2574 app.init_asset::<TestAsset>()
2575 .register_asset_loader(TwoSubassetLoader);
2576
2577 let asset_server = app.world().resource::<AssetServer>().clone();
2578 let _subasset_1: Handle<TestAsset> = asset_server.load("test.txt#A");
2579 let _subasset_2: Handle<TestAsset> = asset_server.load("test.txt#B");
2580
2581 app.update();
2582
2583 assert_eq!(get_started_load_count(app.world()), 2);
2588 }
2589
2590 #[derive(TypePath)]
2592 struct TrivialLoader;
2593
2594 impl AssetLoader for TrivialLoader {
2595 type Asset = TestAsset;
2596 type Settings = ();
2597 type Error = std::io::Error;
2598
2599 async fn load(
2600 &self,
2601 _reader: &mut dyn Reader,
2602 _settings: &Self::Settings,
2603 _load_context: &mut LoadContext<'_>,
2604 ) -> Result<Self::Asset, Self::Error> {
2605 Ok(TestAsset)
2606 }
2607
2608 fn extensions(&self) -> &[&str] {
2609 &["txt"]
2610 }
2611 }
2612
2613 #[test]
2614 fn get_strong_handle_prevents_reload_when_asset_still_alive() {
2615 let (mut app, dir) = create_app();
2616 dir.insert_asset(Path::new("test.txt"), &[]);
2617
2618 app.init_asset::<TestAsset>()
2619 .register_asset_loader(TrivialLoader);
2620
2621 let asset_server = app.world().resource::<AssetServer>().clone();
2622 let original_handle: Handle<TestAsset> = asset_server.load("test.txt");
2623
2624 run_app_until(&mut app, |world| {
2626 world
2627 .resource::<Assets<TestAsset>>()
2628 .get(&original_handle)
2629 .map(|_| ())
2630 });
2631
2632 assert_eq!(get_started_load_count(app.world()), 1);
2633
2634 let new_handle = app
2636 .world_mut()
2637 .resource_mut::<Assets<TestAsset>>()
2638 .get_strong_handle(original_handle.id())
2639 .unwrap();
2640
2641 drop(original_handle);
2643
2644 app.update();
2645 assert!(app
2646 .world()
2647 .resource::<Assets<TestAsset>>()
2648 .get(&new_handle)
2649 .is_some());
2650
2651 let _other_handle: Handle<TestAsset> = asset_server.load("test.txt");
2652 app.update();
2653 assert_eq!(get_started_load_count(app.world()), 2);
2660 }
2661
2662 #[test]
2663 fn immediate_nested_asset_loads_dependency() {
2664 let (mut app, dir) = create_app();
2665
2666 #[derive(Asset, TypePath)]
2668 struct DeferredNested(Handle<TestAsset>);
2669
2670 #[derive(TypePath)]
2671 struct DeferredNestedLoader;
2672
2673 impl AssetLoader for DeferredNestedLoader {
2674 type Asset = DeferredNested;
2675 type Settings = ();
2676 type Error = std::io::Error;
2677
2678 async fn load(
2679 &self,
2680 reader: &mut dyn Reader,
2681 _: &Self::Settings,
2682 load_context: &mut LoadContext<'_>,
2683 ) -> Result<Self::Asset, Self::Error> {
2684 let mut nested_path = String::new();
2685 reader.read_to_string(&mut nested_path).await?;
2686 Ok(DeferredNested(load_context.load(nested_path)))
2687 }
2688
2689 fn extensions(&self) -> &[&str] {
2690 &["defer"]
2691 }
2692 }
2693
2694 #[derive(Asset, TypePath)]
2696 struct ImmediateNested(Handle<TestAsset>);
2697
2698 #[derive(TypePath)]
2699 struct ImmediateNestedLoader;
2700
2701 impl AssetLoader for ImmediateNestedLoader {
2702 type Asset = ImmediateNested;
2703 type Settings = ();
2704 type Error = std::io::Error;
2705
2706 async fn load(
2707 &self,
2708 reader: &mut dyn Reader,
2709 _: &Self::Settings,
2710 load_context: &mut LoadContext<'_>,
2711 ) -> Result<Self::Asset, Self::Error> {
2712 let mut nested_path = String::new();
2713 reader.read_to_string(&mut nested_path).await?;
2714 let deferred_nested: LoadedAsset<DeferredNested> = load_context
2715 .loader()
2716 .immediate()
2717 .load(nested_path)
2718 .await
2719 .unwrap();
2720 Ok(ImmediateNested(deferred_nested.get().0.clone()))
2721 }
2722
2723 fn extensions(&self) -> &[&str] {
2724 &["immediate"]
2725 }
2726 }
2727
2728 app.init_asset::<TestAsset>()
2729 .init_asset::<DeferredNested>()
2730 .init_asset::<ImmediateNested>()
2731 .register_asset_loader(TrivialLoader)
2732 .register_asset_loader(DeferredNestedLoader)
2733 .register_asset_loader(ImmediateNestedLoader);
2734
2735 dir.insert_asset_text(Path::new("a.immediate"), "b.defer");
2736 dir.insert_asset_text(Path::new("b.defer"), "c.txt");
2737 dir.insert_asset_text(Path::new("c.txt"), "hiya");
2738
2739 let server = app.world().resource::<AssetServer>().clone();
2740 let immediate_handle: Handle<ImmediateNested> = server.load("a.immediate");
2741
2742 run_app_until(&mut app, |world| {
2743 let immediate_assets = world.resource::<Assets<ImmediateNested>>();
2744 let immediate = immediate_assets.get(&immediate_handle)?;
2745
2746 let test_asset_handle = immediate.0.clone();
2747 world
2748 .resource::<Assets<TestAsset>>()
2749 .get(&test_asset_handle)?;
2750
2751 Some(())
2754 });
2755 }
2756
2757 #[expect(dead_code, reason = "used by tests not backported to 0.18")]
2758 pub(crate) fn read_asset_as_string(dir: &Dir, path: &Path) -> String {
2759 let bytes = dir.get_asset(path).unwrap();
2760 str::from_utf8(bytes.value()).unwrap().to_string()
2761 }
2762
2763 pub(crate) fn read_meta_as_string(dir: &Dir, path: &Path) -> String {
2764 let bytes = dir.get_metadata(path).unwrap();
2765 str::from_utf8(bytes.value()).unwrap().to_string()
2766 }
2767
2768 #[test]
2769 fn write_default_meta_does_not_overwrite() {
2770 let (mut app, source) = create_app();
2771
2772 app.register_asset_loader(CoolTextLoader);
2773
2774 const ASSET_PATH: &str = "abc.cool.ron";
2775 source.insert_asset_text(Path::new(ASSET_PATH), "blah");
2776 const META_TEXT: &str = "hey i'm walkin here!";
2777 source.insert_meta_text(Path::new(ASSET_PATH), META_TEXT);
2778
2779 let asset_server = app.world().resource::<AssetServer>().clone();
2780 assert!(matches!(
2781 bevy_tasks::block_on(asset_server.write_default_loader_meta_file_for_path(ASSET_PATH)),
2782 Err(WriteDefaultMetaError::MetaAlreadyExists)
2783 ));
2784
2785 assert_eq!(
2786 read_meta_as_string(&source, Path::new(ASSET_PATH)),
2787 META_TEXT
2788 );
2789 }
2790
2791 #[test]
2792 fn asset_dependency_is_tracked_when_not_loaded() {
2793 let (mut app, dir) = create_app();
2794
2795 #[derive(Asset, TypePath)]
2796 struct AssetWithDep {
2797 #[dependency]
2798 dep: Handle<TestAsset>,
2799 }
2800
2801 #[derive(TypePath)]
2802 struct AssetWithDepLoader;
2803
2804 impl AssetLoader for AssetWithDepLoader {
2805 type Asset = TestAsset;
2806 type Settings = ();
2807 type Error = std::io::Error;
2808
2809 async fn load(
2810 &self,
2811 _reader: &mut dyn Reader,
2812 _settings: &Self::Settings,
2813 load_context: &mut LoadContext<'_>,
2814 ) -> Result<Self::Asset, Self::Error> {
2815 let dep = load_context.load::<TestAsset>("abc.ron");
2818 load_context.add_labeled_asset("subasset".into(), AssetWithDep { dep });
2819 Ok(TestAsset)
2820 }
2821
2822 fn extensions(&self) -> &[&str] {
2823 &["with_deps"]
2824 }
2825 }
2826
2827 dir.insert_asset_text(Path::new("abc.ron"), "");
2830 dir.insert_asset_text(Path::new("blah.with_deps"), "");
2831
2832 let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2833 let (gate_sender, gate_receiver) = async_channel::bounded(1);
2834 app.init_asset::<TestAsset>()
2835 .init_asset::<AssetWithDep>()
2836 .register_asset_loader(GatedLoader {
2837 in_loader_sender,
2838 gate_receiver,
2839 })
2840 .register_asset_loader(AssetWithDepLoader);
2841
2842 let asset_server = app.world().resource::<AssetServer>().clone();
2843 let subasset_handle: Handle<AssetWithDep> = asset_server.load("blah.with_deps#subasset");
2844
2845 run_app_until(&mut app, |_| {
2846 asset_server.is_loaded(&subasset_handle).then_some(())
2847 });
2848 assert!(!asset_server.is_loaded_with_dependencies(&subasset_handle));
2851
2852 let dep_handle: Handle<TestAsset> = app
2853 .world()
2854 .resource::<Assets<AssetWithDep>>()
2855 .get(&subasset_handle)
2856 .unwrap()
2857 .dep
2858 .clone();
2859
2860 in_loader_receiver.recv_blocking().unwrap();
2862 gate_sender.send_blocking(()).unwrap();
2863
2864 run_app_until(&mut app, |_| {
2865 asset_server.is_loaded(&dep_handle).then_some(())
2866 });
2867 assert!(asset_server.is_loaded_with_dependencies(&subasset_handle));
2869 }
2870}