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