Skip to main content

bevy_asset/
lib.rs

1//! In the context of game development, an "asset" is a piece of content that is loaded from disk and displayed in the game.
2//! Typically, these are authored by artists and designers (in contrast to code),
3//! are relatively large in size, and include everything from textures and models to sounds and music to levels and scripts.
4//!
5//! This presents two main challenges:
6//! - Assets take up a lot of memory; simply storing a copy for each instance of an asset in the game would be prohibitively expensive.
7//! - Loading assets from disk is slow, and can cause long load times and delays.
8//!
9//! These problems play into each other, for if assets are expensive to store in memory,
10//! then larger game worlds will need to load them from disk as needed, ideally without a loading screen.
11//!
12//! As is common in Rust, non-blocking asset loading is done using `async`, with background tasks used to load assets while the game is running.
13//! Bevy coordinates these tasks using the [`AssetServer`] resource, storing each loaded asset in a strongly-typed [`Assets<T>`] collection (also a resource).
14//! [`Handle`]s serve as an id-based reference to entries in the [`Assets`] collection, allowing them to be cheaply shared between systems,
15//! and providing a way to initialize objects (generally entities) before the required assets are loaded.
16//! In short: [`Handle`]s are not the assets themselves, they just tell how to look them up!
17//!
18//! ## Loading assets
19//!
20//! The [`AssetServer`] is the main entry point for loading assets.
21//! Typically, you'll use the [`AssetServer::load`] method to load an asset from disk, which returns a [`Handle`].
22//! Note that this method does not attempt to reload the asset if it has already been loaded: as long as at least one handle has not been dropped,
23//! calling [`AssetServer::load`] on the same path will return the same handle.
24//! The handle that's returned can be used to instantiate various [`Component`]s that require asset data to function,
25//! which will then be spawned into the world as part of an entity.
26//!
27//! To avoid assets "popping" into existence, you may want to check that all of the required assets are loaded before transitioning to a new scene.
28//! This can be done by checking the [`LoadState`] of the asset handle using [`AssetServer::is_loaded_with_dependencies`],
29//! which will be `true` when the asset is ready to use.
30//!
31//! Keep track of what you're waiting on by using a [`HashSet`] of asset handles or similar data structure,
32//! which iterate over and poll in your update loop, and transition to the new scene once all assets are loaded.
33//! Bevy's built-in states system can be very helpful for this!
34//!
35//! # Modifying entities that use assets
36//!
37//! If we later want to change the asset data a given component uses (such as changing an entity's material), we have three options:
38//!
39//! 1. Change the handle stored on the responsible component to the handle of a different asset
40//! 2. Despawn the entity and spawn a new one with the new asset data.
41//! 3. Use the [`Assets`] collection to directly modify the current handle's asset data
42//!
43//! The first option is the most common: just query for the component that holds the handle, and mutate it, pointing to the new asset.
44//! Check how the handle was passed in to the entity when it was spawned: if a mesh-related component required a handle to a mesh asset,
45//! you'll need to find that component via a query and change the handle to the new mesh asset.
46//! This is so commonly done that you should think about strategies for how to store and swap handles in your game.
47//!
48//! The second option is the simplest, but can be slow if done frequently,
49//! and can lead to frustrating bugs as references to the old entity (such as what is targeting it) and other data on the entity are lost.
50//! Generally, this isn't a great strategy.
51//!
52//! The third option has different semantics: rather than modifying the asset data for a single entity, it modifies the asset data for *all* entities using this handle.
53//! While this might be what you want, it generally isn't!
54//!
55//! # Hot reloading assets
56//!
57//! Bevy supports asset hot reloading, allowing you to change assets on disk and see the changes reflected in your game without restarting.
58//! When enabled, any changes to the underlying asset file will be detected by the [`AssetServer`], which will then reload the asset,
59//! mutating the asset data in the [`Assets`] collection and thus updating all entities that use the asset.
60//! While it has limited uses in published games, it is very useful when developing, as it allows you to iterate quickly.
61//!
62//! To enable asset hot reloading on desktop platforms, enable `bevy`'s `file_watcher` cargo feature.
63//! To toggle it at runtime, you can use the `watch_for_changes_override` field in the [`AssetPlugin`] to enable or disable hot reloading.
64//!
65//! # Procedural asset creation
66//!
67//! Not all assets are loaded from disk: some are generated at runtime, such as procedural materials, sounds or even levels.
68//! After creating an item of a type that implements [`Asset`], you can add it to the [`Assets`] collection using [`Assets::add`].
69//! Once in the asset collection, this data can be operated on like any other asset.
70//!
71//! Note that, unlike assets loaded from a file path, no general mechanism currently exists to deduplicate procedural assets:
72//! calling [`Assets::add`] for every entity that needs the asset will create a new copy of the asset for each entity,
73//! quickly consuming memory.
74//!
75//! ## Handles and reference counting
76//!
77//! [`Handle`] (or their untyped counterpart [`UntypedHandle`]) are used to reference assets in the [`Assets`] collection,
78//! and are the primary way to interact with assets in Bevy.
79//! As a user, you'll be working with handles a lot!
80//!
81//! The most important thing to know about handles is that they are reference counted: when you clone a handle, you're incrementing a reference count.
82//! When the object holding the handle is dropped (generally because an entity was despawned), the reference count is decremented.
83//! When the reference count hits zero, the asset it references is removed from the [`Assets`] collection.
84//!
85//! This reference counting is a simple, largely automatic way to avoid holding onto memory for game objects that are no longer in use.
86//! However, it can lead to surprising behavior if you're not careful!
87//!
88//! There are two categories of problems to watch out for:
89//! - never dropping a handle, causing the asset to never be removed from memory
90//! - dropping a handle too early, causing the asset to be removed from memory while it's still in use
91//!
92//! The first problem is less critical for beginners, as for tiny games, you can often get away with simply storing all of the assets in memory at once,
93//! and loading them all at the start of the game.
94//! As your game grows, you'll need to be more careful about when you load and unload assets,
95//! segmenting them by level or area, and loading them on-demand.
96//! This problem generally arises when handles are stored in a persistent "collection" or "manifest" of possible objects (generally in a resource),
97//! which is convenient for easy access and zero-latency spawning, but can result in high but stable memory usage.
98//!
99//! The second problem is more concerning, and looks like your models or textures suddenly disappearing from the game.
100//! Debugging reveals that the *entities* are still there, but nothing is rendering!
101//! This is because the assets were removed from memory while they were still in use.
102//! You were probably too aggressive with the use of weak handles (which don't increment the reference count of the asset): think through the lifecycle of your assets carefully!
103//! As soon as an asset is loaded, you must ensure that at least one strong handle is held to it until all matching entities are out of sight of the player.
104//!
105//! # Asset dependencies
106//!
107//! Some assets depend on other assets to be loaded before they can be loaded themselves.
108//! For example, a 3D model might require both textures and meshes to be loaded,
109//! or a 2D level might require a tileset to be loaded.
110//!
111//! The assets that are required to load another asset are called "dependencies".
112//! An asset is only considered fully loaded when it and all of its dependencies are loaded.
113//! Asset dependencies can be declared when implementing the [`Asset`] trait by implementing the [`VisitAssetDependencies`] trait,
114//! and the `#[dependency]` attribute can be used to automatically derive this implementation.
115//!
116//! # Custom asset types
117//!
118//! While Bevy comes with implementations for a large number of common game-oriented asset types (often behind off-by-default feature flags!),
119//! implementing a custom asset type can be useful when dealing with unusual, game-specific, or proprietary formats.
120//!
121//! Defining a new asset type is as simple as implementing the [`Asset`] trait.
122//! This requires [`TypePath`] for metadata about the asset type,
123//! and [`VisitAssetDependencies`] to track asset dependencies.
124//! In simple cases, you can derive [`Asset`] and [`Reflect`] and be done with it: the required supertraits will be implemented for you.
125//!
126//! With a new asset type in place, we now need to figure out how to load it.
127//! While [`AssetReader`](io::AssetReader) describes strategies to read asset bytes from various sources,
128//! [`AssetLoader`] is the trait that actually turns those into your desired in-memory format.
129//! Generally, (only) [`AssetLoader`] needs to be implemented for custom assets, as the [`AssetReader`](io::AssetReader) implementations are provided by Bevy.
130//!
131//! However, [`AssetLoader`] shouldn't be implemented for your asset type directly: instead, this is implemented for a "loader" type
132//! that can store settings and any additional data required to load your asset, while your asset type is used as the [`AssetLoader::Asset`] associated type.
133//! As the trait documentation explains, this allows various [`AssetLoader::Settings`] to be used to configure the loader.
134//!
135//! After the loader is implemented, it needs to be registered with the [`AssetServer`] using [`App::register_asset_loader`](AssetApp::register_asset_loader).
136//! Once your asset type is loaded, you can use it in your game like any other asset type!
137//!
138
139#![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
150// Required to make proc macros work in bevy itself.
151extern 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
160/// The asset prelude.
161///
162/// This includes the most common types in this crate, re-exported for your convenience.
163pub 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
228/// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetSource`],
229/// which can be something like a filesystem, a network, etc.
230///
231/// Supports flexible "modes", such as [`AssetMode::Processed`] and
232/// [`AssetMode::Unprocessed`] that enable using the asset workflow that best suits your project.
233///
234/// [`AssetSource`]: io::AssetSource
235pub struct AssetPlugin {
236    /// The default file path to use (relative to the project root) for unprocessed assets.
237    pub file_path: String,
238    /// The default file path to use (relative to the project root) for processed assets.
239    pub processed_file_path: String,
240    /// If set, will override the default "watch for changes" setting. By default "watch for changes" will be `false` unless
241    /// the `watch` cargo feature is set. `watch` can be enabled manually, or it will be automatically enabled if a specific watcher
242    /// like `file_watcher` is enabled.
243    ///
244    /// Most use cases should leave this set to [`None`] and enable a specific watcher feature such as `file_watcher` to enable
245    /// watching for dev-scenarios.
246    pub watch_for_changes_override: Option<bool>,
247    /// If set, will override the default "use asset processor" setting. By default "use asset
248    /// processor" will be `false` unless the `asset_processor` cargo feature is set.
249    ///
250    /// Most use cases should leave this set to [`None`] and enable the `asset_processor` cargo
251    /// feature.
252    pub use_asset_processor_override: Option<bool>,
253    /// The [`AssetMode`] to use for this server.
254    pub mode: AssetMode,
255    /// How/If asset meta files should be checked.
256    pub meta_check: AssetMetaCheck,
257    /// How to handle load requests of files that are outside the approved directories.
258    ///
259    /// Approved folders are [`AssetPlugin::file_path`] and the folder of each
260    /// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
261    pub unapproved_path_mode: UnapprovedPathMode,
262}
263
264/// Determines how to react to attempts to load assets not inside the approved folders.
265///
266/// Approved folders are [`AssetPlugin::file_path`] and the folder of each
267/// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
268///
269/// It is strongly discouraged to use [`Allow`](UnapprovedPathMode::Allow) if your
270/// app will include scripts or modding support, as it could allow arbitrary file
271/// access for malicious code.
272///
273/// The default value is [`Forbid`](UnapprovedPathMode::Forbid).
274///
275/// See [`AssetPath::is_unapproved`](crate::AssetPath::is_unapproved)
276#[derive(Clone, Copy, Default)]
277pub enum UnapprovedPathMode {
278    /// Unapproved asset loading is allowed. This is strongly discouraged.
279    Allow,
280    /// Fails to load any asset that is unapproved, unless [`LoadBuilder::override_unapproved`] is
281    /// used.
282    Deny,
283    /// Fails to load any asset that is unapproved.
284    #[default]
285    Forbid,
286}
287
288/// Controls whether or not assets are pre-processed before being loaded.
289///
290/// This setting is controlled by setting [`AssetPlugin::mode`].
291///
292/// When building on web, asset preprocessing can cause problems due to the lack of filesystem access.
293/// See [bevy#10157](https://github.com/bevyengine/bevy/issues/10157) for context.
294#[derive(Debug)]
295pub enum AssetMode {
296    /// Loads assets from their [`AssetSource`]'s default [`AssetReader`] without any "preprocessing".
297    ///
298    /// [`AssetReader`]: io::AssetReader
299    /// [`AssetSource`]: io::AssetSource
300    Unprocessed,
301    /// Assets will be "pre-processed". This enables assets to be imported / converted / optimized ahead of time.
302    ///
303    /// Assets will be read from their unprocessed [`AssetSource`] (defaults to the `assets` folder),
304    /// processed according to their [`AssetMeta`], and written to their processed [`AssetSource`] (defaults to the `imported_assets/Default` folder).
305    ///
306    /// By default, this assumes the processor _has already been run_. It will load assets from their final processed [`AssetReader`].
307    ///
308    /// When developing an app, you should enable the `asset_processor` cargo feature, which will run the asset processor at startup. This should generally
309    /// be used in combination with the `file_watcher` cargo feature, which enables hot-reloading of assets that have changed. When both features are enabled,
310    /// changes to "original/source assets" will be detected, the asset will be re-processed, and then the final processed asset will be hot-reloaded in the app.
311    ///
312    /// [`AssetMeta`]: meta::AssetMeta
313    /// [`AssetSource`]: io::AssetSource
314    /// [`AssetReader`]: io::AssetReader
315    Processed,
316}
317
318/// Configures how / if meta files will be checked. If an asset's meta file is not checked, the default meta for the asset
319/// will be used.
320#[derive(Debug, Default, Clone)]
321pub enum AssetMetaCheck {
322    /// Always check if assets have meta files. If the meta does not exist, the default meta will be used.
323    #[default]
324    Always,
325    /// Only look up meta files for the provided paths. The default meta will be used for any paths not contained in this set.
326    Paths(HashSet<AssetPath<'static>>),
327    /// Never check if assets have meta files and always use the default meta. If meta files exist, they will be ignored and the default meta will be used.
328    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    /// NOTE: this is in the Default sub-folder to make this forward compatible with "import profiles"
348    /// and to allow us to put the "processor transaction log" at `imported_assets/log`
349    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                        // the main asset server shares loaders with the processor asset server
391                        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            // `handle_internal_asset_events` requires the use of `&mut World`,
425            // and as a result has ambiguous system ordering with all other systems in `PreUpdate`.
426            // This is virtually never a real problem: asset loading is async and so anything that interacts directly with it
427            // needs to be robust to stochastic delays anyways.
428            .add_systems(
429                PreUpdate,
430                (
431                    handle_internal_asset_events.ambiguous_with_all(),
432                    // TODO: Remove the run condition and use `If` once
433                    // https://github.com/bevyengine/bevy/issues/21549 is resolved.
434                    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/// Declares that this type is an asset,
443/// which can be loaded and managed by the [`AssetServer`] and stored in [`Assets`] collections.
444///
445/// Generally, assets are large, complex, and/or expensive to load from disk, and are often authored by artists or designers.
446///
447/// [`TypePath`] is largely used for diagnostic purposes, and should almost always be implemented by deriving [`Reflect`] on your type.
448/// [`VisitAssetDependencies`] is used to track asset dependencies, and an implementation is automatically generated when deriving [`Asset`].
449#[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
456/// A trait for components that can be used as asset identifiers, e.g. handle wrappers.
457pub trait AsAssetId: Component {
458    /// The underlying asset type.
459    type Asset: Asset;
460
461    /// Retrieves the asset id from this component.
462    fn as_asset_id(&self) -> AssetId<Self::Asset>;
463}
464
465/// This trait defines how to visit the dependencies of an asset.
466/// For example, a 3D model might require both textures and meshes to be loaded.
467///
468/// Note that this trait is automatically implemented when deriving [`Asset`].
469pub trait VisitAssetDependencies {
470    /// Apply the `visit` closure to every asset dependency.
471    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
570/// Adds asset-related builder methods to [`App`].
571pub trait AssetApp {
572    /// Registers the given `loader` in the [`App`]'s [`AssetServer`].
573    fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self;
574    /// Registers the given `processor` in the [`App`]'s [`AssetProcessor`].
575    fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self;
576    /// Registers the given [`AssetSourceBuilder`] with the given `id`.
577    ///
578    /// Note that asset sources must be registered before adding [`AssetPlugin`] to your application,
579    /// since registered asset sources are built at that point and not after.
580    fn register_asset_source(
581        &mut self,
582        id: impl Into<AssetSourceId<'static>>,
583        source: AssetSourceBuilder,
584    ) -> &mut Self;
585    /// Sets the default asset processor for the given `extension`.
586    fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self;
587    /// Initializes the given loader in the [`App`]'s [`AssetServer`].
588    fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self;
589    /// Initializes the given [`Asset`] in the [`App`] by:
590    /// * Registering the [`Asset`] in the [`AssetServer`]
591    /// * Initializing the [`AssetEvent`] resource for the [`Asset`]
592    /// * Adding other relevant systems and resources for the [`Asset`]
593    /// * Ignoring schedule ambiguities in [`Assets`] resource. Any time a system takes
594    ///   mutable access to this resource this causes a conflict, but they rarely actually
595    ///   modify the same underlying asset.
596    fn init_asset<A: Asset>(&mut self) -> &mut Self;
597    /// Registers the asset type `T` using `[App::register]`,
598    /// and adds [`ReflectAsset`] type data to `T` and [`ReflectHandle`] type data to [`Handle<T>`] in the type registry.
599    ///
600    /// This enables reflection code to access assets. For detailed information, see the docs on [`ReflectAsset`] and [`ReflectHandle`].
601    fn register_asset_reflect<A>(&mut self) -> &mut Self
602    where
603        A: Asset + Reflect + FromReflect + GetTypeRegistration;
604    /// Preregisters a loader for the given extensions, that will block asset loads until a real loader
605    /// is registered.
606    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            // The processor should have its own handle provider separate from the Asset storage
664            // to ensure the id spaces are entirely separate. Not _strictly_ necessary, but
665            // desirable.
666            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/// A system set that holds all "track asset" operations.
719#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
720pub struct AssetTrackingSystems;
721
722/// A system set where events accumulated in [`Assets`] are applied to the [`AssetEvent`] [`Messages`] resource.
723///
724/// [`Messages`]: bevy_ecs::message::Messages
725#[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    /// An asset whose loader performs a nested *untyped* load, to exercise
791    /// [`NestedLoadBuilder::load_untyped`](crate::NestedLoadBuilder::load_untyped).
792    #[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    /// A dummy [`CoolText`] asset reader that only succeeds after `failure_count` times it's read from for each asset.
897    #[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    /// Creates a basic asset app and an in-memory file system.
965    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    /// Serializes `text` into a `CoolText` that can be loaded.
1050    ///
1051    /// This doesn't support all the features of `CoolText`, so more complex scenarios may require
1052    /// doing this manually.
1053    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        // Allow "a" to load ... wait for it to finish loading and validate results
1141        // Dependencies are still gated so they should not be loaded yet
1142        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        // Allow "b" to load ... wait for it to finish loading and validate results
1172        // "c" should not be loaded yet
1173        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        // Allow "c" to load ... wait for it to finish loading and validate results
1203        // all "a" dependencies should be loaded now
1204        gate_opener.open(c_path);
1205
1206        // Re-open a and b gates to allow c to load embedded deps (gates are closed after each load)
1207        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        // this requires a second update because the parent asset was freed in the previous app.update()
1321        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            // Only one load still!
1447            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                // wait until d has exited the loading state
1473                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                // wait until b fails
1579                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            // wait until c loads
1596            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        // handle is dropped
1649        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        // handle is dropped
1697        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        // remove event is emitted
1703        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        // No loads have occurred yet.
1712        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            // this dependency is behind a manual load gate, which should prevent 'a' from emitting a LoadedWithDependencies event
1725            dependencies: vec![dep_handle.clone()],
1726            sub_texts: Vec::new(),
1727        };
1728        let a_handle = app.world().resource::<AssetServer>().load_asset(a);
1729
1730        // load_asset does not count as a load.
1731        assert_eq!(get_started_load_count(app.world()), 1);
1732
1733        app.update();
1734        // TODO: ideally it doesn't take two updates for the added event to emit
1735        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        // The folder started loading. The task will also try to start loading the first asset in
1812        // the folder. With the multi_threaded feature this check is racing with the first load, so
1813        // allow 1 or 2 load tasks to start.
1814        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    /// Tests that `AssetLoadFailedEvent<A>` events are emitted and can be used to retry failed assets.
1868    #[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            // In the real world, this would refer to time (not ticks)
1895            tracker.tick += 1;
1896
1897            // Retry loading past failed items
1898            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            // Check what just failed
1912            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                                // Retry in 10 ticks
1922                                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                // This reader is unused, but we set it here so we don't accidentally use the
1959                // filesystem.
1960                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        // running schedule does not error on ambiguity between the 2 uses_assets systems
2020        app.world_mut().run_schedule(Update);
2021    }
2022
2023    // This test is not checking a requirement, but documenting a current limitation. We simply are
2024    // not capable of loading subassets when doing nested immediate loads.
2025    #[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                // We expect this load to fail.
2058                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    // validate the Asset derive macro for various asset types
2088    #[derive(Asset, TypePath)]
2089    pub struct TestAsset;
2090
2091    // Test that `VisitAssetDependencies` can be derived without deriving
2092    // `Asset`, and that the type can be used as a `#[dependency]` within an
2093    // asset.
2094    #[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        // An approved asset that declares the unapproved asset above as a dependency, so that the
2173        // unapproved path is reached through a nested load from inside an `AssetLoader`.
2174        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        // Reached through a nested *untyped* load; the contents are unused.
2186        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        // Make sure this asset actually loads.
2246        run_app_until(&mut app, |_| asset_server.is_loaded(&handle).then_some(()));
2247    }
2248
2249    /// Regression test for [#21584](https://github.com/bevyengine/bevy/issues/21584).
2250    ///
2251    /// Rejecting an unapproved path yields a default (`Uuid`) handle, but nested loads assumed
2252    /// they always received a `Strong` handle and unwrapped the conversion, panicking inside the
2253    /// asset loader. The nested load should instead be skipped, matching the behavior of a
2254    /// top-level [`AssetServer::load`] of an unapproved path.
2255    #[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        // The rejected dependency resolves to a default handle rather than panicking.
2279        assert_eq!(cool_text.dependencies, vec![Handle::default()]);
2280    }
2281
2282    /// Regression test for [#21584](https://github.com/bevyengine/bevy/issues/21584), covering the
2283    /// untyped nested load path, which rejects unapproved paths the same way.
2284    #[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        // Make sure this asset actually loads.
2320        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        // Update once more, just to make sure there's no race condition between the task finishing
2364        // and the load event being processed by the app.
2365        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        // We still have the asset ID, but we've dropped the handle so the asset is no longer live.
2386        let asset_id = handle.id();
2387        drop(handle);
2388
2389        // Allow `Assets` to detect the dropped handle.
2390        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        // Try to insert an asset into the dropped handle's spot. This should not panic.
2399        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    /// A loader that notifies a sender when the loader has started, and blocks on a receiver to
2408    /// simulate a long asset loader.
2409    // Note: we can't just use the GatedReader, since currently we hold the handle until after
2410    // we've selected the reader. The GatedReader blocks this process, so we need to wait until
2411    // we gate in the loader instead.
2412    #[derive(TypePath)]
2413    struct GatedLoader {
2414        in_loader_sender: Sender<()>,
2415        gate_receiver: Receiver<()>,
2416        /// An extra sender for indicating whether the task continued after the gate was opened or
2417        /// not.
2418        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            // There's technically a race condition above. It is possible that this order happens:
2448            // 1. This loader sends through `in_loader_sender`.
2449            // 2. The cancellation tests receive through the `in_loader_sender` channel.
2450            // 3. The cancellation tests drop their handle, then update the app, which drops the
2451            //    `Task` (which cancels the task).
2452            // 4. The cancellation tests send through the `gate_receiver` channel.
2453            // 5. This loader receives through `gate_receiver` **without yielding** because there's
2454            //    already a message in the channel.
2455            // This would mean that the loader never yields and therefore never gets a chance to be
2456            // cancelled. This explicit yield gives us that chance to be cancelled. This however
2457            // depends on the async runtime not doing anything "clever" (e.g., re-polling the future
2458            // if it's woken immediately after returning `Poll::Pending`).
2459            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        // Start loading the asset. This load will get blocked by the gate.
2493        let handle = asset_server.load::<TestAsset>(path);
2494        assert!(asset_server.get_load_state(&handle).unwrap().is_loading());
2495        app.update();
2496
2497        // Make sure we are inside the loader before continuing.
2498        in_loader_receiver.recv_blocking().unwrap();
2499
2500        let asset_id = handle.id();
2501        // Dropping the handle and doing another update should result in the load being cancelled.
2502        drop(handle);
2503        app.update();
2504        assert!(asset_server.get_load_state(asset_id).is_none());
2505
2506        // Unblock the loader and then update a few times, showing that the asset never loads.
2507        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        // Start loading the subasset. This load will get blocked by the gate.
2551        // Note: it doesn't matter that the subasset doesn't actually end up existing, since the
2552        // asset system doesn't know that until after the load completes, which we cancel anyway.
2553        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        // Make sure we are inside the loader before continuing.
2558        in_loader_receiver.recv_blocking().unwrap();
2559
2560        let asset_id = handle.id();
2561        // Dropping the handle and doing another update should result in the load being cancelled.
2562        drop(handle);
2563        app.update();
2564        assert!(asset_server.get_load_state(asset_id).is_none());
2565
2566        // Unblock the loader and then update a few times, showing that the asset never loads.
2567        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    // Creates a basic app with the default asset source engineered to get back the asset event
2591    // sender.
2592    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        // Create a channel to pass the source event sender back to us.
2598        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        // Sending an asset event should result in the asset being reloaded - resulting in a
2678        // "Modified" message.
2679        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        // The asset has already been considered as failed to load. Now we add the asset data, and
2724        // send an AddedAsset event.
2725        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    /// A simple asset storing just a u8.
2755    #[derive(Asset, TypePath)]
2756    struct U8Asset(u8);
2757
2758    /// Settings for [`U8Loader`], which stores the value that the returned [`U8Asset`] will store.
2759    #[derive(Serialize, Deserialize, Default)]
2760    struct U8LoaderSettings(u8);
2761
2762    /// A loader that loads a [`U8Asset`] by ignoring the reader entirely and just getting the value
2763    /// from its loader settings.
2764    #[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        // Test loading the same asset twice with different settings. This should
2789        // produce two distinct assets.
2790
2791        // Create a test asset and setup the app.
2792
2793        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        // Load the test asset twice but with different settings.
2801
2802        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        // Handles should be different.
2813
2814        // These handles should be different, but due to
2815        // https://github.com/bevyengine/bevy/pull/21564, they are not. Once 21564 is fixed, we
2816        // should replace these expects.
2817        //
2818        // assert_ne!(handle_1, handle_2);
2819        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            // Values should match the settings.
2830
2831            // These values should be different, but due to
2832            // https://github.com/bevyengine/bevy/pull/21564, they are not. Once 21564 is fixed, we
2833            // should replace these expects.
2834            //
2835            // assert_eq!(asset_1.0, 1);
2836            // assert_eq!(asset_2.0, 2);
2837            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        // Due to https://github.com/bevyengine/bevy/issues/12756, this expectation fails. Once
2882        // #12756 is fixed, we should swap these asserts.
2883        //
2884        // assert_eq!(get_started_load_count(app.world()), 1);
2885        assert_eq!(get_started_load_count(app.world()), 2);
2886    }
2887
2888    /// A loader that immediately returns a [`TestAsset`].
2889    #[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        // Wait for the asset to load.
2923        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        // Get a new strong handle from the original handle's ID.
2933        let new_handle = app
2934            .world_mut()
2935            .resource_mut::<Assets<TestAsset>>()
2936            .get_strong_handle(original_handle.id())
2937            .unwrap();
2938
2939        // Drop the original handle. This should still leave the asset alive.
2940        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        // The asset server should **not** have started a new load, since the asset is still alive.
2952
2953        // Due to https://github.com/bevyengine/bevy/issues/20651, we do get a second load. Once
2954        // #20651 is fixed, we should swap these asserts.
2955        //
2956        // assert_eq!(get_started_load_count(app.world()), 1);
2957        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        /// This asset holds a handle to its dependency.
2965        #[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        /// This asset holds a handle a dependency of one of its dependencies.
2993        #[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            // The immediate asset is loaded, and the asset it got from its immediate load is also
3049            // loaded.
3050            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                // Load the asset in the root context, but then put the handle in the subasset. So
3136                // the subasset's (internal) load context never loaded `dep`.
3137                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        // Write some data so the loaders have something to load (even though they don't use the
3148        // data).
3149        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        // Even though the subasset is loaded, and its load context never loaded its dependency, it
3170        // still depends on its dependency, so that is tracked correctly here.
3171        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        // Pass the gate in the dependency loader.
3182        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        // Now that the dependency is loaded, the subasset is counted as loaded with dependencies!
3189        assert!(asset_server.is_loaded_with_dependencies(&subasset_handle));
3190    }
3191
3192    // A simplified version of `LoadState` for easier comparison.
3193    #[derive(Debug, PartialEq, Eq)]
3194    enum TestLoadState {
3195        NotLoaded,
3196        Loading,
3197        Loaded,
3198        Failed(TestAssetLoadError),
3199    }
3200
3201    // A simplified subset of `AssetLoadError` for easier comparison.
3202    #[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    // An asset type that doesn't have a registered loader.
3244    #[derive(Asset, TypePath)]
3245    struct LoaderlessAsset;
3246
3247    // Load the given path and test that `AssetServer::get_load_state` returns
3248    // the given state.
3249    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    // Tests that `AssetServer::get_load_state` returns the correct state after
3296    // various loads, some of which trigger errors.
3297    #[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        // Not necessary but better to make things more realistic to ensure we hit the right error
3358        // case.
3359        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            // TODO: We have way too many "load" variants. We **need** to simplify this.
3370            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        // Sort the handles so we know abc is first and def is second.
3503        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        // Before doing any hot reloading stuff, clear out any AssetEvent messages.
3513        app.world_mut()
3514            .resource_mut::<Messages<AssetEvent<LoadedFolder>>>()
3515            .clear();
3516
3517        // Add a new asset to the folder, and send an event to trigger hot-reloading.
3518        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        // Sort the handles so we know the order is abc, def, and ghi.
3551        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}