1#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
140#![cfg_attr(docsrs, feature(doc_cfg))]
141#![doc(
142 html_logo_url = "https://bevy.org/assets/icon.png",
143 html_favicon_url = "https://bevy.org/assets/icon.png"
144)]
145#![no_std]
146
147extern crate alloc;
148extern crate std;
149
150extern crate self as bevy_asset;
152
153pub mod io;
154pub mod meta;
155pub mod processor;
156pub mod saver;
157pub mod transformer;
158
159pub mod prelude {
163 #[doc(hidden)]
164 pub use crate::asset_changed::AssetChanged;
165
166 #[doc(hidden)]
167 pub use crate::{
168 asset_value, Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer,
169 Assets, DirectAssetAccessExt, Handle, UntypedHandle,
170 };
171}
172
173mod asset_changed;
174mod assets;
175mod direct_access_ext;
176mod event;
177mod folder;
178mod handle;
179mod id;
180mod loader;
181mod loader_builders;
182mod path;
183mod reflect;
184mod render_asset;
185mod server;
186
187pub use assets::*;
188pub use bevy_asset_macros::{Asset, VisitAssetDependencies};
189use bevy_diagnostic::{Diagnostic, DiagnosticsStore, RegisterDiagnostic};
190pub use direct_access_ext::DirectAssetAccessExt;
191pub use event::*;
192pub use folder::*;
193pub use futures_lite::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
194pub use handle::*;
195pub use id::*;
196pub use loader::*;
197pub use loader_builders::NestedLoadBuilder;
198pub use path::*;
199pub use reflect::*;
200pub use render_asset::*;
201pub use server::*;
202
203pub use uuid;
204
205use crate::{
206 io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId},
207 processor::{AssetProcessor, Process},
208};
209use alloc::{
210 string::{String, ToString},
211 sync::Arc,
212 vec::Vec,
213};
214use bevy_app::{App, Plugin, PostUpdate, PreUpdate};
215use bevy_ecs::{prelude::Component, schedule::common_conditions::resource_exists};
216use bevy_ecs::{
217 reflect::AppTypeRegistry,
218 schedule::{IntoScheduleConfigs, SystemSet},
219 world::FromWorld,
220};
221use bevy_platform::collections::{HashMap, HashSet};
222use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath};
223use core::any::TypeId;
224use tracing::error;
225
226pub struct AssetPlugin {
234 pub file_path: String,
236 pub processed_file_path: String,
238 pub watch_for_changes_override: Option<bool>,
245 pub use_asset_processor_override: Option<bool>,
251 pub mode: AssetMode,
253 pub meta_check: AssetMetaCheck,
255 pub unapproved_path_mode: UnapprovedPathMode,
260}
261
262#[derive(Clone, Default)]
275pub enum UnapprovedPathMode {
276 Allow,
278 Deny,
281 #[default]
283 Forbid,
284}
285
286#[derive(Debug)]
293pub enum AssetMode {
294 Unprocessed,
299 Processed,
314}
315
316#[derive(Debug, Default, Clone)]
319pub enum AssetMetaCheck {
320 #[default]
322 Always,
323 Paths(HashSet<AssetPath<'static>>),
325 Never,
327}
328
329impl Default for AssetPlugin {
330 fn default() -> Self {
331 Self {
332 mode: AssetMode::Unprocessed,
333 file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(),
334 processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
335 watch_for_changes_override: None,
336 use_asset_processor_override: None,
337 meta_check: AssetMetaCheck::default(),
338 unapproved_path_mode: UnapprovedPathMode::default(),
339 }
340 }
341}
342
343impl AssetPlugin {
344 const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets";
345 const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default";
348}
349
350impl Plugin for AssetPlugin {
351 fn build(&self, app: &mut App) {
352 let embedded = EmbeddedAssetRegistry::default();
353 {
354 let mut sources = app
355 .world_mut()
356 .get_resource_or_init::<AssetSourceBuilders>();
357 sources.init_default_source(
358 &self.file_path,
359 (!matches!(self.mode, AssetMode::Unprocessed))
360 .then_some(self.processed_file_path.as_str()),
361 );
362 embedded.register_source(&mut sources);
363 }
364 {
365 let watch = self
366 .watch_for_changes_override
367 .unwrap_or(cfg!(feature = "watch"));
368 match self.mode {
369 AssetMode::Unprocessed => {
370 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
371 let sources = builders.build_sources(watch, false);
372
373 app.insert_resource(AssetServer::new_with_meta_check(
374 Arc::new(sources),
375 AssetServerMode::Unprocessed,
376 self.meta_check.clone(),
377 watch,
378 self.unapproved_path_mode.clone(),
379 ));
380 }
381 AssetMode::Processed => {
382 let use_asset_processor = self
383 .use_asset_processor_override
384 .unwrap_or(cfg!(feature = "asset_processor"));
385 if use_asset_processor {
386 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
387 let (processor, sources) = AssetProcessor::new(&mut builders, watch);
388 app.insert_resource(AssetServer::new_with_loaders(
390 sources,
391 processor.server().data.loaders.clone(),
392 AssetServerMode::Processed,
393 AssetMetaCheck::Always,
394 watch,
395 self.unapproved_path_mode.clone(),
396 ))
397 .insert_resource(processor)
398 .add_systems(bevy_app::Startup, AssetProcessor::start);
399 } else {
400 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
401 let sources = builders.build_sources(false, watch);
402 app.insert_resource(AssetServer::new_with_meta_check(
403 Arc::new(sources),
404 AssetServerMode::Processed,
405 AssetMetaCheck::Always,
406 watch,
407 self.unapproved_path_mode.clone(),
408 ));
409 }
410 }
411 }
412 }
413 app.insert_resource(embedded)
414 .init_asset::<LoadedFolder>()
415 .init_asset::<LoadedUntypedAsset>()
416 .init_asset::<()>()
417 .add_message::<UntypedAssetLoadFailedEvent>()
418 .configure_sets(
419 PreUpdate,
420 AssetTrackingSystems.after(handle_internal_asset_events),
421 )
422 .add_systems(
427 PreUpdate,
428 (
429 handle_internal_asset_events.ambiguous_with_all(),
430 publish_asset_server_diagnostics.run_if(resource_exists::<DiagnosticsStore>),
433 )
434 .chain(),
435 )
436 .register_diagnostic(Diagnostic::new(AssetServer::STARTED_LOAD_COUNT));
437 }
438}
439
440#[diagnostic::on_unimplemented(
448 message = "`{Self}` is not an `Asset`",
449 label = "invalid `Asset`",
450 note = "consider annotating `{Self}` with `#[derive(Asset)]`"
451)]
452pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {}
453
454pub trait AsAssetId: Component {
456 type Asset: Asset;
458
459 fn as_asset_id(&self) -> AssetId<Self::Asset>;
461}
462
463pub trait VisitAssetDependencies {
468 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId));
469}
470
471impl<A: Asset> VisitAssetDependencies for Handle<A> {
472 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
473 visit(self.id().untyped());
474 }
475}
476
477impl<A: Asset> VisitAssetDependencies for Option<Handle<A>> {
478 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
479 if let Some(handle) = self {
480 visit(handle.id().untyped());
481 }
482 }
483}
484
485impl VisitAssetDependencies for UntypedHandle {
486 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
487 visit(self.id());
488 }
489}
490
491impl VisitAssetDependencies for Option<UntypedHandle> {
492 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
493 if let Some(handle) = self {
494 visit(handle.id());
495 }
496 }
497}
498
499impl VisitAssetDependencies for UntypedAssetId {
500 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
501 visit(*self);
502 }
503}
504
505impl<A: Asset, const N: usize> VisitAssetDependencies for [Handle<A>; N] {
506 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
507 for dependency in self {
508 visit(dependency.id().untyped());
509 }
510 }
511}
512
513impl<const N: usize> VisitAssetDependencies for [UntypedHandle; N] {
514 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
515 for dependency in self {
516 visit(dependency.id());
517 }
518 }
519}
520
521impl<V: VisitAssetDependencies> VisitAssetDependencies for Vec<V> {
522 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
523 for dependency in self {
524 dependency.visit_dependencies(visit);
525 }
526 }
527}
528
529impl<V: VisitAssetDependencies> VisitAssetDependencies for HashSet<V> {
530 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
531 for dependency in self {
532 dependency.visit_dependencies(visit);
533 }
534 }
535}
536
537impl<A: Asset, K> VisitAssetDependencies for HashMap<K, Handle<A>> {
538 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
539 for dependency in self.values() {
540 visit(dependency.id().untyped());
541 }
542 }
543}
544
545impl<K> VisitAssetDependencies for HashMap<K, UntypedHandle> {
546 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
547 for dependency in self.values() {
548 visit(dependency.id());
549 }
550 }
551}
552
553pub trait AssetApp {
555 fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self;
557 fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self;
559 fn register_asset_source(
564 &mut self,
565 id: impl Into<AssetSourceId<'static>>,
566 source: AssetSourceBuilder,
567 ) -> &mut Self;
568 fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self;
570 fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self;
572 fn init_asset<A: Asset>(&mut self) -> &mut Self;
580 fn register_asset_reflect<A>(&mut self) -> &mut Self
585 where
586 A: Asset + Reflect + FromReflect + GetTypeRegistration;
587 fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self;
590}
591
592impl AssetApp for App {
593 fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
594 self.world()
595 .resource::<AssetServer>()
596 .register_loader(loader);
597 self
598 }
599
600 fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self {
601 if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
602 asset_processor.register_processor(processor);
603 }
604 self
605 }
606
607 fn register_asset_source(
608 &mut self,
609 id: impl Into<AssetSourceId<'static>>,
610 source: AssetSourceBuilder,
611 ) -> &mut Self {
612 let id = id.into();
613 if self.world().get_resource::<AssetServer>().is_some() {
614 error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id);
615 }
616
617 {
618 let mut sources = self
619 .world_mut()
620 .get_resource_or_init::<AssetSourceBuilders>();
621 sources.insert(id, source);
622 }
623
624 self
625 }
626
627 fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self {
628 if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
629 asset_processor.set_default_processor::<P>(extension);
630 }
631 self
632 }
633
634 fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self {
635 let loader = L::from_world(self.world_mut());
636 self.register_asset_loader(loader)
637 }
638
639 fn init_asset<A: Asset>(&mut self) -> &mut Self {
640 let assets = Assets::<A>::default();
641 self.world()
642 .resource::<AssetServer>()
643 .register_asset(&assets);
644 if self.world().contains_resource::<AssetProcessor>() {
645 let processor = self.world().resource::<AssetProcessor>();
646 processor
650 .server()
651 .register_handle_provider(AssetHandleProvider::new(
652 TypeId::of::<A>(),
653 Arc::new(AssetIndexAllocator::default()),
654 ));
655 }
656 self.insert_resource(assets)
657 .allow_ambiguous_resource::<Assets<A>>()
658 .add_message::<AssetEvent<A>>()
659 .add_message::<AssetLoadFailedEvent<A>>()
660 .register_type::<Handle<A>>()
661 .add_systems(
662 PostUpdate,
663 Assets::<A>::asset_events
664 .run_if(Assets::<A>::asset_events_condition)
665 .in_set(AssetEventSystems),
666 )
667 .add_systems(
668 PreUpdate,
669 Assets::<A>::track_assets.in_set(AssetTrackingSystems),
670 )
671 }
672
673 fn register_asset_reflect<A>(&mut self) -> &mut Self
674 where
675 A: Asset + Reflect + FromReflect + GetTypeRegistration,
676 {
677 let type_registry = self.world().resource::<AppTypeRegistry>();
678 {
679 let mut type_registry = type_registry.write();
680
681 type_registry.register::<A>();
682 type_registry.register::<Handle<A>>();
683 type_registry.register::<HandleTemplate<A>>();
684 type_registry.register_type_data::<A, ReflectAsset>();
685 type_registry.register_type_data::<Handle<A>, ReflectHandle>();
686 type_registry
687 .register_type_conversion::<String, HandleTemplate<A>, _>(|s| Ok(s.into()));
688 }
689
690 self
691 }
692
693 fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self {
694 self.world_mut()
695 .resource_mut::<AssetServer>()
696 .preregister_loader::<L>(extensions);
697 self
698 }
699}
700
701#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
703pub struct AssetTrackingSystems;
704
705#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
709pub struct AssetEventSystems;
710
711#[cfg(test)]
712mod tests {
713 use crate::{
714 folder::LoadedFolder,
715 handle::Handle,
716 io::{
717 gated::{GateOpener, GatedReader},
718 memory::{Dir, MemoryAssetReader, MemoryAssetWriter},
719 AssetReader, AssetReaderError, AssetSourceBuilder, AssetSourceEvent, AssetSourceId,
720 AssetWatcher, Reader,
721 },
722 loader::{AssetLoader, LoadContext},
723 Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
724 AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, LoadedAsset,
725 UnapprovedPathMode, UntypedHandle, VisitAssetDependencies, WriteDefaultMetaError,
726 };
727 use alloc::{
728 boxed::Box,
729 format,
730 string::{String, ToString},
731 sync::Arc,
732 vec,
733 vec::Vec,
734 };
735 use async_channel::{Receiver, Sender};
736 use bevy_app::{App, TaskPoolPlugin, Update};
737 use bevy_diagnostic::{DiagnosticsPlugin, DiagnosticsStore};
738 use bevy_ecs::{
739 message::MessageCursor,
740 prelude::*,
741 schedule::{LogLevel, ScheduleBuildSettings},
742 };
743 use bevy_platform::{
744 collections::{HashMap, HashSet},
745 sync::Mutex,
746 };
747 use bevy_reflect::{Reflect, TypePath};
748 use bevy_tasks::block_on;
749 use core::{any::TypeId, time::Duration};
750 use futures_lite::AsyncReadExt;
751 use ron::ser::PrettyConfig;
752 use serde::{Deserialize, Serialize};
753 use std::path::{Path, PathBuf};
754 use thiserror::Error;
755
756 #[derive(Asset, Debug, Default, Reflect)]
757 pub struct CoolText {
758 pub text: String,
759 pub embedded: String,
760 #[dependency]
761 pub dependencies: Vec<Handle<CoolText>>,
762 #[dependency]
763 pub sub_texts: Vec<Handle<SubText>>,
764 }
765
766 #[derive(Asset, TypePath, Debug)]
767 pub struct SubText {
768 pub text: String,
769 }
770
771 #[derive(Serialize, Deserialize, Default)]
772 pub struct CoolTextRon {
773 pub text: String,
774 pub dependencies: Vec<String>,
775 pub embedded_dependencies: Vec<String>,
776 pub sub_texts: Vec<String>,
777 }
778
779 #[derive(Default, TypePath)]
780 pub struct CoolTextLoader;
781
782 #[derive(Error, Debug)]
783 pub enum CoolTextLoaderError {
784 #[error("Could not load dependency: {dependency}")]
785 CannotLoadDependency { dependency: AssetPath<'static> },
786 #[error("A RON error occurred during loading")]
787 RonSpannedError(#[from] ron::error::SpannedError),
788 #[error("An IO error occurred during loading")]
789 Io(#[from] std::io::Error),
790 }
791
792 impl AssetLoader for CoolTextLoader {
793 type Asset = CoolText;
794
795 type Settings = ();
796
797 type Error = CoolTextLoaderError;
798
799 async fn load(
800 &self,
801 reader: &mut dyn Reader,
802 _settings: &Self::Settings,
803 load_context: &mut LoadContext<'_>,
804 ) -> Result<Self::Asset, Self::Error> {
805 let mut bytes = Vec::new();
806 reader.read_to_end(&mut bytes).await?;
807 let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
808 let mut embedded = String::new();
809 for dep in ron.embedded_dependencies {
810 let loaded = load_context
811 .load_builder()
812 .load_value::<CoolText>(&dep)
813 .await
814 .map_err(|_| Self::Error::CannotLoadDependency {
815 dependency: dep.into(),
816 })?;
817 let cool = loaded.get();
818 embedded.push_str(&cool.text);
819 }
820 Ok(CoolText {
821 text: ron.text,
822 embedded,
823 dependencies: ron
824 .dependencies
825 .iter()
826 .map(|p| load_context.load(p))
827 .collect(),
828 sub_texts: ron
829 .sub_texts
830 .drain(..)
831 .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text }))
832 .collect(),
833 })
834 }
835
836 fn extensions(&self) -> &[&str] {
837 &["cool.ron"]
838 }
839 }
840
841 #[derive(Default, Clone)]
843 pub struct UnstableMemoryAssetReader {
844 pub attempt_counters: Arc<Mutex<HashMap<Box<Path>, usize>>>,
845 pub load_delay: Duration,
846 memory_reader: MemoryAssetReader,
847 failure_count: usize,
848 }
849
850 impl UnstableMemoryAssetReader {
851 pub fn new(root: Dir, failure_count: usize) -> Self {
852 Self {
853 load_delay: Duration::from_millis(10),
854 memory_reader: MemoryAssetReader { root },
855 attempt_counters: Default::default(),
856 failure_count,
857 }
858 }
859 }
860
861 impl AssetReader for UnstableMemoryAssetReader {
862 async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
863 self.memory_reader.is_directory(path).await
864 }
865 async fn read_directory<'a>(
866 &'a self,
867 path: &'a Path,
868 ) -> Result<Box<bevy_asset::io::PathStream>, AssetReaderError> {
869 self.memory_reader.read_directory(path).await
870 }
871 async fn read_meta<'a>(
872 &'a self,
873 path: &'a Path,
874 ) -> Result<impl Reader + 'a, AssetReaderError> {
875 self.memory_reader.read_meta(path).await
876 }
877 async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
878 let attempt_number = {
879 let mut attempt_counters = self.attempt_counters.lock().unwrap();
880 if let Some(existing) = attempt_counters.get_mut(path) {
881 *existing += 1;
882 *existing
883 } else {
884 attempt_counters.insert(path.into(), 1);
885 1
886 }
887 };
888
889 if attempt_number <= self.failure_count {
890 let io_error = std::io::Error::new(
891 std::io::ErrorKind::ConnectionRefused,
892 format!(
893 "Simulated failure {attempt_number} of {}",
894 self.failure_count
895 ),
896 );
897 let wait = self.load_delay;
898 return async move {
899 std::thread::sleep(wait);
900 Err(AssetReaderError::Io(io_error.into()))
901 }
902 .await;
903 }
904
905 self.memory_reader.read(path).await
906 }
907 }
908
909 pub(crate) fn create_app() -> (App, Dir) {
911 let mut app = App::new();
912 let dir = Dir::default();
913 let dir_clone = dir.clone();
914 let dir_clone2 = dir.clone();
915 app.register_asset_source(
916 AssetSourceId::Default,
917 AssetSourceBuilder::new(move || {
918 Box::new(MemoryAssetReader {
919 root: dir_clone.clone(),
920 })
921 })
922 .with_writer(move |_| {
923 Some(Box::new(MemoryAssetWriter {
924 root: dir_clone2.clone(),
925 }))
926 }),
927 )
928 .add_plugins((
929 TaskPoolPlugin::default(),
930 AssetPlugin {
931 watch_for_changes_override: Some(false),
932 use_asset_processor_override: Some(false),
933 ..Default::default()
934 },
935 DiagnosticsPlugin,
936 ));
937 (app, dir)
938 }
939
940 fn create_app_with_gate(dir: Dir) -> (App, GateOpener) {
941 let mut app = App::new();
942 let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir });
943 app.register_asset_source(
944 AssetSourceId::Default,
945 AssetSourceBuilder::new(move || Box::new(gated_memory_reader.clone())),
946 )
947 .add_plugins((
948 TaskPoolPlugin::default(),
949 AssetPlugin {
950 watch_for_changes_override: Some(false),
951 use_asset_processor_override: Some(false),
952 ..Default::default()
953 },
954 DiagnosticsPlugin,
955 ));
956 (app, gate_opener)
957 }
958
959 pub fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) {
960 for _ in 0..LARGE_ITERATION_COUNT {
961 app.update();
962 if predicate(app.world_mut()).is_some() {
963 return;
964 }
965 }
966
967 panic!("Ran out of loops to return `Some` from `predicate`");
968 }
969
970 const LARGE_ITERATION_COUNT: usize = 10000;
971
972 fn get<A: Asset>(world: &World, id: AssetId<A>) -> Option<&A> {
973 world.resource::<Assets<A>>().get(id)
974 }
975
976 fn get_started_load_count(world: &World) -> usize {
977 world
978 .resource::<DiagnosticsStore>()
979 .get_measurement(&AssetServer::STARTED_LOAD_COUNT)
980 .map(|measurement| measurement.value as _)
981 .unwrap_or_default()
982 }
983
984 #[derive(Resource, Default)]
985 struct StoredEvents(Vec<AssetEvent<CoolText>>);
986
987 fn store_asset_events(
988 mut reader: MessageReader<AssetEvent<CoolText>>,
989 mut storage: ResMut<StoredEvents>,
990 ) {
991 storage.0.extend(reader.read().cloned());
992 }
993
994 pub(crate) fn serialize_as_cool_text(text: &str) -> String {
999 let cool_text_ron = CoolTextRon {
1000 text: text.into(),
1001 dependencies: vec![],
1002 embedded_dependencies: vec![],
1003 sub_texts: vec![],
1004 };
1005 ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()
1006 }
1007
1008 #[test]
1009 fn load_dependencies() {
1010 let dir = Dir::default();
1011
1012 let a_path = "a.cool.ron";
1013 let a_ron = r#"
1014(
1015 text: "a",
1016 dependencies: [
1017 "foo/b.cool.ron",
1018 "c.cool.ron",
1019 ],
1020 embedded_dependencies: [],
1021 sub_texts: [],
1022)"#;
1023 let b_path = "foo/b.cool.ron";
1024 let b_ron = r#"
1025(
1026 text: "b",
1027 dependencies: [],
1028 embedded_dependencies: [],
1029 sub_texts: [],
1030)"#;
1031
1032 let c_path = "c.cool.ron";
1033 let c_ron = r#"
1034(
1035 text: "c",
1036 dependencies: [
1037 "d.cool.ron",
1038 ],
1039 embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"],
1040 sub_texts: ["hello"],
1041)"#;
1042
1043 let d_path = "d.cool.ron";
1044 let d_ron = r#"
1045(
1046 text: "d",
1047 dependencies: [],
1048 embedded_dependencies: [],
1049 sub_texts: [],
1050)"#;
1051
1052 dir.insert_asset_text(Path::new(a_path), a_ron);
1053 dir.insert_asset_text(Path::new(b_path), b_ron);
1054 dir.insert_asset_text(Path::new(c_path), c_ron);
1055 dir.insert_asset_text(Path::new(d_path), d_ron);
1056
1057 #[derive(Resource)]
1058 struct IdResults {
1059 b_id: AssetId<CoolText>,
1060 c_id: AssetId<CoolText>,
1061 d_id: AssetId<CoolText>,
1062 }
1063
1064 let (mut app, gate_opener) = create_app_with_gate(dir);
1065 app.init_asset::<CoolText>()
1066 .init_asset::<SubText>()
1067 .init_resource::<StoredEvents>()
1068 .register_asset_loader(CoolTextLoader)
1069 .add_systems(Update, store_asset_events);
1070 let asset_server = app.world().resource::<AssetServer>().clone();
1071 let handle: Handle<CoolText> = asset_server.load(a_path);
1072 let a_id = handle.id();
1073 app.update();
1074 assert_eq!(get_started_load_count(app.world()), 1);
1075
1076 {
1077 let a_text = get::<CoolText>(app.world(), a_id);
1078 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1079 assert!(a_text.is_none(), "a's asset should not exist yet");
1080 assert!(a_load.is_loading());
1081 assert!(a_deps.is_loading());
1082 assert!(a_rec_deps.is_loading());
1083 }
1084
1085 gate_opener.open(a_path);
1088 run_app_until(&mut app, |world| {
1089 let a_text = get::<CoolText>(world, a_id)?;
1090 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1091 assert_eq!(a_text.text, "a");
1092 assert_eq!(a_text.dependencies.len(), 2);
1093 assert!(a_load.is_loaded());
1094 assert!(a_deps.is_loading());
1095 assert!(a_rec_deps.is_loading());
1096
1097 let b_id = a_text.dependencies[0].id();
1098 let b_text = get::<CoolText>(world, b_id);
1099 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1100 assert!(b_text.is_none(), "b component should not exist yet");
1101 assert!(b_load.is_loading());
1102 assert!(b_deps.is_loading());
1103 assert!(b_rec_deps.is_loading());
1104
1105 let c_id = a_text.dependencies[1].id();
1106 let c_text = get::<CoolText>(world, c_id);
1107 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1108 assert!(c_text.is_none(), "c component should not exist yet");
1109 assert!(c_load.is_loading());
1110 assert!(c_deps.is_loading());
1111 assert!(c_rec_deps.is_loading());
1112 Some(())
1113 });
1114 assert_eq!(get_started_load_count(app.world()), 3);
1115
1116 gate_opener.open(b_path);
1119 run_app_until(&mut app, |world| {
1120 let a_text = get::<CoolText>(world, a_id)?;
1121 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1122 assert_eq!(a_text.text, "a");
1123 assert_eq!(a_text.dependencies.len(), 2);
1124 assert!(a_load.is_loaded());
1125 assert!(a_deps.is_loading());
1126 assert!(a_rec_deps.is_loading());
1127
1128 let b_id = a_text.dependencies[0].id();
1129 let b_text = get::<CoolText>(world, b_id)?;
1130 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1131 assert_eq!(b_text.text, "b");
1132 assert!(b_load.is_loaded());
1133 assert!(b_deps.is_loaded());
1134 assert!(b_rec_deps.is_loaded());
1135
1136 let c_id = a_text.dependencies[1].id();
1137 let c_text = get::<CoolText>(world, c_id);
1138 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1139 assert!(c_text.is_none(), "c component should not exist yet");
1140 assert!(c_load.is_loading());
1141 assert!(c_deps.is_loading());
1142 assert!(c_rec_deps.is_loading());
1143 Some(())
1144 });
1145 assert_eq!(get_started_load_count(app.world()), 3);
1146
1147 gate_opener.open(c_path);
1150
1151 gate_opener.open(a_path);
1153 gate_opener.open(b_path);
1154 run_app_until(&mut app, |world| {
1155 let a_text = get::<CoolText>(world, a_id)?;
1156 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1157 assert_eq!(a_text.text, "a");
1158 assert_eq!(a_text.embedded, "");
1159 assert_eq!(a_text.dependencies.len(), 2);
1160 assert!(a_load.is_loaded());
1161
1162 let b_id = a_text.dependencies[0].id();
1163 let b_text = get::<CoolText>(world, b_id)?;
1164 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1165 assert_eq!(b_text.text, "b");
1166 assert_eq!(b_text.embedded, "");
1167 assert!(b_load.is_loaded());
1168 assert!(b_deps.is_loaded());
1169 assert!(b_rec_deps.is_loaded());
1170
1171 let c_id = a_text.dependencies[1].id();
1172 let c_text = get::<CoolText>(world, c_id)?;
1173 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1174 assert_eq!(c_text.text, "c");
1175 assert_eq!(c_text.embedded, "ab");
1176 assert!(c_load.is_loaded());
1177 assert!(
1178 c_deps.is_loading(),
1179 "c deps should not be loaded yet because d has not loaded"
1180 );
1181 assert!(
1182 c_rec_deps.is_loading(),
1183 "c rec deps should not be loaded yet because d has not loaded"
1184 );
1185
1186 let sub_text_id = c_text.sub_texts[0].id();
1187 let sub_text = get::<SubText>(world, sub_text_id)
1188 .expect("subtext should exist if c exists. it came from the same loader");
1189 assert_eq!(sub_text.text, "hello");
1190 let (sub_text_load, sub_text_deps, sub_text_rec_deps) =
1191 asset_server.get_load_states(sub_text_id).unwrap();
1192 assert!(sub_text_load.is_loaded());
1193 assert!(sub_text_deps.is_loaded());
1194 assert!(sub_text_rec_deps.is_loaded());
1195
1196 let d_id = c_text.dependencies[0].id();
1197 let d_text = get::<CoolText>(world, d_id);
1198 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1199 assert!(d_text.is_none(), "d component should not exist yet");
1200 assert!(d_load.is_loading());
1201 assert!(d_deps.is_loading());
1202 assert!(d_rec_deps.is_loading());
1203
1204 assert!(
1205 a_deps.is_loaded(),
1206 "If c has been loaded, the a deps should all be considered loaded"
1207 );
1208 assert!(
1209 a_rec_deps.is_loading(),
1210 "d is not loaded, so a's recursive deps should still be loading"
1211 );
1212 world.insert_resource(IdResults { b_id, c_id, d_id });
1213 Some(())
1214 });
1215 assert_eq!(get_started_load_count(app.world()), 6);
1216
1217 gate_opener.open(d_path);
1218 run_app_until(&mut app, |world| {
1219 let a_text = get::<CoolText>(world, a_id)?;
1220 let (_a_load, _a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1221 let c_id = a_text.dependencies[1].id();
1222 let c_text = get::<CoolText>(world, c_id)?;
1223 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1224 assert_eq!(c_text.text, "c");
1225 assert_eq!(c_text.embedded, "ab");
1226
1227 let d_id = c_text.dependencies[0].id();
1228 let d_text = get::<CoolText>(world, d_id)?;
1229 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1230 assert_eq!(d_text.text, "d");
1231 assert_eq!(d_text.embedded, "");
1232
1233 assert!(c_load.is_loaded());
1234 assert!(c_deps.is_loaded());
1235 assert!(c_rec_deps.is_loaded());
1236
1237 assert!(d_load.is_loaded());
1238 assert!(d_deps.is_loaded());
1239 assert!(d_rec_deps.is_loaded());
1240
1241 assert!(
1242 a_rec_deps.is_loaded(),
1243 "d is loaded, so a's recursive deps should be loaded"
1244 );
1245 Some(())
1246 });
1247
1248 assert_eq!(get_started_load_count(app.world()), 6);
1249
1250 {
1251 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1252 let mut a = texts.get_mut(a_id).unwrap();
1253 a.text = "Changed".to_string();
1254 }
1255
1256 drop(handle);
1257
1258 app.update();
1259 assert_eq!(
1260 app.world().resource::<Assets<CoolText>>().len(),
1261 0,
1262 "CoolText asset entities should be despawned when no more handles exist"
1263 );
1264 app.update();
1265 assert_eq!(
1267 app.world().resource::<Assets<SubText>>().len(),
1268 0,
1269 "SubText asset entities should be despawned when no more handles exist"
1270 );
1271 let events = app.world_mut().remove_resource::<StoredEvents>().unwrap();
1272 let id_results = app.world_mut().remove_resource::<IdResults>().unwrap();
1273 let expected_events = vec![
1274 AssetEvent::Added { id: a_id },
1275 AssetEvent::LoadedWithDependencies {
1276 id: id_results.b_id,
1277 },
1278 AssetEvent::Added {
1279 id: id_results.b_id,
1280 },
1281 AssetEvent::Added {
1282 id: id_results.c_id,
1283 },
1284 AssetEvent::LoadedWithDependencies {
1285 id: id_results.d_id,
1286 },
1287 AssetEvent::LoadedWithDependencies {
1288 id: id_results.c_id,
1289 },
1290 AssetEvent::LoadedWithDependencies { id: a_id },
1291 AssetEvent::Added {
1292 id: id_results.d_id,
1293 },
1294 AssetEvent::Modified { id: a_id },
1295 AssetEvent::Unused { id: a_id },
1296 AssetEvent::Removed { id: a_id },
1297 AssetEvent::Unused {
1298 id: id_results.b_id,
1299 },
1300 AssetEvent::Removed {
1301 id: id_results.b_id,
1302 },
1303 AssetEvent::Unused {
1304 id: id_results.c_id,
1305 },
1306 AssetEvent::Removed {
1307 id: id_results.c_id,
1308 },
1309 AssetEvent::Unused {
1310 id: id_results.d_id,
1311 },
1312 AssetEvent::Removed {
1313 id: id_results.d_id,
1314 },
1315 ];
1316 assert_eq!(events.0, expected_events);
1317 }
1318
1319 #[test]
1320 fn failure_load_states() {
1321 let dir = Dir::default();
1322
1323 let a_path = "a.cool.ron";
1324 let a_ron = r#"
1325(
1326 text: "a",
1327 dependencies: [
1328 "b.cool.ron",
1329 "c.cool.ron",
1330 ],
1331 embedded_dependencies: [],
1332 sub_texts: []
1333)"#;
1334 let b_path = "b.cool.ron";
1335 let b_ron = r#"
1336(
1337 text: "b",
1338 dependencies: [],
1339 embedded_dependencies: [],
1340 sub_texts: []
1341)"#;
1342
1343 let c_path = "c.cool.ron";
1344 let c_ron = r#"
1345(
1346 text: "c",
1347 dependencies: [
1348 "d.cool.ron",
1349 ],
1350 embedded_dependencies: [],
1351 sub_texts: []
1352)"#;
1353
1354 let d_path = "d.cool.ron";
1355 let d_ron = r#"
1356(
1357 text: "d",
1358 dependencies: [],
1359 OH NO THIS ASSET IS MALFORMED
1360 embedded_dependencies: [],
1361 sub_texts: []
1362)"#;
1363
1364 dir.insert_asset_text(Path::new(a_path), a_ron);
1365 dir.insert_asset_text(Path::new(b_path), b_ron);
1366 dir.insert_asset_text(Path::new(c_path), c_ron);
1367 dir.insert_asset_text(Path::new(d_path), d_ron);
1368
1369 let (mut app, gate_opener) = create_app_with_gate(dir);
1370 app.init_asset::<CoolText>()
1371 .register_asset_loader(CoolTextLoader);
1372 let asset_server = app.world().resource::<AssetServer>().clone();
1373 let handle: Handle<CoolText> = asset_server.load(a_path);
1374 let a_id = handle.id();
1375
1376 app.update();
1377 assert_eq!(get_started_load_count(app.world()), 1);
1378 {
1379 let other_handle: Handle<CoolText> = asset_server.load(a_path);
1380 assert_eq!(
1381 other_handle, handle,
1382 "handles from consecutive load calls should be equal"
1383 );
1384 assert_eq!(
1385 other_handle.id(),
1386 handle.id(),
1387 "handle ids from consecutive load calls should be equal"
1388 );
1389
1390 app.update();
1391 assert_eq!(get_started_load_count(app.world()), 1);
1393 }
1394
1395 gate_opener.open(a_path);
1396 gate_opener.open(b_path);
1397 gate_opener.open(c_path);
1398 gate_opener.open(d_path);
1399
1400 run_app_until(&mut app, |world| {
1401 let a_text = get::<CoolText>(world, a_id)?;
1402 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1403
1404 let b_id = a_text.dependencies[0].id();
1405 let b_text = get::<CoolText>(world, b_id)?;
1406 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1407
1408 let c_id = a_text.dependencies[1].id();
1409 let c_text = get::<CoolText>(world, c_id)?;
1410 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1411
1412 let d_id = c_text.dependencies[0].id();
1413 let d_text = get::<CoolText>(world, d_id);
1414 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1415
1416 if !d_load.is_failed() {
1417 return None;
1419 }
1420
1421 assert!(d_text.is_none());
1422 assert!(d_load.is_failed());
1423 assert!(d_deps.is_failed());
1424 assert!(d_rec_deps.is_failed());
1425
1426 assert_eq!(a_text.text, "a");
1427 assert!(a_load.is_loaded());
1428 assert!(a_deps.is_loaded());
1429 assert!(a_rec_deps.is_failed());
1430
1431 assert_eq!(b_text.text, "b");
1432 assert!(b_load.is_loaded());
1433 assert!(b_deps.is_loaded());
1434 assert!(b_rec_deps.is_loaded());
1435
1436 assert_eq!(c_text.text, "c");
1437 assert!(c_load.is_loaded());
1438 assert!(c_deps.is_failed());
1439 assert!(c_rec_deps.is_failed());
1440
1441 assert!(asset_server.load_state(a_id).is_loaded());
1442 assert!(asset_server.dependency_load_state(a_id).is_loaded());
1443 assert!(asset_server
1444 .recursive_dependency_load_state(a_id)
1445 .is_failed());
1446
1447 assert!(asset_server.is_loaded(a_id));
1448 assert!(asset_server.is_loaded_with_direct_dependencies(a_id));
1449 assert!(!asset_server.is_loaded_with_dependencies(a_id));
1450
1451 Some(())
1452 });
1453
1454 assert_eq!(get_started_load_count(app.world()), 4);
1455 }
1456
1457 #[test]
1458 fn dependency_load_states() {
1459 let a_path = "a.cool.ron";
1460 let a_ron = r#"
1461(
1462 text: "a",
1463 dependencies: [
1464 "b.cool.ron",
1465 "c.cool.ron",
1466 ],
1467 embedded_dependencies: [],
1468 sub_texts: []
1469)"#;
1470 let b_path = "b.cool.ron";
1471 let b_ron = r#"
1472(
1473 text: "b",
1474 dependencies: [],
1475 MALFORMED
1476 embedded_dependencies: [],
1477 sub_texts: []
1478)"#;
1479
1480 let c_path = "c.cool.ron";
1481 let c_ron = r#"
1482(
1483 text: "c",
1484 dependencies: [],
1485 embedded_dependencies: [],
1486 sub_texts: []
1487)"#;
1488
1489 let dir = Dir::default();
1490 dir.insert_asset_text(Path::new(a_path), a_ron);
1491 dir.insert_asset_text(Path::new(b_path), b_ron);
1492 dir.insert_asset_text(Path::new(c_path), c_ron);
1493
1494 let (mut app, gate_opener) = create_app_with_gate(dir);
1495 app.init_asset::<CoolText>()
1496 .register_asset_loader(CoolTextLoader);
1497 let asset_server = app.world().resource::<AssetServer>().clone();
1498 let handle: Handle<CoolText> = asset_server.load(a_path);
1499 let a_id = handle.id();
1500
1501 app.update();
1502 assert_eq!(get_started_load_count(app.world()), 1);
1503
1504 gate_opener.open(a_path);
1505 run_app_until(&mut app, |world| {
1506 let _a_text = get::<CoolText>(world, a_id)?;
1507 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1508 assert!(a_load.is_loaded());
1509 assert!(a_deps.is_loading());
1510 assert!(a_rec_deps.is_loading());
1511 Some(())
1512 });
1513
1514 assert_eq!(get_started_load_count(app.world()), 3);
1515
1516 gate_opener.open(b_path);
1517 run_app_until(&mut app, |world| {
1518 let a_text = get::<CoolText>(world, a_id)?;
1519 let b_id = a_text.dependencies[0].id();
1520
1521 let (b_load, _b_deps, _b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1522 if !b_load.is_failed() {
1523 return None;
1525 }
1526
1527 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1528 assert!(a_load.is_loaded());
1529 assert!(a_deps.is_failed());
1530 assert!(a_rec_deps.is_failed());
1531 Some(())
1532 });
1533
1534 assert_eq!(get_started_load_count(app.world()), 3);
1535
1536 gate_opener.open(c_path);
1537 run_app_until(&mut app, |world| {
1538 let a_text = get::<CoolText>(world, a_id)?;
1539 let c_id = a_text.dependencies[1].id();
1540 let _c_text = get::<CoolText>(world, c_id)?;
1542
1543 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1544 assert!(a_load.is_loaded());
1545 assert!(
1546 a_deps.is_failed(),
1547 "Successful dependency load should not overwrite a previous failure"
1548 );
1549 assert!(
1550 a_rec_deps.is_failed(),
1551 "Successful dependency load should not overwrite a previous failure"
1552 );
1553 Some(())
1554 });
1555
1556 assert_eq!(get_started_load_count(app.world()), 3);
1557 }
1558
1559 const SIMPLE_TEXT: &str = r#"
1560(
1561 text: "dep",
1562 dependencies: [],
1563 embedded_dependencies: [],
1564 sub_texts: [],
1565)"#;
1566 #[test]
1567 fn keep_gotten_strong_handles() {
1568 let dir = Dir::default();
1569 dir.insert_asset_text(Path::new("dep.cool.ron"), SIMPLE_TEXT);
1570
1571 let (mut app, _) = create_app_with_gate(dir);
1572 app.init_asset::<CoolText>()
1573 .init_asset::<SubText>()
1574 .init_resource::<StoredEvents>()
1575 .register_asset_loader(CoolTextLoader)
1576 .add_systems(Update, store_asset_events);
1577
1578 let id = {
1579 let handle = {
1580 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1581 let handle = texts.add(CoolText::default());
1582 texts.get_strong_handle(handle.id()).unwrap()
1583 };
1584
1585 app.update();
1586
1587 {
1588 let text = app.world().resource::<Assets<CoolText>>().get(&handle);
1589 assert!(text.is_some());
1590 }
1591 handle.id()
1592 };
1593 app.update();
1595 assert!(
1596 app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1597 "asset has no handles, so it should have been dropped last update"
1598 );
1599 }
1600
1601 #[test]
1602 fn manual_asset_management() {
1603 let dir = Dir::default();
1604 let dep_path = "dep.cool.ron";
1605
1606 dir.insert_asset_text(Path::new(dep_path), SIMPLE_TEXT);
1607
1608 let (mut app, gate_opener) = create_app_with_gate(dir);
1609 app.init_asset::<CoolText>()
1610 .init_asset::<SubText>()
1611 .init_resource::<StoredEvents>()
1612 .register_asset_loader(CoolTextLoader)
1613 .add_systems(Update, store_asset_events);
1614
1615 let hello = "hello".to_string();
1616 let empty = "".to_string();
1617
1618 let id = {
1619 let handle = {
1620 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1621 texts.add(CoolText {
1622 text: hello.clone(),
1623 embedded: empty.clone(),
1624 dependencies: vec![],
1625 sub_texts: Vec::new(),
1626 })
1627 };
1628
1629 app.update();
1630
1631 {
1632 let text = app
1633 .world()
1634 .resource::<Assets<CoolText>>()
1635 .get(&handle)
1636 .unwrap();
1637 assert_eq!(text.text, hello);
1638 }
1639 handle.id()
1640 };
1641 app.update();
1643 assert!(
1644 app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1645 "asset has no handles, so it should have been dropped last update"
1646 );
1647 app.update();
1649 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1650 let expected_events = vec![
1651 AssetEvent::Added { id },
1652 AssetEvent::Unused { id },
1653 AssetEvent::Removed { id },
1654 ];
1655
1656 assert_eq!(get_started_load_count(app.world()), 0);
1658
1659 assert_eq!(events, expected_events);
1660
1661 let dep_handle = app.world().resource::<AssetServer>().load(dep_path);
1662
1663 app.update();
1664 assert_eq!(get_started_load_count(app.world()), 1);
1665
1666 let a = CoolText {
1667 text: "a".to_string(),
1668 embedded: empty,
1669 dependencies: vec![dep_handle.clone()],
1671 sub_texts: Vec::new(),
1672 };
1673 let a_handle = app.world().resource::<AssetServer>().load_asset(a);
1674
1675 assert_eq!(get_started_load_count(app.world()), 1);
1677
1678 app.update();
1679 app.update();
1681
1682 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1683 let expected_events = vec![AssetEvent::Added { id: a_handle.id() }];
1684 assert_eq!(events, expected_events);
1685
1686 gate_opener.open(dep_path);
1687 loop {
1688 app.update();
1689 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1690 if events.is_empty() {
1691 continue;
1692 }
1693 let expected_events = vec![
1694 AssetEvent::LoadedWithDependencies {
1695 id: dep_handle.id(),
1696 },
1697 AssetEvent::LoadedWithDependencies { id: a_handle.id() },
1698 ];
1699 assert_eq!(events, expected_events);
1700 break;
1701 }
1702
1703 assert_eq!(get_started_load_count(app.world()), 1);
1704
1705 app.update();
1706 let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1707 let expected_events = vec![AssetEvent::Added {
1708 id: dep_handle.id(),
1709 }];
1710 assert_eq!(events, expected_events);
1711 }
1712
1713 #[test]
1714 fn load_folder() {
1715 let dir = Dir::default();
1716
1717 let a_path = "text/a.cool.ron";
1718 let a_ron = r#"
1719(
1720 text: "a",
1721 dependencies: [
1722 "b.cool.ron",
1723 ],
1724 embedded_dependencies: [],
1725 sub_texts: [],
1726)"#;
1727 let b_path = "b.cool.ron";
1728 let b_ron = r#"
1729(
1730 text: "b",
1731 dependencies: [],
1732 embedded_dependencies: [],
1733 sub_texts: [],
1734)"#;
1735
1736 let c_path = "text/c.cool.ron";
1737 let c_ron = r#"
1738(
1739 text: "c",
1740 dependencies: [
1741 ],
1742 embedded_dependencies: [],
1743 sub_texts: [],
1744)"#;
1745 dir.insert_asset_text(Path::new(a_path), a_ron);
1746 dir.insert_asset_text(Path::new(b_path), b_ron);
1747 dir.insert_asset_text(Path::new(c_path), c_ron);
1748
1749 let (mut app, gate_opener) = create_app_with_gate(dir);
1750 app.init_asset::<CoolText>()
1751 .init_asset::<SubText>()
1752 .register_asset_loader(CoolTextLoader);
1753 let asset_server = app.world().resource::<AssetServer>().clone();
1754 let handle: Handle<LoadedFolder> = asset_server.load_folder("text");
1755
1756 app.update();
1760 let started_load_tasks = get_started_load_count(app.world());
1761 assert!((1..=2).contains(&started_load_tasks));
1762
1763 gate_opener.open(a_path);
1764 gate_opener.open(b_path);
1765 gate_opener.open(c_path);
1766
1767 let mut cursor = MessageCursor::default();
1768 run_app_until(&mut app, |world| {
1769 let events = world.resource::<Messages<AssetEvent<LoadedFolder>>>();
1770 let asset_server = world.resource::<AssetServer>();
1771 let loaded_folders = world.resource::<Assets<LoadedFolder>>();
1772 let cool_texts = world.resource::<Assets<CoolText>>();
1773 for event in cursor.read(events) {
1774 if let AssetEvent::LoadedWithDependencies { id } = event
1775 && *id == handle.id()
1776 {
1777 let loaded_folder = loaded_folders.get(&handle).unwrap();
1778 let a_handle: Handle<CoolText> =
1779 asset_server.get_handle("text/a.cool.ron").unwrap();
1780 let c_handle: Handle<CoolText> =
1781 asset_server.get_handle("text/c.cool.ron").unwrap();
1782
1783 let mut found_a = false;
1784 let mut found_c = false;
1785 for asset_handle in &loaded_folder.handles {
1786 if asset_handle.id() == a_handle.id().untyped() {
1787 found_a = true;
1788 } else if asset_handle.id() == c_handle.id().untyped() {
1789 found_c = true;
1790 }
1791 }
1792 assert!(found_a);
1793 assert!(found_c);
1794 assert_eq!(loaded_folder.handles.len(), 2);
1795
1796 let a_text = cool_texts.get(&a_handle).unwrap();
1797 let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();
1798 let c_text = cool_texts.get(&c_handle).unwrap();
1799
1800 assert_eq!("a", a_text.text);
1801 assert_eq!("b", b_text.text);
1802 assert_eq!("c", c_text.text);
1803
1804 return Some(());
1805 }
1806 }
1807 None
1808 });
1809 assert_eq!(get_started_load_count(app.world()), 4);
1810 }
1811
1812 #[test]
1814 fn load_error_events() {
1815 #[derive(Resource, Default)]
1816 struct ErrorTracker {
1817 tick: u64,
1818 failures: usize,
1819 queued_retries: Vec<(AssetPath<'static>, AssetId<CoolText>, u64)>,
1820 finished_asset: Option<AssetId<CoolText>>,
1821 }
1822
1823 fn asset_event_handler(
1824 mut events: MessageReader<AssetEvent<CoolText>>,
1825 mut tracker: ResMut<ErrorTracker>,
1826 ) {
1827 for event in events.read() {
1828 if let AssetEvent::LoadedWithDependencies { id } = event {
1829 tracker.finished_asset = Some(*id);
1830 }
1831 }
1832 }
1833
1834 fn asset_load_error_event_handler(
1835 server: Res<AssetServer>,
1836 mut errors: MessageReader<AssetLoadFailedEvent<CoolText>>,
1837 mut tracker: ResMut<ErrorTracker>,
1838 ) {
1839 tracker.tick += 1;
1841
1842 let now = tracker.tick;
1844 tracker
1845 .queued_retries
1846 .retain(|(path, old_id, retry_after)| {
1847 if now > *retry_after {
1848 let new_handle = server.load::<CoolText>(path);
1849 assert_eq!(&new_handle.id(), old_id);
1850 false
1851 } else {
1852 true
1853 }
1854 });
1855
1856 for error in errors.read() {
1858 let (load_state, _, _) = server.get_load_states(error.id).unwrap();
1859 assert!(load_state.is_failed());
1860 assert_eq!(*error.path.source(), AssetSourceId::Name("unstable".into()));
1861 match &error.error {
1862 AssetLoadError::AssetReaderError(read_error) => match read_error {
1863 AssetReaderError::Io(_) => {
1864 tracker.failures += 1;
1865 if tracker.failures <= 2 {
1866 tracker.queued_retries.push((
1868 error.path.clone(),
1869 error.id,
1870 now + 10,
1871 ));
1872 } else {
1873 panic!(
1874 "Unexpected failure #{} (expected only 2)",
1875 tracker.failures
1876 );
1877 }
1878 }
1879 _ => panic!("Unexpected error type {}", read_error),
1880 },
1881 _ => panic!("Unexpected error type {}", error.error),
1882 }
1883 }
1884 }
1885
1886 let a_path = "text/a.cool.ron";
1887 let a_ron = r#"
1888(
1889 text: "a",
1890 dependencies: [],
1891 embedded_dependencies: [],
1892 sub_texts: [],
1893)"#;
1894
1895 let dir = Dir::default();
1896 dir.insert_asset_text(Path::new(a_path), a_ron);
1897 let unstable_reader = UnstableMemoryAssetReader::new(dir, 2);
1898
1899 let mut app = App::new();
1900 app.register_asset_source(
1901 AssetSourceId::Default,
1902 AssetSourceBuilder::new(move || {
1903 Box::new(MemoryAssetReader {
1906 root: Dir::default(),
1907 })
1908 }),
1909 )
1910 .register_asset_source(
1911 "unstable",
1912 AssetSourceBuilder::new(move || Box::new(unstable_reader.clone())),
1913 )
1914 .add_plugins((
1915 TaskPoolPlugin::default(),
1916 AssetPlugin {
1917 watch_for_changes_override: Some(false),
1918 use_asset_processor_override: Some(false),
1919 ..Default::default()
1920 },
1921 ))
1922 .init_asset::<CoolText>()
1923 .register_asset_loader(CoolTextLoader)
1924 .init_resource::<ErrorTracker>()
1925 .add_systems(
1926 Update,
1927 (asset_event_handler, asset_load_error_event_handler).chain(),
1928 );
1929
1930 let asset_server = app.world().resource::<AssetServer>().clone();
1931 let a_path = format!("unstable://{a_path}");
1932 let a_handle: Handle<CoolText> = asset_server.load(a_path);
1933 let a_id = a_handle.id();
1934
1935 run_app_until(&mut app, |world| {
1936 let tracker = world.resource::<ErrorTracker>();
1937 match tracker.finished_asset {
1938 Some(asset_id) => {
1939 assert_eq!(asset_id, a_id);
1940 let assets = world.resource::<Assets<CoolText>>();
1941 let result = assets.get(asset_id).unwrap();
1942 assert_eq!(result.text, "a");
1943 Some(())
1944 }
1945 None => None,
1946 }
1947 });
1948 }
1949
1950 #[test]
1951 fn ignore_system_ambiguities_on_assets() {
1952 let mut app = create_app().0;
1953 app.init_asset::<CoolText>();
1954
1955 fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1956 app.add_systems(Update, (uses_assets, uses_assets));
1957 app.edit_schedule(Update, |s| {
1958 s.set_build_settings(ScheduleBuildSettings {
1959 ambiguity_detection: LogLevel::Error,
1960 ..Default::default()
1961 });
1962 });
1963
1964 app.world_mut().run_schedule(Update);
1966 }
1967
1968 #[test]
1971 fn error_on_nested_immediate_load_of_subasset() {
1972 let (mut app, dir) = create_app();
1973 dir.insert_asset_text(
1974 Path::new("a.cool.ron"),
1975 r#"(
1976 text: "b",
1977 dependencies: [],
1978 embedded_dependencies: [],
1979 sub_texts: ["A"],
1980)"#,
1981 );
1982 dir.insert_asset_text(Path::new("empty.txt"), "");
1983
1984 app.init_asset::<CoolText>()
1985 .init_asset::<SubText>()
1986 .register_asset_loader(CoolTextLoader);
1987
1988 #[derive(TypePath)]
1989 struct NestedLoadOfSubassetLoader;
1990
1991 impl AssetLoader for NestedLoadOfSubassetLoader {
1992 type Asset = TestAsset;
1993 type Error = crate::loader::LoadDirectError;
1994 type Settings = ();
1995
1996 async fn load(
1997 &self,
1998 _: &mut dyn Reader,
1999 _: &Self::Settings,
2000 load_context: &mut LoadContext<'_>,
2001 ) -> Result<Self::Asset, Self::Error> {
2002 load_context
2004 .load_builder()
2005 .load_value::<SubText>("a.cool.ron#A")
2006 .await?;
2007 Ok(TestAsset)
2008 }
2009
2010 fn extensions(&self) -> &[&str] {
2011 &["txt"]
2012 }
2013 }
2014
2015 app.init_asset::<TestAsset>()
2016 .register_asset_loader(NestedLoadOfSubassetLoader);
2017
2018 let asset_server = app.world().resource::<AssetServer>().clone();
2019 let handle = asset_server.load::<TestAsset>("empty.txt");
2020
2021 run_app_until(&mut app, |_world| match asset_server.load_state(&handle) {
2022 LoadState::Loading => None,
2023 LoadState::Failed(err) => {
2024 let error_message = format!("{err}");
2025 assert!(error_message.contains("Requested to load an asset path (a.cool.ron#A) with a subasset, but this is unsupported"), "what? \"{error_message}\"");
2026 Some(())
2027 }
2028 state => panic!("Unexpected asset state: {state:?}"),
2029 });
2030 }
2031
2032 #[derive(Asset, TypePath)]
2034 pub struct TestAsset;
2035
2036 #[derive(VisitAssetDependencies)]
2040 pub struct TestNonAssetType(#[dependency] Handle<TestAsset>);
2041
2042 #[derive(Asset, TypePath)]
2043 #[expect(
2044 dead_code,
2045 reason = "This exists to ensure that `#[derive(Asset)]` works on enums. The inner variants are known not to be used."
2046 )]
2047 pub enum EnumTestAsset {
2048 Unnamed(#[dependency] Handle<TestAsset>),
2049 Named {
2050 #[dependency]
2051 handle: Handle<TestAsset>,
2052 #[dependency]
2053 vec_handles: Vec<Handle<TestAsset>>,
2054 #[dependency]
2055 embedded: TestAsset,
2056 #[dependency]
2057 set_handles: HashSet<Handle<TestAsset>>,
2058 #[dependency]
2059 untyped_set_handles: HashSet<UntypedHandle>,
2060 #[dependency]
2061 map_handles: HashMap<String, Handle<TestAsset>>,
2062 #[dependency]
2063 untyped_map_handles: HashMap<String, UntypedHandle>,
2064 #[dependency]
2065 non_asset_type: TestNonAssetType,
2066 },
2067 StructStyle(#[dependency] TestAsset),
2068 Empty,
2069 }
2070
2071 #[expect(
2072 dead_code,
2073 reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed."
2074 )]
2075 #[derive(Asset, TypePath)]
2076 pub struct StructTestAsset {
2077 #[dependency]
2078 handle: Handle<TestAsset>,
2079 #[dependency]
2080 embedded: TestAsset,
2081 #[dependency]
2082 array_handles: [Handle<TestAsset>; 5],
2083 #[dependency]
2084 untyped_array_handles: [UntypedHandle; 5],
2085 #[dependency]
2086 set_handles: HashSet<Handle<TestAsset>>,
2087 #[dependency]
2088 untyped_set_handles: HashSet<UntypedHandle>,
2089 #[dependency]
2090 map_handles: HashMap<String, Handle<TestAsset>>,
2091 #[dependency]
2092 untyped_map_handles: HashMap<String, UntypedHandle>,
2093 #[dependency]
2094 non_asset_type: TestNonAssetType,
2095 }
2096
2097 #[expect(
2098 dead_code,
2099 reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed."
2100 )]
2101 #[derive(Asset, TypePath)]
2102 pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);
2103
2104 fn unapproved_path_setup(mode: UnapprovedPathMode) -> App {
2105 let dir = Dir::default();
2106 let a_path = "../a.cool.ron";
2107 let a_ron = r#"
2108(
2109 text: "a",
2110 dependencies: [],
2111 embedded_dependencies: [],
2112 sub_texts: [],
2113)"#;
2114
2115 dir.insert_asset_text(Path::new(a_path), a_ron);
2116
2117 let mut app = App::new();
2118 let memory_reader = MemoryAssetReader { root: dir };
2119 app.register_asset_source(
2120 AssetSourceId::Default,
2121 AssetSourceBuilder::new(move || Box::new(memory_reader.clone())),
2122 )
2123 .add_plugins((
2124 TaskPoolPlugin::default(),
2125 AssetPlugin {
2126 unapproved_path_mode: mode,
2127 watch_for_changes_override: Some(false),
2128 use_asset_processor_override: Some(false),
2129 ..Default::default()
2130 },
2131 ));
2132 app.init_asset::<CoolText>()
2133 .register_asset_loader(CoolTextLoader);
2134
2135 app
2136 }
2137
2138 #[test]
2139 fn unapproved_path_forbid_does_not_load_even_with_override() {
2140 let app = unapproved_path_setup(UnapprovedPathMode::Forbid);
2141
2142 let asset_server = app.world().resource::<AssetServer>().clone();
2143 assert_eq!(
2144 asset_server
2145 .load_builder()
2146 .override_unapproved()
2147 .load::<CoolText>("../a.cool.ron"),
2148 Handle::default()
2149 );
2150 }
2151
2152 #[test]
2153 fn unapproved_path_deny_does_not_load() {
2154 let app = unapproved_path_setup(UnapprovedPathMode::Deny);
2155
2156 let asset_server = app.world().resource::<AssetServer>().clone();
2157 assert_eq!(
2158 asset_server.load::<CoolText>("../a.cool.ron"),
2159 Handle::default()
2160 );
2161 }
2162
2163 #[test]
2164 fn unapproved_path_deny_loads_with_override() {
2165 let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);
2166
2167 let asset_server = app.world().resource::<AssetServer>().clone();
2168 let handle = asset_server
2169 .load_builder()
2170 .override_unapproved()
2171 .load::<CoolText>("../a.cool.ron");
2172 assert_ne!(handle, Handle::default());
2173
2174 run_app_until(&mut app, |_| asset_server.is_loaded(&handle).then_some(()));
2176 }
2177
2178 #[test]
2179 fn unapproved_path_allow_loads() {
2180 let mut app = unapproved_path_setup(UnapprovedPathMode::Allow);
2181
2182 let asset_server = app.world().resource::<AssetServer>().clone();
2183 let handle = asset_server.load::<CoolText>("../a.cool.ron");
2184 assert_ne!(handle, Handle::default());
2185
2186 run_app_until(&mut app, |_| asset_server.is_loaded(&handle).then_some(()));
2188 }
2189
2190 #[test]
2191 fn insert_dropped_handle_returns_error() {
2192 let mut app = create_app().0;
2193
2194 app.init_asset::<TestAsset>();
2195
2196 let handle = app.world().resource::<Assets<TestAsset>>().reserve_handle();
2197 let asset_id = handle.id();
2199 drop(handle);
2200
2201 app.world_mut()
2203 .run_system_cached(Assets::<TestAsset>::track_assets)
2204 .unwrap();
2205
2206 let AssetId::Index { index, .. } = asset_id else {
2207 unreachable!("Reserving a handle always produces an index");
2208 };
2209
2210 assert_eq!(
2212 app.world_mut()
2213 .resource_mut::<Assets<TestAsset>>()
2214 .insert(asset_id, TestAsset),
2215 Err(InvalidGenerationError::Removed { index })
2216 );
2217 }
2218
2219 #[derive(TypePath)]
2225 struct GatedLoader {
2226 in_loader_sender: Sender<()>,
2227 gate_receiver: Receiver<()>,
2228 }
2229
2230 impl AssetLoader for GatedLoader {
2231 type Asset = TestAsset;
2232 type Error = std::io::Error;
2233 type Settings = ();
2234
2235 async fn load(
2236 &self,
2237 _reader: &mut dyn Reader,
2238 _settings: &Self::Settings,
2239 _load_context: &mut LoadContext<'_>,
2240 ) -> Result<Self::Asset, Self::Error> {
2241 self.in_loader_sender.send_blocking(()).unwrap();
2242 let _ = self.gate_receiver.recv().await;
2243 Ok(TestAsset)
2244 }
2245
2246 fn extensions(&self) -> &[&str] {
2247 &["ron"]
2248 }
2249 }
2250
2251 #[test]
2252 fn dropping_handle_while_loading_cancels_load() {
2253 let (mut app, dir) = create_app();
2254
2255 let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2256 let (gate_sender, gate_receiver) = async_channel::bounded(1);
2257
2258 app.init_asset::<TestAsset>()
2259 .register_asset_loader(GatedLoader {
2260 in_loader_sender,
2261 gate_receiver,
2262 });
2263
2264 let path = Path::new("abc.ron");
2265 dir.insert_asset_text(path, "blah");
2266
2267 let asset_server = app.world().resource::<AssetServer>().clone();
2268
2269 let handle = asset_server.load::<TestAsset>(path);
2271 assert!(asset_server.get_load_state(&handle).unwrap().is_loading());
2272 app.update();
2273
2274 in_loader_receiver.recv_blocking().unwrap();
2276
2277 let asset_id = handle.id();
2278 drop(handle);
2280 app.update();
2281 assert!(asset_server.get_load_state(asset_id).is_none());
2282
2283 gate_sender.send_blocking(()).unwrap();
2285 for _ in 0..10 {
2286 app.update();
2287 for message in app
2288 .world()
2289 .resource::<Messages<AssetEvent<TestAsset>>>()
2290 .iter_current_update_messages()
2291 {
2292 match message {
2293 AssetEvent::Unused { .. } => {}
2294 message => panic!("No asset events are allowed: {message:?}"),
2295 }
2296 }
2297 }
2298 }
2299
2300 #[test]
2301 fn dropping_subasset_handle_while_loading_cancels_load() {
2302 let (mut app, dir) = create_app();
2303
2304 let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2305 let (gate_sender, gate_receiver) = async_channel::bounded(1);
2306
2307 app.init_asset::<TestAsset>()
2308 .register_asset_loader(GatedLoader {
2309 in_loader_sender,
2310 gate_receiver,
2311 });
2312
2313 let path = Path::new("abc.ron");
2314 dir.insert_asset_text(path, "blah");
2315
2316 let asset_server = app.world().resource::<AssetServer>().clone();
2317
2318 let handle = asset_server.load::<TestAsset>("abc.ron#sub");
2322 assert!(asset_server.get_load_state(&handle).unwrap().is_loading());
2323 app.update();
2324
2325 in_loader_receiver.recv_blocking().unwrap();
2327
2328 let asset_id = handle.id();
2329 drop(handle);
2331 app.update();
2332 assert!(asset_server.get_load_state(asset_id).is_none());
2333
2334 gate_sender.send_blocking(()).unwrap();
2336 for _ in 0..10 {
2337 app.update();
2338 for message in app
2339 .world()
2340 .resource::<Messages<AssetEvent<TestAsset>>>()
2341 .iter_current_update_messages()
2342 {
2343 match message {
2344 AssetEvent::Unused { .. } => {}
2345 message => panic!("No asset events are allowed: {message:?}"),
2346 }
2347 }
2348 }
2349 }
2350
2351 fn create_app_with_source_event_sender() -> (App, Dir, Sender<AssetSourceEvent>) {
2354 let mut app = App::new();
2355 let dir = Dir::default();
2356 let memory_reader = MemoryAssetReader { root: dir.clone() };
2357
2358 let (sender_sender, sender_receiver) = crossbeam_channel::bounded(1);
2360
2361 struct FakeWatcher;
2362 impl AssetWatcher for FakeWatcher {}
2363
2364 app.register_asset_source(
2365 AssetSourceId::Default,
2366 AssetSourceBuilder::new(move || Box::new(memory_reader.clone())).with_watcher(
2367 move |sender| {
2368 sender_sender.send(sender).unwrap();
2369 Some(Box::new(FakeWatcher))
2370 },
2371 ),
2372 )
2373 .add_plugins((
2374 TaskPoolPlugin::default(),
2375 AssetPlugin {
2376 watch_for_changes_override: Some(true),
2377 use_asset_processor_override: Some(false),
2378 ..Default::default()
2379 },
2380 ));
2381
2382 let sender = sender_receiver.try_recv().unwrap();
2383
2384 (app, dir, sender)
2385 }
2386
2387 fn collect_asset_events<A: Asset>(world: &mut World) -> Vec<AssetEvent<A>> {
2388 world
2389 .resource_mut::<Messages<AssetEvent<A>>>()
2390 .drain()
2391 .collect()
2392 }
2393
2394 fn collect_asset_load_failed_events<A: Asset>(
2395 world: &mut World,
2396 ) -> Vec<AssetLoadFailedEvent<A>> {
2397 world
2398 .resource_mut::<Messages<AssetLoadFailedEvent<A>>>()
2399 .drain()
2400 .collect()
2401 }
2402
2403 #[test]
2404 fn reloads_asset_after_source_event() {
2405 let (mut app, dir, source_events) = create_app_with_source_event_sender();
2406 let asset_server = app.world().resource::<AssetServer>().clone();
2407
2408 dir.insert_asset_text(
2409 Path::new("abc.cool.ron"),
2410 r#"(
2411 text: "a",
2412 dependencies: [],
2413 embedded_dependencies: [],
2414 sub_texts: [],
2415)"#,
2416 );
2417
2418 app.init_asset::<CoolText>()
2419 .init_asset::<SubText>()
2420 .register_asset_loader(CoolTextLoader);
2421
2422 let handle: Handle<CoolText> = asset_server.load("abc.cool.ron");
2423 run_app_until(&mut app, |world| {
2424 let messages = collect_asset_events(world);
2425 if messages.is_empty() {
2426 return None;
2427 }
2428 assert_eq!(
2429 messages,
2430 [
2431 AssetEvent::LoadedWithDependencies { id: handle.id() },
2432 AssetEvent::Added { id: handle.id() },
2433 ]
2434 );
2435 Some(())
2436 });
2437
2438 source_events
2441 .send_blocking(AssetSourceEvent::ModifiedAsset(PathBuf::from(
2442 "abc.cool.ron",
2443 )))
2444 .unwrap();
2445
2446 run_app_until(&mut app, |world| {
2447 let messages = collect_asset_events(world);
2448 if messages.is_empty() {
2449 return None;
2450 }
2451 assert_eq!(
2452 messages,
2453 [
2454 AssetEvent::LoadedWithDependencies { id: handle.id() },
2455 AssetEvent::Modified { id: handle.id() }
2456 ]
2457 );
2458 Some(())
2459 });
2460 }
2461
2462 #[test]
2463 fn added_asset_reloads_previously_missing_asset() {
2464 let (mut app, dir, source_events) = create_app_with_source_event_sender();
2465 let asset_server = app.world().resource::<AssetServer>().clone();
2466
2467 app.init_asset::<CoolText>()
2468 .init_asset::<SubText>()
2469 .register_asset_loader(CoolTextLoader);
2470
2471 let handle: Handle<CoolText> = asset_server.load("abc.cool.ron");
2472 run_app_until(&mut app, |world| {
2473 let failed_ids = collect_asset_load_failed_events(world)
2474 .drain(..)
2475 .map(|event| event.id)
2476 .collect::<Vec<_>>();
2477 if failed_ids.is_empty() {
2478 return None;
2479 }
2480 assert_eq!(failed_ids, [handle.id()]);
2481 Some(())
2482 });
2483
2484 dir.insert_asset_text(
2487 Path::new("abc.cool.ron"),
2488 r#"(
2489 text: "a",
2490 dependencies: [],
2491 embedded_dependencies: [],
2492 sub_texts: [],
2493)"#,
2494 );
2495 source_events
2496 .send_blocking(AssetSourceEvent::AddedAsset(PathBuf::from("abc.cool.ron")))
2497 .unwrap();
2498
2499 run_app_until(&mut app, |world| {
2500 let messages = collect_asset_events(world);
2501 if messages.is_empty() {
2502 return None;
2503 }
2504 assert_eq!(
2505 messages,
2506 [
2507 AssetEvent::LoadedWithDependencies { id: handle.id() },
2508 AssetEvent::Added { id: handle.id() }
2509 ]
2510 );
2511 Some(())
2512 });
2513 }
2514
2515 #[test]
2516 fn same_asset_different_settings() {
2517 #[derive(Asset, TypePath)]
2524 struct U8Asset(u8);
2525
2526 #[derive(Serialize, Deserialize, Default)]
2527 struct U8LoaderSettings(u8);
2528
2529 #[derive(TypePath)]
2530 struct U8Loader;
2531
2532 impl AssetLoader for U8Loader {
2533 type Asset = U8Asset;
2534 type Settings = U8LoaderSettings;
2535 type Error = crate::loader::LoadDirectError;
2536
2537 async fn load(
2538 &self,
2539 _: &mut dyn Reader,
2540 settings: &Self::Settings,
2541 _: &mut LoadContext<'_>,
2542 ) -> Result<Self::Asset, Self::Error> {
2543 Ok(U8Asset(settings.0))
2544 }
2545
2546 fn extensions(&self) -> &[&str] {
2547 &["u8"]
2548 }
2549 }
2550
2551 let (mut app, dir) = create_app();
2554 dir.insert_asset(Path::new("test.u8"), &[]);
2555
2556 app.init_asset::<U8Asset>().register_asset_loader(U8Loader);
2557
2558 let asset_server = app.world().resource::<AssetServer>();
2559
2560 fn load(asset_server: &AssetServer, path: &'static str, value: u8) -> Handle<U8Asset> {
2563 asset_server
2564 .load_builder()
2565 .with_settings(move |s: &mut U8LoaderSettings| s.0 = value)
2566 .load::<U8Asset>(path)
2567 }
2568
2569 let handle_1 = load(asset_server, "test.u8", 1);
2570 let handle_2 = load(asset_server, "test.u8", 2);
2571
2572 assert_eq!(handle_1, handle_2);
2580
2581 run_app_until(&mut app, |world| {
2582 let (Some(asset_1), Some(asset_2)) = (
2583 world.resource::<Assets<U8Asset>>().get(&handle_1),
2584 world.resource::<Assets<U8Asset>>().get(&handle_2),
2585 ) else {
2586 return None;
2587 };
2588
2589 assert_eq!(asset_1.0, asset_2.0);
2598
2599 Some(())
2600 });
2601 }
2602
2603 #[test]
2604 fn loading_two_subassets_does_not_start_two_loads() {
2605 let (mut app, dir) = create_app();
2606 dir.insert_asset(Path::new("test.txt"), &[]);
2607
2608 #[derive(TypePath)]
2609 struct TwoSubassetLoader;
2610
2611 impl AssetLoader for TwoSubassetLoader {
2612 type Asset = TestAsset;
2613 type Settings = ();
2614 type Error = std::io::Error;
2615
2616 async fn load(
2617 &self,
2618 _reader: &mut dyn Reader,
2619 _settings: &Self::Settings,
2620 load_context: &mut LoadContext<'_>,
2621 ) -> Result<Self::Asset, Self::Error> {
2622 load_context.add_labeled_asset("A", TestAsset);
2623 load_context.add_labeled_asset("B", TestAsset);
2624 Ok(TestAsset)
2625 }
2626
2627 fn extensions(&self) -> &[&str] {
2628 &["txt"]
2629 }
2630 }
2631
2632 app.init_asset::<TestAsset>()
2633 .register_asset_loader(TwoSubassetLoader);
2634
2635 let asset_server = app.world().resource::<AssetServer>().clone();
2636 let _subasset_1: Handle<TestAsset> = asset_server.load("test.txt#A");
2637 let _subasset_2: Handle<TestAsset> = asset_server.load("test.txt#B");
2638
2639 app.update();
2640
2641 assert_eq!(get_started_load_count(app.world()), 2);
2646 }
2647
2648 #[derive(TypePath)]
2650 struct TrivialLoader;
2651
2652 impl AssetLoader for TrivialLoader {
2653 type Asset = TestAsset;
2654 type Settings = ();
2655 type Error = std::io::Error;
2656
2657 async fn load(
2658 &self,
2659 _reader: &mut dyn Reader,
2660 _settings: &Self::Settings,
2661 _load_context: &mut LoadContext<'_>,
2662 ) -> Result<Self::Asset, Self::Error> {
2663 Ok(TestAsset)
2664 }
2665
2666 fn extensions(&self) -> &[&str] {
2667 &["txt"]
2668 }
2669 }
2670
2671 #[test]
2672 fn get_strong_handle_prevents_reload_when_asset_still_alive() {
2673 let (mut app, dir) = create_app();
2674 dir.insert_asset(Path::new("test.txt"), &[]);
2675
2676 app.init_asset::<TestAsset>()
2677 .register_asset_loader(TrivialLoader);
2678
2679 let asset_server = app.world().resource::<AssetServer>().clone();
2680 let original_handle: Handle<TestAsset> = asset_server.load("test.txt");
2681
2682 run_app_until(&mut app, |world| {
2684 world
2685 .resource::<Assets<TestAsset>>()
2686 .get(&original_handle)
2687 .map(|_| ())
2688 });
2689
2690 assert_eq!(get_started_load_count(app.world()), 1);
2691
2692 let new_handle = app
2694 .world_mut()
2695 .resource_mut::<Assets<TestAsset>>()
2696 .get_strong_handle(original_handle.id())
2697 .unwrap();
2698
2699 drop(original_handle);
2701
2702 app.update();
2703 assert!(app
2704 .world()
2705 .resource::<Assets<TestAsset>>()
2706 .get(&new_handle)
2707 .is_some());
2708
2709 let _other_handle: Handle<TestAsset> = asset_server.load("test.txt");
2710 app.update();
2711 assert_eq!(get_started_load_count(app.world()), 2);
2718 }
2719
2720 #[test]
2721 fn immediate_nested_asset_loads_dependency() {
2722 let (mut app, dir) = create_app();
2723
2724 #[derive(Asset, TypePath)]
2726 struct DeferredNested(Handle<TestAsset>);
2727
2728 #[derive(TypePath)]
2729 struct DeferredNestedLoader;
2730
2731 impl AssetLoader for DeferredNestedLoader {
2732 type Asset = DeferredNested;
2733 type Settings = ();
2734 type Error = std::io::Error;
2735
2736 async fn load(
2737 &self,
2738 reader: &mut dyn Reader,
2739 _: &Self::Settings,
2740 load_context: &mut LoadContext<'_>,
2741 ) -> Result<Self::Asset, Self::Error> {
2742 let mut nested_path = String::new();
2743 reader.read_to_string(&mut nested_path).await?;
2744 Ok(DeferredNested(load_context.load(nested_path)))
2745 }
2746
2747 fn extensions(&self) -> &[&str] {
2748 &["defer"]
2749 }
2750 }
2751
2752 #[derive(Asset, TypePath)]
2754 struct ImmediateNested(Handle<TestAsset>);
2755
2756 #[derive(TypePath)]
2757 struct ImmediateNestedLoader;
2758
2759 impl AssetLoader for ImmediateNestedLoader {
2760 type Asset = ImmediateNested;
2761 type Settings = ();
2762 type Error = std::io::Error;
2763
2764 async fn load(
2765 &self,
2766 reader: &mut dyn Reader,
2767 _: &Self::Settings,
2768 load_context: &mut LoadContext<'_>,
2769 ) -> Result<Self::Asset, Self::Error> {
2770 let mut nested_path = String::new();
2771 reader.read_to_string(&mut nested_path).await?;
2772 let deferred_nested: LoadedAsset<DeferredNested> = load_context
2773 .load_builder()
2774 .load_value(nested_path)
2775 .await
2776 .unwrap();
2777 Ok(ImmediateNested(deferred_nested.get().0.clone()))
2778 }
2779
2780 fn extensions(&self) -> &[&str] {
2781 &["immediate"]
2782 }
2783 }
2784
2785 app.init_asset::<TestAsset>()
2786 .init_asset::<DeferredNested>()
2787 .init_asset::<ImmediateNested>()
2788 .register_asset_loader(TrivialLoader)
2789 .register_asset_loader(DeferredNestedLoader)
2790 .register_asset_loader(ImmediateNestedLoader);
2791
2792 dir.insert_asset_text(Path::new("a.immediate"), "b.defer");
2793 dir.insert_asset_text(Path::new("b.defer"), "c.txt");
2794 dir.insert_asset_text(Path::new("c.txt"), "hiya");
2795
2796 let server = app.world().resource::<AssetServer>().clone();
2797 let immediate_handle: Handle<ImmediateNested> = server.load("a.immediate");
2798
2799 run_app_until(&mut app, |world| {
2800 let immediate_assets = world.resource::<Assets<ImmediateNested>>();
2801 let immediate = immediate_assets.get(&immediate_handle)?;
2802
2803 let test_asset_handle = immediate.0.clone();
2804 world
2805 .resource::<Assets<TestAsset>>()
2806 .get(&test_asset_handle)?;
2807
2808 Some(())
2811 });
2812 }
2813
2814 pub(crate) fn read_asset_as_string(dir: &Dir, path: &Path) -> String {
2815 let bytes = dir.get_asset(path).unwrap();
2816 str::from_utf8(bytes.value()).unwrap().to_string()
2817 }
2818
2819 pub(crate) fn read_meta_as_string(dir: &Dir, path: &Path) -> String {
2820 let bytes = dir.get_metadata(path).unwrap();
2821 str::from_utf8(bytes.value()).unwrap().to_string()
2822 }
2823
2824 #[test]
2825 fn writes_default_meta_for_loader() {
2826 let (mut app, source) = create_app();
2827
2828 app.register_asset_loader(CoolTextLoader);
2829
2830 const ASSET_PATH: &str = "abc.cool.ron";
2831 source.insert_asset_text(Path::new(ASSET_PATH), "blah");
2832
2833 let asset_server = app.world().resource::<AssetServer>().clone();
2834 block_on(asset_server.write_default_loader_meta_file_for_path(ASSET_PATH)).unwrap();
2835
2836 assert_eq!(
2837 read_meta_as_string(&source, Path::new(ASSET_PATH)),
2838 r#"(
2839 meta_format_version: "1.0",
2840 asset: Load(
2841 loader: "bevy_asset::tests::CoolTextLoader",
2842 settings: (),
2843 ),
2844)"#
2845 );
2846 }
2847
2848 #[test]
2849 fn write_default_meta_does_not_overwrite() {
2850 let (mut app, source) = create_app();
2851
2852 app.register_asset_loader(CoolTextLoader);
2853
2854 const ASSET_PATH: &str = "abc.cool.ron";
2855 source.insert_asset_text(Path::new(ASSET_PATH), "blah");
2856 const META_TEXT: &str = "hey i'm walkin here!";
2857 source.insert_meta_text(Path::new(ASSET_PATH), META_TEXT);
2858
2859 let asset_server = app.world().resource::<AssetServer>().clone();
2860 assert!(matches!(
2861 block_on(asset_server.write_default_loader_meta_file_for_path(ASSET_PATH)),
2862 Err(WriteDefaultMetaError::MetaAlreadyExists)
2863 ));
2864
2865 assert_eq!(
2866 read_meta_as_string(&source, Path::new(ASSET_PATH)),
2867 META_TEXT
2868 );
2869 }
2870
2871 #[test]
2872 fn asset_dependency_is_tracked_when_not_loaded() {
2873 let (mut app, dir) = create_app();
2874
2875 #[derive(Asset, TypePath)]
2876 struct AssetWithDep {
2877 #[dependency]
2878 dep: Handle<TestAsset>,
2879 }
2880
2881 #[derive(TypePath)]
2882 struct AssetWithDepLoader;
2883
2884 impl AssetLoader for AssetWithDepLoader {
2885 type Asset = TestAsset;
2886 type Settings = ();
2887 type Error = std::io::Error;
2888
2889 async fn load(
2890 &self,
2891 _reader: &mut dyn Reader,
2892 _settings: &Self::Settings,
2893 load_context: &mut LoadContext<'_>,
2894 ) -> Result<Self::Asset, Self::Error> {
2895 let dep = load_context.load::<TestAsset>("abc.ron");
2898 load_context.add_labeled_asset("subasset", AssetWithDep { dep });
2899 Ok(TestAsset)
2900 }
2901
2902 fn extensions(&self) -> &[&str] {
2903 &["with_deps"]
2904 }
2905 }
2906
2907 dir.insert_asset_text(Path::new("abc.ron"), "");
2910 dir.insert_asset_text(Path::new("blah.with_deps"), "");
2911
2912 let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2913 let (gate_sender, gate_receiver) = async_channel::bounded(1);
2914 app.init_asset::<TestAsset>()
2915 .init_asset::<AssetWithDep>()
2916 .register_asset_loader(GatedLoader {
2917 in_loader_sender,
2918 gate_receiver,
2919 })
2920 .register_asset_loader(AssetWithDepLoader);
2921
2922 let asset_server = app.world().resource::<AssetServer>().clone();
2923 let subasset_handle: Handle<AssetWithDep> = asset_server.load("blah.with_deps#subasset");
2924
2925 run_app_until(&mut app, |_| {
2926 asset_server.is_loaded(&subasset_handle).then_some(())
2927 });
2928 assert!(!asset_server.is_loaded_with_dependencies(&subasset_handle));
2931
2932 let dep_handle: Handle<TestAsset> = app
2933 .world()
2934 .resource::<Assets<AssetWithDep>>()
2935 .get(&subasset_handle)
2936 .unwrap()
2937 .dep
2938 .clone();
2939
2940 in_loader_receiver.recv_blocking().unwrap();
2942 gate_sender.send_blocking(()).unwrap();
2943
2944 run_app_until(&mut app, |_| {
2945 asset_server.is_loaded(&dep_handle).then_some(())
2946 });
2947 assert!(asset_server.is_loaded_with_dependencies(&subasset_handle));
2949 }
2950
2951 #[derive(Debug, PartialEq, Eq)]
2953 enum TestLoadState {
2954 NotLoaded,
2955 Loading,
2956 Loaded,
2957 Failed(TestAssetLoadError),
2958 }
2959
2960 #[derive(Debug, PartialEq, Eq)]
2962 enum TestAssetLoadError {
2963 RequestedHandleTypeMismatch {
2964 requested: TypeId,
2965 actual_asset_name: &'static str,
2966 },
2967 MissingAssetLoader,
2968 AssetReaderErrorNotFound,
2969 AssetLoaderError,
2970 MissingLabel,
2971 }
2972
2973 impl From<LoadState> for TestLoadState {
2974 fn from(value: LoadState) -> Self {
2975 match value {
2976 LoadState::NotLoaded => Self::NotLoaded,
2977 LoadState::Loading => Self::Loading,
2978 LoadState::Loaded => Self::Loaded,
2979 LoadState::Failed(err) => Self::Failed((&*err).into()),
2980 }
2981 }
2982 }
2983
2984 impl From<&AssetLoadError> for TestAssetLoadError {
2985 fn from(value: &AssetLoadError) -> TestAssetLoadError {
2986 match value {
2987 AssetLoadError::RequestedHandleTypeMismatch {
2988 requested,
2989 actual_asset_name,
2990 ..
2991 } => Self::RequestedHandleTypeMismatch {
2992 requested: *requested,
2993 actual_asset_name,
2994 },
2995 AssetLoadError::MissingAssetLoader { .. } => Self::MissingAssetLoader,
2996 AssetLoadError::AssetReaderError(AssetReaderError::NotFound(_)) => {
2997 Self::AssetReaderErrorNotFound
2998 }
2999 AssetLoadError::AssetLoaderError { .. } => Self::AssetLoaderError,
3000 AssetLoadError::MissingLabel { .. } => Self::MissingLabel,
3001 _ => panic!("TestAssetLoadError's From<&AssetLoaderError> is missing a case for AssetLoadError \"{:?}\".", value),
3002 }
3003 }
3004 }
3005
3006 #[derive(Asset, TypePath)]
3008 struct LoaderlessAsset;
3009
3010 fn test_load_state<A: Asset>(
3013 label: &'static str,
3014 path: &'static str,
3015 expected_load_state: TestLoadState,
3016 ) {
3017 let (mut app, dir) = create_app();
3018
3019 app.init_asset::<CoolText>()
3020 .init_asset::<SubText>()
3021 .init_asset::<LoaderlessAsset>()
3022 .register_asset_loader(CoolTextLoader);
3023
3024 dir.insert_asset_text(
3025 Path::new("test.cool.ron"),
3026 r#"
3027(
3028 text: "test",
3029 dependencies: [],
3030 embedded_dependencies: [],
3031 sub_texts: ["subasset"],
3032)"#,
3033 );
3034
3035 dir.insert_asset_text(Path::new("malformed.cool.ron"), "MALFORMED");
3036
3037 let asset_server = app.world().resource::<AssetServer>().clone();
3038 let handle = asset_server.load::<A>(path);
3039 let mut load_state = TestLoadState::NotLoaded;
3040
3041 for _ in 0..LARGE_ITERATION_COUNT {
3042 app.update();
3043 load_state = asset_server.get_load_state(&handle).unwrap().into();
3044 if load_state == expected_load_state {
3045 break;
3046 }
3047 }
3048
3049 assert!(
3050 load_state == expected_load_state,
3051 "For test \"{}\", expected {:?} but got {:?}.",
3052 label,
3053 expected_load_state,
3054 load_state,
3055 );
3056 }
3057
3058 #[test]
3061 fn load_failure() {
3062 test_load_state::<CoolText>("root asset exists", "test.cool.ron", TestLoadState::Loaded);
3063
3064 test_load_state::<SubText>(
3065 "sub-asset exists",
3066 "test.cool.ron#subasset",
3067 TestLoadState::Loaded,
3068 );
3069
3070 test_load_state::<CoolText>(
3071 "root asset does not exist",
3072 "does_not_exist.cool.ron",
3073 TestLoadState::Failed(TestAssetLoadError::AssetReaderErrorNotFound),
3074 );
3075
3076 test_load_state::<CoolText>(
3077 "sub-asset of root asset that does not exist",
3078 "does_not_exist.cool.ron#subasset",
3079 TestLoadState::Failed(TestAssetLoadError::AssetReaderErrorNotFound),
3080 );
3081
3082 test_load_state::<SubText>(
3083 "sub-asset does not exist",
3084 "test.cool.ron#does_not_exist",
3085 TestLoadState::Failed(TestAssetLoadError::MissingLabel),
3086 );
3087
3088 test_load_state::<CoolText>(
3089 "sub-asset is not requested type",
3090 "test.cool.ron#subasset",
3091 TestLoadState::Failed(TestAssetLoadError::RequestedHandleTypeMismatch {
3092 requested: TypeId::of::<CoolText>(),
3093 actual_asset_name: "bevy_asset::tests::SubText",
3094 }),
3095 );
3096
3097 test_load_state::<CoolText>(
3098 "malformed root asset",
3099 "malformed.cool.ron",
3100 TestLoadState::Failed(TestAssetLoadError::AssetLoaderError),
3101 );
3102
3103 test_load_state::<CoolText>(
3104 "sub-asset of malformed root asset",
3105 "malformed.cool.ron#subasset",
3106 TestLoadState::Failed(TestAssetLoadError::AssetLoaderError),
3107 );
3108
3109 test_load_state::<LoaderlessAsset>(
3110 "root asset has no loader",
3111 "loaderless",
3112 TestLoadState::Failed(TestAssetLoadError::MissingAssetLoader),
3113 );
3114 }
3115
3116 #[test]
3117 fn load_empty_path_returns_default() {
3118 let mut app = create_app().0;
3119
3120 app.init_asset::<TestAsset>()
3123 .register_asset_loader(TrivialLoader);
3124
3125 const TYPE_ID: TypeId = TypeId::of::<TestAsset>();
3126
3127 fn boring_settings(_: &mut ()) {}
3128
3129 let asset_server = app.world().resource::<AssetServer>().clone();
3130
3131 for path in ["", "no_path://#WithALabel"] {
3132 assert_eq!(asset_server.load(path), Handle::<TestAsset>::default());
3134 assert_eq!(
3135 asset_server.load_builder().with_guard(()).load(path),
3136 Handle::<TestAsset>::default()
3137 );
3138 assert_eq!(
3139 asset_server
3140 .load_builder()
3141 .with_guard(())
3142 .override_unapproved()
3143 .load(path),
3144 Handle::<TestAsset>::default()
3145 );
3146 assert_eq!(
3147 asset_server
3148 .load_builder()
3149 .with_guard(())
3150 .with_settings(boring_settings)
3151 .load(path),
3152 Handle::<TestAsset>::default()
3153 );
3154 assert_eq!(
3155 asset_server.load_builder().load_erased(TYPE_ID, path),
3156 Handle::<TestAsset>::default()
3157 );
3158 assert_eq!(
3159 asset_server.load_builder().override_unapproved().load(path),
3160 Handle::<TestAsset>::default()
3161 );
3162 assert_eq!(
3163 asset_server.load_builder().load_untyped(path),
3164 Handle::default()
3165 );
3166 assert!(matches!(
3167 block_on(asset_server.load_builder().load_untyped_async(path)),
3168 Err(AssetLoadError::EmptyPath(reported_path)) if AssetPath::from(path) == reported_path
3169 ));
3170 assert_eq!(
3171 asset_server
3172 .load_builder()
3173 .with_settings(|_: &mut ()| {})
3174 .load(path),
3175 Handle::<TestAsset>::default()
3176 );
3177 assert_eq!(
3178 asset_server
3179 .load_builder()
3180 .with_settings(|_: &mut ()| {})
3181 .override_unapproved()
3182 .load(path),
3183 Handle::<TestAsset>::default()
3184 );
3185 }
3186 }
3187
3188 #[test]
3189 fn resource_are_dependencies_loaded() {
3190 let (mut app, dir) = create_app();
3191 dir.insert_asset_text(Path::new("abc.txt"), "");
3192 dir.insert_asset_text(Path::new("def.txt"), "");
3193 dir.insert_asset_text(Path::new("ghi.txt"), "");
3194
3195 app.init_asset::<TestAsset>()
3196 .register_asset_loader(TrivialLoader);
3197
3198 let asset_server = app.world().resource::<AssetServer>().clone();
3199
3200 #[derive(Resource, VisitAssetDependencies)]
3201 struct MyAssetHolder {
3202 #[dependency]
3203 abc: Handle<TestAsset>,
3204 #[dependency]
3205 def: Handle<TestAsset>,
3206 #[dependency]
3207 ghi: Handle<TestAsset>,
3208 }
3209
3210 app.insert_resource(MyAssetHolder {
3211 abc: asset_server.load("abc.txt"),
3212 def: asset_server.load("def.txt"),
3213 ghi: asset_server.load("ghi.txt"),
3214 });
3215
3216 assert!(!asset_server.are_dependencies_loaded(app.world().resource::<MyAssetHolder>()));
3217 assert!(
3218 !asset_server.are_direct_dependencies_loaded(app.world().resource::<MyAssetHolder>())
3219 );
3220
3221 run_app_until(&mut app, |world| {
3222 asset_server
3223 .are_dependencies_loaded(world.resource::<MyAssetHolder>())
3224 .then_some(())
3225 });
3226 assert!(
3227 asset_server.are_direct_dependencies_loaded(app.world().resource::<MyAssetHolder>())
3228 );
3229 }
3230
3231 #[test]
3232 fn hot_reload_folder() {
3233 let (mut app, dir, event_sender) = create_app_with_source_event_sender();
3234
3235 app.init_asset::<CoolText>()
3236 .init_asset::<SubText>()
3237 .register_asset_loader(CoolTextLoader);
3238
3239 let abc_path = Path::new("dir/abc.cool.ron");
3240 let def_path = Path::new("dir/def.cool.ron");
3241 dir.insert_asset_text(abc_path, &serialize_as_cool_text("abc"));
3242 dir.insert_asset_text(def_path, &serialize_as_cool_text("def"));
3243
3244 let asset_server = app.world().resource::<AssetServer>().clone();
3245
3246 let folder_handle = asset_server.load_folder("dir");
3247 run_app_until(&mut app, |_| {
3248 asset_server
3249 .is_loaded_with_dependencies(&folder_handle)
3250 .then_some(())
3251 });
3252
3253 let folder = app
3254 .world()
3255 .resource::<Assets<LoadedFolder>>()
3256 .get(&folder_handle)
3257 .unwrap();
3258 assert_eq!(folder.handles.len(), 2);
3259 let mut handles = folder
3260 .handles
3261 .iter()
3262 .cloned()
3263 .map(UntypedHandle::typed::<CoolText>)
3264 .collect::<Vec<_>>();
3265 handles.sort_by_key(|handle| handle.path().unwrap().path().to_path_buf());
3267
3268 let abc_handle = handles[0].clone();
3269 let def_handle = handles[1].clone();
3270
3271 let cool_texts = app.world().resource::<Assets<CoolText>>();
3272 assert_eq!(cool_texts.get(&abc_handle).unwrap().text, "abc");
3273 assert_eq!(cool_texts.get(&def_handle).unwrap().text, "def");
3274
3275 app.world_mut()
3277 .resource_mut::<Messages<AssetEvent<LoadedFolder>>>()
3278 .clear();
3279
3280 let ghi_path = Path::new("dir/ghi.cool.ron");
3282 dir.insert_asset_text(ghi_path, &serialize_as_cool_text("ghi"));
3283 event_sender
3284 .send_blocking(AssetSourceEvent::AddedAsset(ghi_path.to_path_buf()))
3285 .unwrap();
3286
3287 run_app_until(&mut app, |world| {
3288 for event in world
3289 .resource_mut::<Messages<AssetEvent<LoadedFolder>>>()
3290 .drain()
3291 {
3292 if let AssetEvent::LoadedWithDependencies { id } = event
3293 && id == folder_handle.id()
3294 {
3295 return Some(());
3296 }
3297 }
3298 None
3299 });
3300
3301 let folder = app
3302 .world()
3303 .resource::<Assets<LoadedFolder>>()
3304 .get(&folder_handle)
3305 .unwrap();
3306 assert_eq!(folder.handles.len(), 3);
3307 let mut handles = folder
3308 .handles
3309 .iter()
3310 .cloned()
3311 .map(UntypedHandle::typed::<CoolText>)
3312 .collect::<Vec<_>>();
3313 handles.sort_by_key(|handle| handle.path().unwrap().path().to_path_buf());
3315
3316 let new_abc_handle = handles[0].clone();
3317 let new_def_handle = handles[1].clone();
3318 let new_ghi_handle = handles[2].clone();
3319
3320 assert_eq!(new_abc_handle, abc_handle);
3321 assert_eq!(new_def_handle, def_handle);
3322
3323 let cool_texts = app.world().resource::<Assets<CoolText>>();
3324 assert_eq!(cool_texts.get(&new_abc_handle).unwrap().text, "abc");
3325 assert_eq!(cool_texts.get(&new_def_handle).unwrap().text, "def");
3326 assert_eq!(cool_texts.get(&new_ghi_handle).unwrap().text, "ghi");
3327 }
3328}