1#![allow(missing_docs)]
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4#![doc(
5 html_logo_url = "https://bevyengine.org/assets/icon.png",
6 html_favicon_url = "https://bevyengine.org/assets/icon.png"
7)]
8
9pub mod io;
10pub mod meta;
11pub mod processor;
12pub mod saver;
13pub mod transformer;
14
15pub mod prelude {
16 #[doc(hidden)]
17 pub use crate::{
18 Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets,
19 DirectAssetAccessExt, Handle, UntypedHandle,
20 };
21}
22
23mod assets;
24mod direct_access_ext;
25mod event;
26mod folder;
27mod handle;
28mod id;
29mod loader;
30mod loader_builders;
31mod path;
32mod reflect;
33mod server;
34
35pub use assets::*;
36pub use bevy_asset_macros::Asset;
37pub use direct_access_ext::DirectAssetAccessExt;
38pub use event::*;
39pub use folder::*;
40pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
41pub use handle::*;
42pub use id::*;
43pub use loader::*;
44pub use loader_builders::{
45 DirectNestedLoader, NestedLoader, UntypedDirectNestedLoader, UntypedNestedLoader,
46};
47pub use path::*;
48pub use reflect::*;
49pub use server::*;
50
51pub use ron;
53
54use crate::{
55 io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId},
56 processor::{AssetProcessor, Process},
57};
58use bevy_app::{App, Last, Plugin, PreUpdate};
59use bevy_ecs::{
60 reflect::AppTypeRegistry,
61 schedule::{IntoSystemConfigs, IntoSystemSetConfigs, SystemSet},
62 world::FromWorld,
63};
64use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath};
65use bevy_utils::{tracing::error, HashSet};
66use std::{any::TypeId, sync::Arc};
67
68#[cfg(all(feature = "file_watcher", not(feature = "multi_threaded")))]
69compile_error!(
70 "The \"file_watcher\" feature for hot reloading requires the \
71 \"multi_threaded\" feature to be functional.\n\
72 Consider either disabling the \"file_watcher\" feature or enabling \"multi_threaded\""
73);
74
75pub struct AssetPlugin {
83 pub file_path: String,
85 pub processed_file_path: String,
87 pub watch_for_changes_override: Option<bool>,
94 pub mode: AssetMode,
96 pub meta_check: AssetMetaCheck,
98}
99
100#[derive(Debug)]
101pub enum AssetMode {
102 Unprocessed,
107 Processed,
122}
123
124#[derive(Debug, Default, Clone)]
127pub enum AssetMetaCheck {
128 #[default]
130 Always,
131 Paths(HashSet<AssetPath<'static>>),
133 Never,
135}
136
137impl Default for AssetPlugin {
138 fn default() -> Self {
139 Self {
140 mode: AssetMode::Unprocessed,
141 file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(),
142 processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
143 watch_for_changes_override: None,
144 meta_check: AssetMetaCheck::default(),
145 }
146 }
147}
148
149impl AssetPlugin {
150 const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets";
151 const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default";
154}
155
156impl Plugin for AssetPlugin {
157 fn build(&self, app: &mut App) {
158 let embedded = EmbeddedAssetRegistry::default();
159 {
160 let mut sources = app
161 .world_mut()
162 .get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
163 sources.init_default_source(
164 &self.file_path,
165 (!matches!(self.mode, AssetMode::Unprocessed))
166 .then_some(self.processed_file_path.as_str()),
167 );
168 embedded.register_source(&mut sources);
169 }
170 {
171 let mut watch = cfg!(feature = "watch");
172 if let Some(watch_override) = self.watch_for_changes_override {
173 watch = watch_override;
174 }
175 match self.mode {
176 AssetMode::Unprocessed => {
177 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
178 let sources = builders.build_sources(watch, false);
179
180 app.insert_resource(AssetServer::new_with_meta_check(
181 sources,
182 AssetServerMode::Unprocessed,
183 self.meta_check.clone(),
184 watch,
185 ));
186 }
187 AssetMode::Processed => {
188 #[cfg(feature = "asset_processor")]
189 {
190 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
191 let processor = AssetProcessor::new(&mut builders);
192 let mut sources = builders.build_sources(false, watch);
193 sources.gate_on_processor(processor.data.clone());
194 app.insert_resource(AssetServer::new_with_loaders(
196 sources,
197 processor.server().data.loaders.clone(),
198 AssetServerMode::Processed,
199 AssetMetaCheck::Always,
200 watch,
201 ))
202 .insert_resource(processor)
203 .add_systems(bevy_app::Startup, AssetProcessor::start);
204 }
205 #[cfg(not(feature = "asset_processor"))]
206 {
207 let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
208 let sources = builders.build_sources(false, watch);
209 app.insert_resource(AssetServer::new_with_meta_check(
210 sources,
211 AssetServerMode::Processed,
212 AssetMetaCheck::Always,
213 watch,
214 ));
215 }
216 }
217 }
218 }
219 app.insert_resource(embedded)
220 .init_asset::<LoadedFolder>()
221 .init_asset::<LoadedUntypedAsset>()
222 .init_asset::<()>()
223 .add_event::<UntypedAssetLoadFailedEvent>()
224 .configure_sets(PreUpdate, TrackAssets.after(handle_internal_asset_events))
225 .add_systems(PreUpdate, handle_internal_asset_events)
226 .register_type::<AssetPath>();
227 }
228}
229
230#[diagnostic::on_unimplemented(
231 message = "`{Self}` is not an `Asset`",
232 label = "invalid `Asset`",
233 note = "consider annotating `{Self}` with `#[derive(Asset)]`"
234)]
235pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {}
236
237pub trait VisitAssetDependencies {
238 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId));
239}
240
241impl<A: Asset> VisitAssetDependencies for Handle<A> {
242 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
243 visit(self.id().untyped());
244 }
245}
246
247impl<A: Asset> VisitAssetDependencies for Option<Handle<A>> {
248 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
249 if let Some(handle) = self {
250 visit(handle.id().untyped());
251 }
252 }
253}
254
255impl VisitAssetDependencies for UntypedHandle {
256 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
257 visit(self.id());
258 }
259}
260
261impl VisitAssetDependencies for Option<UntypedHandle> {
262 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
263 if let Some(handle) = self {
264 visit(handle.id());
265 }
266 }
267}
268
269impl<A: Asset> VisitAssetDependencies for Vec<Handle<A>> {
270 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
271 for dependency in self {
272 visit(dependency.id().untyped());
273 }
274 }
275}
276
277impl VisitAssetDependencies for Vec<UntypedHandle> {
278 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
279 for dependency in self {
280 visit(dependency.id());
281 }
282 }
283}
284
285pub trait AssetApp {
287 fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self;
289 fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self;
291 fn register_asset_source(
296 &mut self,
297 id: impl Into<AssetSourceId<'static>>,
298 source: AssetSourceBuilder,
299 ) -> &mut Self;
300 fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self;
302 fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self;
304 fn init_asset<A: Asset>(&mut self) -> &mut Self;
312 fn register_asset_reflect<A>(&mut self) -> &mut Self
317 where
318 A: Asset + Reflect + FromReflect + GetTypeRegistration;
319 fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self;
322}
323
324impl AssetApp for App {
325 fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
326 self.world()
327 .resource::<AssetServer>()
328 .register_loader(loader);
329 self
330 }
331
332 fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self {
333 if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
334 asset_processor.register_processor(processor);
335 }
336 self
337 }
338
339 fn register_asset_source(
340 &mut self,
341 id: impl Into<AssetSourceId<'static>>,
342 source: AssetSourceBuilder,
343 ) -> &mut Self {
344 let id = id.into();
345 if self.world().get_resource::<AssetServer>().is_some() {
346 error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id);
347 }
348
349 {
350 let mut sources = self
351 .world_mut()
352 .get_resource_or_insert_with(AssetSourceBuilders::default);
353 sources.insert(id, source);
354 }
355
356 self
357 }
358
359 fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self {
360 if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
361 asset_processor.set_default_processor::<P>(extension);
362 }
363 self
364 }
365
366 fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self {
367 let loader = L::from_world(self.world_mut());
368 self.register_asset_loader(loader)
369 }
370
371 fn init_asset<A: Asset>(&mut self) -> &mut Self {
372 let assets = Assets::<A>::default();
373 self.world()
374 .resource::<AssetServer>()
375 .register_asset(&assets);
376 if self.world().contains_resource::<AssetProcessor>() {
377 let processor = self.world().resource::<AssetProcessor>();
378 processor
382 .server()
383 .register_handle_provider(AssetHandleProvider::new(
384 TypeId::of::<A>(),
385 Arc::new(AssetIndexAllocator::default()),
386 ));
387 }
388 self.insert_resource(assets)
389 .allow_ambiguous_resource::<Assets<A>>()
390 .add_event::<AssetEvent<A>>()
391 .add_event::<AssetLoadFailedEvent<A>>()
392 .register_type::<Handle<A>>()
393 .add_systems(
394 Last,
395 Assets::<A>::asset_events
396 .run_if(Assets::<A>::asset_events_condition)
397 .in_set(AssetEvents),
398 )
399 .add_systems(PreUpdate, Assets::<A>::track_assets.in_set(TrackAssets))
400 }
401
402 fn register_asset_reflect<A>(&mut self) -> &mut Self
403 where
404 A: Asset + Reflect + FromReflect + GetTypeRegistration,
405 {
406 let type_registry = self.world().resource::<AppTypeRegistry>();
407 {
408 let mut type_registry = type_registry.write();
409
410 type_registry.register::<A>();
411 type_registry.register::<Handle<A>>();
412 type_registry.register_type_data::<A, ReflectAsset>();
413 type_registry.register_type_data::<Handle<A>, ReflectHandle>();
414 }
415
416 self
417 }
418
419 fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self {
420 self.world_mut()
421 .resource_mut::<AssetServer>()
422 .preregister_loader::<L>(extensions);
423 self
424 }
425}
426
427#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
429pub struct TrackAssets;
430
431#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
435pub struct AssetEvents;
436
437#[cfg(test)]
438mod tests {
439 use crate::{
440 self as bevy_asset,
441 folder::LoadedFolder,
442 handle::Handle,
443 io::{
444 gated::{GateOpener, GatedReader},
445 memory::{Dir, MemoryAssetReader},
446 AssetReader, AssetReaderError, AssetSource, AssetSourceId, Reader,
447 },
448 loader::{AssetLoader, LoadContext},
449 Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
450 AssetPlugin, AssetServer, Assets, DependencyLoadState, LoadState,
451 RecursiveDependencyLoadState,
452 };
453 use bevy_app::{App, Update};
454 use bevy_core::TaskPoolPlugin;
455 use bevy_ecs::prelude::*;
456 use bevy_ecs::{
457 event::ManualEventReader,
458 schedule::{LogLevel, ScheduleBuildSettings},
459 };
460 use bevy_log::LogPlugin;
461 use bevy_reflect::TypePath;
462 use bevy_utils::{Duration, HashMap};
463 use futures_lite::AsyncReadExt;
464 use serde::{Deserialize, Serialize};
465 use std::{path::Path, sync::Arc};
466 use thiserror::Error;
467
468 #[derive(Asset, TypePath, Debug, Default)]
469 pub struct CoolText {
470 pub text: String,
471 pub embedded: String,
472 #[dependency]
473 pub dependencies: Vec<Handle<CoolText>>,
474 #[dependency]
475 pub sub_texts: Vec<Handle<SubText>>,
476 }
477
478 #[derive(Asset, TypePath, Debug)]
479 pub struct SubText {
480 text: String,
481 }
482
483 #[derive(Serialize, Deserialize)]
484 pub struct CoolTextRon {
485 text: String,
486 dependencies: Vec<String>,
487 embedded_dependencies: Vec<String>,
488 sub_texts: Vec<String>,
489 }
490
491 #[derive(Default)]
492 pub struct CoolTextLoader;
493
494 #[derive(Error, Debug)]
495 pub enum CoolTextLoaderError {
496 #[error("Could not load dependency: {dependency}")]
497 CannotLoadDependency { dependency: AssetPath<'static> },
498 #[error("A RON error occurred during loading")]
499 RonSpannedError(#[from] ron::error::SpannedError),
500 #[error("An IO error occurred during loading")]
501 Io(#[from] std::io::Error),
502 }
503
504 impl AssetLoader for CoolTextLoader {
505 type Asset = CoolText;
506
507 type Settings = ();
508
509 type Error = CoolTextLoaderError;
510
511 async fn load<'a>(
512 &'a self,
513 reader: &'a mut Reader<'_>,
514 _settings: &'a Self::Settings,
515 load_context: &'a mut LoadContext<'_>,
516 ) -> Result<Self::Asset, Self::Error> {
517 let mut bytes = Vec::new();
518 reader.read_to_end(&mut bytes).await?;
519 let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
520 let mut embedded = String::new();
521 for dep in ron.embedded_dependencies {
522 let loaded = load_context
523 .loader()
524 .direct()
525 .load::<CoolText>(&dep)
526 .await
527 .map_err(|_| Self::Error::CannotLoadDependency {
528 dependency: dep.into(),
529 })?;
530 let cool = loaded.get();
531 embedded.push_str(&cool.text);
532 }
533 Ok(CoolText {
534 text: ron.text,
535 embedded,
536 dependencies: ron
537 .dependencies
538 .iter()
539 .map(|p| load_context.load(p))
540 .collect(),
541 sub_texts: ron
542 .sub_texts
543 .drain(..)
544 .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text }))
545 .collect(),
546 })
547 }
548
549 fn extensions(&self) -> &[&str] {
550 &["cool.ron"]
551 }
552 }
553
554 #[derive(Default, Clone)]
556 pub struct UnstableMemoryAssetReader {
557 pub attempt_counters: Arc<std::sync::Mutex<HashMap<Box<Path>, usize>>>,
558 pub load_delay: Duration,
559 memory_reader: MemoryAssetReader,
560 failure_count: usize,
561 }
562
563 impl UnstableMemoryAssetReader {
564 pub fn new(root: Dir, failure_count: usize) -> Self {
565 Self {
566 load_delay: Duration::from_millis(10),
567 memory_reader: MemoryAssetReader { root },
568 attempt_counters: Default::default(),
569 failure_count,
570 }
571 }
572 }
573
574 impl AssetReader for UnstableMemoryAssetReader {
575 async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
576 self.memory_reader.is_directory(path).await
577 }
578 async fn read_directory<'a>(
579 &'a self,
580 path: &'a Path,
581 ) -> Result<Box<bevy_asset::io::PathStream>, AssetReaderError> {
582 self.memory_reader.read_directory(path).await
583 }
584 async fn read_meta<'a>(
585 &'a self,
586 path: &'a Path,
587 ) -> Result<Box<bevy_asset::io::Reader<'a>>, AssetReaderError> {
588 self.memory_reader.read_meta(path).await
589 }
590 async fn read<'a>(
591 &'a self,
592 path: &'a Path,
593 ) -> Result<Box<bevy_asset::io::Reader<'a>>, bevy_asset::io::AssetReaderError> {
594 let attempt_number = {
595 let mut attempt_counters = self.attempt_counters.lock().unwrap();
596 if let Some(existing) = attempt_counters.get_mut(path) {
597 *existing += 1;
598 *existing
599 } else {
600 attempt_counters.insert(path.into(), 1);
601 1
602 }
603 };
604
605 if attempt_number <= self.failure_count {
606 let io_error = std::io::Error::new(
607 std::io::ErrorKind::ConnectionRefused,
608 format!(
609 "Simulated failure {attempt_number} of {}",
610 self.failure_count
611 ),
612 );
613 let wait = self.load_delay;
614 return async move {
615 std::thread::sleep(wait);
616 Err(AssetReaderError::Io(io_error.into()))
617 }
618 .await;
619 }
620
621 self.memory_reader.read(path).await
622 }
623 }
624
625 fn test_app(dir: Dir) -> (App, GateOpener) {
626 let mut app = App::new();
627 let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir });
628 app.register_asset_source(
629 AssetSourceId::Default,
630 AssetSource::build().with_reader(move || Box::new(gated_memory_reader.clone())),
631 )
632 .add_plugins((
633 TaskPoolPlugin::default(),
634 LogPlugin::default(),
635 AssetPlugin::default(),
636 ));
637 (app, gate_opener)
638 }
639
640 pub fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) {
641 for _ in 0..LARGE_ITERATION_COUNT {
642 app.update();
643 if predicate(app.world_mut()).is_some() {
644 return;
645 }
646 }
647
648 panic!("Ran out of loops to return `Some` from `predicate`");
649 }
650
651 const LARGE_ITERATION_COUNT: usize = 10000;
652
653 fn get<A: Asset>(world: &World, id: AssetId<A>) -> Option<&A> {
654 world.resource::<Assets<A>>().get(id)
655 }
656
657 #[derive(Resource, Default)]
658 struct StoredEvents(Vec<AssetEvent<CoolText>>);
659
660 fn store_asset_events(
661 mut reader: EventReader<AssetEvent<CoolText>>,
662 mut storage: ResMut<StoredEvents>,
663 ) {
664 storage.0.extend(reader.read().cloned());
665 }
666
667 #[test]
668 fn load_dependencies() {
669 #[cfg(not(feature = "multi_threaded"))]
671 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
672
673 let dir = Dir::default();
674
675 let a_path = "a.cool.ron";
676 let a_ron = r#"
677(
678 text: "a",
679 dependencies: [
680 "foo/b.cool.ron",
681 "c.cool.ron",
682 ],
683 embedded_dependencies: [],
684 sub_texts: [],
685)"#;
686 let b_path = "foo/b.cool.ron";
687 let b_ron = r#"
688(
689 text: "b",
690 dependencies: [],
691 embedded_dependencies: [],
692 sub_texts: [],
693)"#;
694
695 let c_path = "c.cool.ron";
696 let c_ron = r#"
697(
698 text: "c",
699 dependencies: [
700 "d.cool.ron",
701 ],
702 embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"],
703 sub_texts: ["hello"],
704)"#;
705
706 let d_path = "d.cool.ron";
707 let d_ron = r#"
708(
709 text: "d",
710 dependencies: [],
711 embedded_dependencies: [],
712 sub_texts: [],
713)"#;
714
715 dir.insert_asset_text(Path::new(a_path), a_ron);
716 dir.insert_asset_text(Path::new(b_path), b_ron);
717 dir.insert_asset_text(Path::new(c_path), c_ron);
718 dir.insert_asset_text(Path::new(d_path), d_ron);
719
720 #[derive(Resource)]
721 struct IdResults {
722 b_id: AssetId<CoolText>,
723 c_id: AssetId<CoolText>,
724 d_id: AssetId<CoolText>,
725 }
726
727 let (mut app, gate_opener) = test_app(dir);
728 app.init_asset::<CoolText>()
729 .init_asset::<SubText>()
730 .init_resource::<StoredEvents>()
731 .register_asset_loader(CoolTextLoader)
732 .add_systems(Update, store_asset_events);
733 let asset_server = app.world().resource::<AssetServer>().clone();
734 let handle: Handle<CoolText> = asset_server.load(a_path);
735 let a_id = handle.id();
736 let entity = app.world_mut().spawn(handle).id();
737 app.update();
738 {
739 let a_text = get::<CoolText>(app.world(), a_id);
740 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
741 assert!(a_text.is_none(), "a's asset should not exist yet");
742 assert_eq!(a_load, LoadState::Loading, "a should still be loading");
743 assert_eq!(
744 a_deps,
745 DependencyLoadState::Loading,
746 "a deps should still be loading"
747 );
748 assert_eq!(
749 a_rec_deps,
750 RecursiveDependencyLoadState::Loading,
751 "a recursive deps should still be loading"
752 );
753 }
754
755 gate_opener.open(a_path);
758 run_app_until(&mut app, |world| {
759 let a_text = get::<CoolText>(world, a_id)?;
760 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
761 assert_eq!(a_text.text, "a");
762 assert_eq!(a_text.dependencies.len(), 2);
763 assert_eq!(a_load, LoadState::Loaded, "a is loaded");
764 assert_eq!(a_deps, DependencyLoadState::Loading);
765 assert_eq!(a_rec_deps, RecursiveDependencyLoadState::Loading);
766
767 let b_id = a_text.dependencies[0].id();
768 let b_text = get::<CoolText>(world, b_id);
769 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
770 assert!(b_text.is_none(), "b component should not exist yet");
771 assert_eq!(b_load, LoadState::Loading);
772 assert_eq!(b_deps, DependencyLoadState::Loading);
773 assert_eq!(b_rec_deps, RecursiveDependencyLoadState::Loading);
774
775 let c_id = a_text.dependencies[1].id();
776 let c_text = get::<CoolText>(world, c_id);
777 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
778 assert!(c_text.is_none(), "c component should not exist yet");
779 assert_eq!(c_load, LoadState::Loading);
780 assert_eq!(c_deps, DependencyLoadState::Loading);
781 assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Loading);
782 Some(())
783 });
784
785 gate_opener.open(b_path);
788 run_app_until(&mut app, |world| {
789 let a_text = get::<CoolText>(world, a_id)?;
790 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
791 assert_eq!(a_text.text, "a");
792 assert_eq!(a_text.dependencies.len(), 2);
793 assert_eq!(a_load, LoadState::Loaded);
794 assert_eq!(a_deps, DependencyLoadState::Loading);
795 assert_eq!(a_rec_deps, RecursiveDependencyLoadState::Loading);
796
797 let b_id = a_text.dependencies[0].id();
798 let b_text = get::<CoolText>(world, b_id)?;
799 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
800 assert_eq!(b_text.text, "b");
801 assert_eq!(b_load, LoadState::Loaded);
802 assert_eq!(b_deps, DependencyLoadState::Loaded);
803 assert_eq!(b_rec_deps, RecursiveDependencyLoadState::Loaded);
804
805 let c_id = a_text.dependencies[1].id();
806 let c_text = get::<CoolText>(world, c_id);
807 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
808 assert!(c_text.is_none(), "c component should not exist yet");
809 assert_eq!(c_load, LoadState::Loading);
810 assert_eq!(c_deps, DependencyLoadState::Loading);
811 assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Loading);
812 Some(())
813 });
814
815 gate_opener.open(c_path);
818
819 gate_opener.open(a_path);
821 gate_opener.open(b_path);
822 run_app_until(&mut app, |world| {
823 let a_text = get::<CoolText>(world, a_id)?;
824 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
825 assert_eq!(a_text.text, "a");
826 assert_eq!(a_text.embedded, "");
827 assert_eq!(a_text.dependencies.len(), 2);
828 assert_eq!(a_load, LoadState::Loaded);
829
830 let b_id = a_text.dependencies[0].id();
831 let b_text = get::<CoolText>(world, b_id)?;
832 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
833 assert_eq!(b_text.text, "b");
834 assert_eq!(b_text.embedded, "");
835 assert_eq!(b_load, LoadState::Loaded);
836 assert_eq!(b_deps, DependencyLoadState::Loaded);
837 assert_eq!(b_rec_deps, RecursiveDependencyLoadState::Loaded);
838
839 let c_id = a_text.dependencies[1].id();
840 let c_text = get::<CoolText>(world, c_id)?;
841 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
842 assert_eq!(c_text.text, "c");
843 assert_eq!(c_text.embedded, "ab");
844 assert_eq!(c_load, LoadState::Loaded);
845 assert_eq!(
846 c_deps,
847 DependencyLoadState::Loading,
848 "c deps should not be loaded yet because d has not loaded"
849 );
850 assert_eq!(
851 c_rec_deps,
852 RecursiveDependencyLoadState::Loading,
853 "c rec deps should not be loaded yet because d has not loaded"
854 );
855
856 let sub_text_id = c_text.sub_texts[0].id();
857 let sub_text = get::<SubText>(world, sub_text_id)
858 .expect("subtext should exist if c exists. it came from the same loader");
859 assert_eq!(sub_text.text, "hello");
860 let (sub_text_load, sub_text_deps, sub_text_rec_deps) =
861 asset_server.get_load_states(sub_text_id).unwrap();
862 assert_eq!(sub_text_load, LoadState::Loaded);
863 assert_eq!(sub_text_deps, DependencyLoadState::Loaded);
864 assert_eq!(sub_text_rec_deps, RecursiveDependencyLoadState::Loaded);
865
866 let d_id = c_text.dependencies[0].id();
867 let d_text = get::<CoolText>(world, d_id);
868 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
869 assert!(d_text.is_none(), "d component should not exist yet");
870 assert_eq!(d_load, LoadState::Loading);
871 assert_eq!(d_deps, DependencyLoadState::Loading);
872 assert_eq!(d_rec_deps, RecursiveDependencyLoadState::Loading);
873
874 assert_eq!(
875 a_deps,
876 DependencyLoadState::Loaded,
877 "If c has been loaded, the a deps should all be considered loaded"
878 );
879 assert_eq!(
880 a_rec_deps,
881 RecursiveDependencyLoadState::Loading,
882 "d is not loaded, so a's recursive deps should still be loading"
883 );
884 world.insert_resource(IdResults { b_id, c_id, d_id });
885 Some(())
886 });
887
888 gate_opener.open(d_path);
889 run_app_until(&mut app, |world| {
890 let a_text = get::<CoolText>(world, a_id)?;
891 let (_a_load, _a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
892 let c_id = a_text.dependencies[1].id();
893 let c_text = get::<CoolText>(world, c_id)?;
894 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
895 assert_eq!(c_text.text, "c");
896 assert_eq!(c_text.embedded, "ab");
897
898 let d_id = c_text.dependencies[0].id();
899 let d_text = get::<CoolText>(world, d_id)?;
900 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
901 assert_eq!(d_text.text, "d");
902 assert_eq!(d_text.embedded, "");
903
904 assert_eq!(c_load, LoadState::Loaded);
905 assert_eq!(c_deps, DependencyLoadState::Loaded);
906 assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Loaded);
907
908 assert_eq!(d_load, LoadState::Loaded);
909 assert_eq!(d_deps, DependencyLoadState::Loaded);
910 assert_eq!(d_rec_deps, RecursiveDependencyLoadState::Loaded);
911
912 assert_eq!(
913 a_rec_deps,
914 RecursiveDependencyLoadState::Loaded,
915 "d is loaded, so a's recursive deps should be loaded"
916 );
917 Some(())
918 });
919
920 {
921 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
922 let a = texts.get_mut(a_id).unwrap();
923 a.text = "Changed".to_string();
924 }
925
926 app.world_mut().despawn(entity);
927 app.update();
928 assert_eq!(
929 app.world().resource::<Assets<CoolText>>().len(),
930 0,
931 "CoolText asset entities should be despawned when no more handles exist"
932 );
933 app.update();
934 assert_eq!(
936 app.world().resource::<Assets<SubText>>().len(),
937 0,
938 "SubText asset entities should be despawned when no more handles exist"
939 );
940 let events = app.world_mut().remove_resource::<StoredEvents>().unwrap();
941 let id_results = app.world_mut().remove_resource::<IdResults>().unwrap();
942 let expected_events = vec![
943 AssetEvent::Added { id: a_id },
944 AssetEvent::LoadedWithDependencies {
945 id: id_results.b_id,
946 },
947 AssetEvent::Added {
948 id: id_results.b_id,
949 },
950 AssetEvent::Added {
951 id: id_results.c_id,
952 },
953 AssetEvent::LoadedWithDependencies {
954 id: id_results.d_id,
955 },
956 AssetEvent::LoadedWithDependencies {
957 id: id_results.c_id,
958 },
959 AssetEvent::LoadedWithDependencies { id: a_id },
960 AssetEvent::Added {
961 id: id_results.d_id,
962 },
963 AssetEvent::Modified { id: a_id },
964 AssetEvent::Unused { id: a_id },
965 AssetEvent::Removed { id: a_id },
966 AssetEvent::Unused {
967 id: id_results.b_id,
968 },
969 AssetEvent::Removed {
970 id: id_results.b_id,
971 },
972 AssetEvent::Unused {
973 id: id_results.c_id,
974 },
975 AssetEvent::Removed {
976 id: id_results.c_id,
977 },
978 AssetEvent::Unused {
979 id: id_results.d_id,
980 },
981 AssetEvent::Removed {
982 id: id_results.d_id,
983 },
984 ];
985 assert_eq!(events.0, expected_events);
986 }
987
988 #[test]
989 fn failure_load_states() {
990 #[cfg(not(feature = "multi_threaded"))]
992 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
993
994 let dir = Dir::default();
995
996 let a_path = "a.cool.ron";
997 let a_ron = r#"
998(
999 text: "a",
1000 dependencies: [
1001 "b.cool.ron",
1002 "c.cool.ron",
1003 ],
1004 embedded_dependencies: [],
1005 sub_texts: []
1006)"#;
1007 let b_path = "b.cool.ron";
1008 let b_ron = r#"
1009(
1010 text: "b",
1011 dependencies: [],
1012 embedded_dependencies: [],
1013 sub_texts: []
1014)"#;
1015
1016 let c_path = "c.cool.ron";
1017 let c_ron = r#"
1018(
1019 text: "c",
1020 dependencies: [
1021 "d.cool.ron",
1022 ],
1023 embedded_dependencies: [],
1024 sub_texts: []
1025)"#;
1026
1027 let d_path = "d.cool.ron";
1028 let d_ron = r#"
1029(
1030 text: "d",
1031 dependencies: [],
1032 OH NO THIS ASSET IS MALFORMED
1033 embedded_dependencies: [],
1034 sub_texts: []
1035)"#;
1036
1037 dir.insert_asset_text(Path::new(a_path), a_ron);
1038 dir.insert_asset_text(Path::new(b_path), b_ron);
1039 dir.insert_asset_text(Path::new(c_path), c_ron);
1040 dir.insert_asset_text(Path::new(d_path), d_ron);
1041
1042 let (mut app, gate_opener) = test_app(dir);
1043 app.init_asset::<CoolText>()
1044 .register_asset_loader(CoolTextLoader);
1045 let asset_server = app.world().resource::<AssetServer>().clone();
1046 let handle: Handle<CoolText> = asset_server.load(a_path);
1047 let a_id = handle.id();
1048 {
1049 let other_handle: Handle<CoolText> = asset_server.load(a_path);
1050 assert_eq!(
1051 other_handle, handle,
1052 "handles from consecutive load calls should be equal"
1053 );
1054 assert_eq!(
1055 other_handle.id(),
1056 handle.id(),
1057 "handle ids from consecutive load calls should be equal"
1058 );
1059 }
1060
1061 app.world_mut().spawn(handle);
1062 gate_opener.open(a_path);
1063 gate_opener.open(b_path);
1064 gate_opener.open(c_path);
1065 gate_opener.open(d_path);
1066
1067 run_app_until(&mut app, |world| {
1068 let a_text = get::<CoolText>(world, a_id)?;
1069 let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1070
1071 let b_id = a_text.dependencies[0].id();
1072 let b_text = get::<CoolText>(world, b_id)?;
1073 let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1074
1075 let c_id = a_text.dependencies[1].id();
1076 let c_text = get::<CoolText>(world, c_id)?;
1077 let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1078
1079 let d_id = c_text.dependencies[0].id();
1080 let d_text = get::<CoolText>(world, d_id);
1081 let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1082 if !matches!(d_load, LoadState::Failed(_)) {
1083 return None;
1085 }
1086
1087 assert!(d_text.is_none());
1088 assert!(matches!(d_load, LoadState::Failed(_)));
1089 assert_eq!(d_deps, DependencyLoadState::Failed);
1090 assert_eq!(d_rec_deps, RecursiveDependencyLoadState::Failed);
1091
1092 assert_eq!(a_text.text, "a");
1093 assert_eq!(a_load, LoadState::Loaded);
1094 assert_eq!(a_deps, DependencyLoadState::Loaded);
1095 assert_eq!(a_rec_deps, RecursiveDependencyLoadState::Failed);
1096
1097 assert_eq!(b_text.text, "b");
1098 assert_eq!(b_load, LoadState::Loaded);
1099 assert_eq!(b_deps, DependencyLoadState::Loaded);
1100 assert_eq!(b_rec_deps, RecursiveDependencyLoadState::Loaded);
1101
1102 assert_eq!(c_text.text, "c");
1103 assert_eq!(c_load, LoadState::Loaded);
1104 assert_eq!(c_deps, DependencyLoadState::Failed);
1105 assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Failed);
1106
1107 Some(())
1108 });
1109 }
1110
1111 const SIMPLE_TEXT: &str = r#"
1112(
1113 text: "dep",
1114 dependencies: [],
1115 embedded_dependencies: [],
1116 sub_texts: [],
1117)"#;
1118 #[test]
1119 fn keep_gotten_strong_handles() {
1120 let dir = Dir::default();
1121 dir.insert_asset_text(Path::new("dep.cool.ron"), SIMPLE_TEXT);
1122
1123 let (mut app, _) = test_app(dir);
1124 app.init_asset::<CoolText>()
1125 .init_asset::<SubText>()
1126 .init_resource::<StoredEvents>()
1127 .register_asset_loader(CoolTextLoader)
1128 .add_systems(Update, store_asset_events);
1129
1130 let id = {
1131 let handle = {
1132 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1133 let handle = texts.add(CoolText::default());
1134 texts.get_strong_handle(handle.id()).unwrap()
1135 };
1136
1137 app.update();
1138
1139 {
1140 let text = app.world().resource::<Assets<CoolText>>().get(&handle);
1141 assert!(text.is_some());
1142 }
1143 handle.id()
1144 };
1145 app.update();
1147 assert!(
1148 app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1149 "asset has no handles, so it should have been dropped last update"
1150 );
1151 }
1152
1153 #[test]
1154 fn manual_asset_management() {
1155 #[cfg(not(feature = "multi_threaded"))]
1157 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1158
1159 let dir = Dir::default();
1160 let dep_path = "dep.cool.ron";
1161
1162 dir.insert_asset_text(Path::new(dep_path), SIMPLE_TEXT);
1163
1164 let (mut app, gate_opener) = test_app(dir);
1165 app.init_asset::<CoolText>()
1166 .init_asset::<SubText>()
1167 .init_resource::<StoredEvents>()
1168 .register_asset_loader(CoolTextLoader)
1169 .add_systems(Update, store_asset_events);
1170
1171 let hello = "hello".to_string();
1172 let empty = "".to_string();
1173
1174 let id = {
1175 let handle = {
1176 let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1177 texts.add(CoolText {
1178 text: hello.clone(),
1179 embedded: empty.clone(),
1180 dependencies: vec![],
1181 sub_texts: Vec::new(),
1182 })
1183 };
1184
1185 app.update();
1186
1187 {
1188 let text = app
1189 .world()
1190 .resource::<Assets<CoolText>>()
1191 .get(&handle)
1192 .unwrap();
1193 assert_eq!(text.text, hello);
1194 }
1195 handle.id()
1196 };
1197 app.update();
1199 assert!(
1200 app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1201 "asset has no handles, so it should have been dropped last update"
1202 );
1203 app.update();
1205 let events = std::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1206 let expected_events = vec![
1207 AssetEvent::Added { id },
1208 AssetEvent::Unused { id },
1209 AssetEvent::Removed { id },
1210 ];
1211 assert_eq!(events, expected_events);
1212
1213 let dep_handle = app.world().resource::<AssetServer>().load(dep_path);
1214 let a = CoolText {
1215 text: "a".to_string(),
1216 embedded: empty,
1217 dependencies: vec![dep_handle.clone()],
1219 sub_texts: Vec::new(),
1220 };
1221 let a_handle = app.world().resource::<AssetServer>().load_asset(a);
1222 app.update();
1223 app.update();
1225
1226 let events = std::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1227 let expected_events = vec![AssetEvent::Added { id: a_handle.id() }];
1228 assert_eq!(events, expected_events);
1229
1230 gate_opener.open(dep_path);
1231 loop {
1232 app.update();
1233 let events = std::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1234 if events.is_empty() {
1235 continue;
1236 }
1237 let expected_events = vec![
1238 AssetEvent::LoadedWithDependencies {
1239 id: dep_handle.id(),
1240 },
1241 AssetEvent::LoadedWithDependencies { id: a_handle.id() },
1242 ];
1243 assert_eq!(events, expected_events);
1244 break;
1245 }
1246 app.update();
1247 let events = std::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1248 let expected_events = vec![AssetEvent::Added {
1249 id: dep_handle.id(),
1250 }];
1251 assert_eq!(events, expected_events);
1252 }
1253
1254 #[test]
1255 fn load_folder() {
1256 #[cfg(not(feature = "multi_threaded"))]
1258 panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1259
1260 let dir = Dir::default();
1261
1262 let a_path = "text/a.cool.ron";
1263 let a_ron = r#"
1264(
1265 text: "a",
1266 dependencies: [
1267 "b.cool.ron",
1268 ],
1269 embedded_dependencies: [],
1270 sub_texts: [],
1271)"#;
1272 let b_path = "b.cool.ron";
1273 let b_ron = r#"
1274(
1275 text: "b",
1276 dependencies: [],
1277 embedded_dependencies: [],
1278 sub_texts: [],
1279)"#;
1280
1281 let c_path = "text/c.cool.ron";
1282 let c_ron = r#"
1283(
1284 text: "c",
1285 dependencies: [
1286 ],
1287 embedded_dependencies: [],
1288 sub_texts: [],
1289)"#;
1290 dir.insert_asset_text(Path::new(a_path), a_ron);
1291 dir.insert_asset_text(Path::new(b_path), b_ron);
1292 dir.insert_asset_text(Path::new(c_path), c_ron);
1293
1294 let (mut app, gate_opener) = test_app(dir);
1295 app.init_asset::<CoolText>()
1296 .init_asset::<SubText>()
1297 .register_asset_loader(CoolTextLoader);
1298 let asset_server = app.world().resource::<AssetServer>().clone();
1299 let handle: Handle<LoadedFolder> = asset_server.load_folder("text");
1300 gate_opener.open(a_path);
1301 gate_opener.open(b_path);
1302 gate_opener.open(c_path);
1303
1304 let mut reader = ManualEventReader::default();
1305 run_app_until(&mut app, |world| {
1306 let events = world.resource::<Events<AssetEvent<LoadedFolder>>>();
1307 let asset_server = world.resource::<AssetServer>();
1308 let loaded_folders = world.resource::<Assets<LoadedFolder>>();
1309 let cool_texts = world.resource::<Assets<CoolText>>();
1310 for event in reader.read(events) {
1311 if let AssetEvent::LoadedWithDependencies { id } = event {
1312 if *id == handle.id() {
1313 let loaded_folder = loaded_folders.get(&handle).unwrap();
1314 let a_handle: Handle<CoolText> =
1315 asset_server.get_handle("text/a.cool.ron").unwrap();
1316 let c_handle: Handle<CoolText> =
1317 asset_server.get_handle("text/c.cool.ron").unwrap();
1318
1319 let mut found_a = false;
1320 let mut found_c = false;
1321 for asset_handle in &loaded_folder.handles {
1322 if asset_handle.id() == a_handle.id().untyped() {
1323 found_a = true;
1324 } else if asset_handle.id() == c_handle.id().untyped() {
1325 found_c = true;
1326 }
1327 }
1328 assert!(found_a);
1329 assert!(found_c);
1330 assert_eq!(loaded_folder.handles.len(), 2);
1331
1332 let a_text = cool_texts.get(&a_handle).unwrap();
1333 let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();
1334 let c_text = cool_texts.get(&c_handle).unwrap();
1335
1336 assert_eq!("a", a_text.text);
1337 assert_eq!("b", b_text.text);
1338 assert_eq!("c", c_text.text);
1339
1340 return Some(());
1341 }
1342 }
1343 }
1344 None
1345 });
1346 }
1347
1348 #[test]
1350 fn load_error_events() {
1351 #[derive(Resource, Default)]
1352 struct ErrorTracker {
1353 tick: u64,
1354 failures: usize,
1355 queued_retries: Vec<(AssetPath<'static>, AssetId<CoolText>, u64)>,
1356 finished_asset: Option<AssetId<CoolText>>,
1357 }
1358
1359 fn asset_event_handler(
1360 mut events: EventReader<AssetEvent<CoolText>>,
1361 mut tracker: ResMut<ErrorTracker>,
1362 ) {
1363 for event in events.read() {
1364 if let AssetEvent::LoadedWithDependencies { id } = event {
1365 tracker.finished_asset = Some(*id);
1366 }
1367 }
1368 }
1369
1370 fn asset_load_error_event_handler(
1371 server: Res<AssetServer>,
1372 mut errors: EventReader<AssetLoadFailedEvent<CoolText>>,
1373 mut tracker: ResMut<ErrorTracker>,
1374 ) {
1375 tracker.tick += 1;
1377
1378 let now = tracker.tick;
1380 tracker
1381 .queued_retries
1382 .retain(|(path, old_id, retry_after)| {
1383 if now > *retry_after {
1384 let new_handle = server.load::<CoolText>(path);
1385 assert_eq!(&new_handle.id(), old_id);
1386 false
1387 } else {
1388 true
1389 }
1390 });
1391
1392 for error in errors.read() {
1394 let (load_state, _, _) = server.get_load_states(error.id).unwrap();
1395 assert!(matches!(load_state, LoadState::Failed(_)));
1396 assert_eq!(*error.path.source(), AssetSourceId::Name("unstable".into()));
1397 match &error.error {
1398 AssetLoadError::AssetReaderError(read_error) => match read_error {
1399 AssetReaderError::Io(_) => {
1400 tracker.failures += 1;
1401 if tracker.failures <= 2 {
1402 tracker.queued_retries.push((
1404 error.path.clone(),
1405 error.id,
1406 now + 10,
1407 ));
1408 } else {
1409 panic!(
1410 "Unexpected failure #{} (expected only 2)",
1411 tracker.failures
1412 );
1413 }
1414 }
1415 _ => panic!("Unexpected error type {:?}", read_error),
1416 },
1417 _ => panic!("Unexpected error type {:?}", error.error),
1418 }
1419 }
1420 }
1421
1422 let a_path = "text/a.cool.ron";
1423 let a_ron = r#"
1424(
1425 text: "a",
1426 dependencies: [],
1427 embedded_dependencies: [],
1428 sub_texts: [],
1429)"#;
1430
1431 let dir = Dir::default();
1432 dir.insert_asset_text(Path::new(a_path), a_ron);
1433 let unstable_reader = UnstableMemoryAssetReader::new(dir, 2);
1434
1435 let mut app = App::new();
1436 app.register_asset_source(
1437 "unstable",
1438 AssetSource::build().with_reader(move || Box::new(unstable_reader.clone())),
1439 )
1440 .add_plugins((
1441 TaskPoolPlugin::default(),
1442 LogPlugin::default(),
1443 AssetPlugin::default(),
1444 ))
1445 .init_asset::<CoolText>()
1446 .register_asset_loader(CoolTextLoader)
1447 .init_resource::<ErrorTracker>()
1448 .add_systems(
1449 Update,
1450 (asset_event_handler, asset_load_error_event_handler).chain(),
1451 );
1452
1453 let asset_server = app.world().resource::<AssetServer>().clone();
1454 let a_path = format!("unstable://{a_path}");
1455 let a_handle: Handle<CoolText> = asset_server.load(a_path);
1456 let a_id = a_handle.id();
1457
1458 app.world_mut().spawn(a_handle);
1459
1460 run_app_until(&mut app, |world| {
1461 let tracker = world.resource::<ErrorTracker>();
1462 match tracker.finished_asset {
1463 Some(asset_id) => {
1464 assert_eq!(asset_id, a_id);
1465 let assets = world.resource::<Assets<CoolText>>();
1466 let result = assets.get(asset_id).unwrap();
1467 assert_eq!(result.text, "a");
1468 Some(())
1469 }
1470 None => None,
1471 }
1472 });
1473 }
1474
1475 #[test]
1476 fn ignore_system_ambiguities_on_assets() {
1477 let mut app = App::new();
1478 app.add_plugins(AssetPlugin::default())
1479 .init_asset::<CoolText>();
1480
1481 fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1482 app.add_systems(Update, (uses_assets, uses_assets));
1483 app.edit_schedule(Update, |s| {
1484 s.set_build_settings(ScheduleBuildSettings {
1485 ambiguity_detection: LogLevel::Error,
1486 ..Default::default()
1487 });
1488 });
1489
1490 app.world_mut().run_schedule(Update);
1492 }
1493
1494 #[derive(Asset, TypePath)]
1496 pub struct TestAsset;
1497
1498 #[allow(dead_code)]
1499 #[derive(Asset, TypePath)]
1500 pub enum EnumTestAsset {
1501 Unnamed(#[dependency] Handle<TestAsset>),
1502 Named {
1503 #[dependency]
1504 handle: Handle<TestAsset>,
1505 #[dependency]
1506 vec_handles: Vec<Handle<TestAsset>>,
1507 #[dependency]
1508 embedded: TestAsset,
1509 },
1510 StructStyle(#[dependency] TestAsset),
1511 Empty,
1512 }
1513
1514 #[allow(dead_code)]
1515 #[derive(Asset, TypePath)]
1516 pub struct StructTestAsset {
1517 #[dependency]
1518 handle: Handle<TestAsset>,
1519 #[dependency]
1520 embedded: TestAsset,
1521 }
1522
1523 #[allow(dead_code)]
1524 #[derive(Asset, TypePath)]
1525 pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);
1526}