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//! If you want to save your assets back to disk, you should implement [`AssetSaver`](saver::AssetSaver) as well.
139//! This trait mirrors [`AssetLoader`] in structure, and works in tandem with [`AssetWriter`](io::AssetWriter), which mirrors [`AssetReader`](io::AssetReader).
140
141#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
142#![cfg_attr(docsrs, feature(doc_auto_cfg))]
143#![doc(
144    html_logo_url = "https://bevyengine.org/assets/icon.png",
145    html_favicon_url = "https://bevyengine.org/assets/icon.png"
146)]
147#![no_std]
148
149extern crate alloc;
150extern crate std;
151
152// Required to make proc macros work in bevy itself.
153extern crate self as bevy_asset;
154
155pub mod io;
156pub mod meta;
157pub mod processor;
158pub mod saver;
159pub mod transformer;
160
161/// The asset prelude.
162///
163/// This includes the most common types in this crate, re-exported for your convenience.
164pub mod prelude {
165    #[doc(hidden)]
166    pub use crate::asset_changed::AssetChanged;
167
168    #[doc(hidden)]
169    pub use crate::{
170        Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets,
171        DirectAssetAccessExt, Handle, UntypedHandle,
172    };
173}
174
175mod asset_changed;
176mod assets;
177mod direct_access_ext;
178mod event;
179mod folder;
180mod handle;
181mod id;
182mod loader;
183mod loader_builders;
184mod path;
185mod reflect;
186mod render_asset;
187mod server;
188
189pub use assets::*;
190pub use bevy_asset_macros::Asset;
191pub use direct_access_ext::DirectAssetAccessExt;
192pub use event::*;
193pub use folder::*;
194pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
195pub use handle::*;
196pub use id::*;
197pub use loader::*;
198pub use loader_builders::{
199    Deferred, DynamicTyped, Immediate, NestedLoader, StaticTyped, UnknownTyped,
200};
201pub use path::*;
202pub use reflect::*;
203pub use render_asset::*;
204pub use server::*;
205
206/// Rusty Object Notation, a crate used to serialize and deserialize bevy assets.
207pub use ron;
208pub use uuid;
209
210use crate::{
211    io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId},
212    processor::{AssetProcessor, Process},
213};
214use alloc::{
215    string::{String, ToString},
216    sync::Arc,
217    vec::Vec,
218};
219use bevy_app::{App, Plugin, PostUpdate, PreUpdate};
220use bevy_ecs::prelude::Component;
221use bevy_ecs::{
222    reflect::AppTypeRegistry,
223    schedule::{IntoScheduleConfigs, SystemSet},
224    world::FromWorld,
225};
226use bevy_platform::collections::HashSet;
227use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath};
228use core::any::TypeId;
229use tracing::error;
230
231#[cfg(all(feature = "file_watcher", not(feature = "multi_threaded")))]
232compile_error!(
233    "The \"file_watcher\" feature for hot reloading requires the \
234    \"multi_threaded\" feature to be functional.\n\
235    Consider either disabling the \"file_watcher\" feature or enabling \"multi_threaded\""
236);
237
238/// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetSource`],
239/// which can be something like a filesystem, a network, etc.
240///
241/// Supports flexible "modes", such as [`AssetMode::Processed`] and
242/// [`AssetMode::Unprocessed`] that enable using the asset workflow that best suits your project.
243///
244/// [`AssetSource`]: io::AssetSource
245pub struct AssetPlugin {
246    /// The default file path to use (relative to the project root) for unprocessed assets.
247    pub file_path: String,
248    /// The default file path to use (relative to the project root) for processed assets.
249    pub processed_file_path: String,
250    /// If set, will override the default "watch for changes" setting. By default "watch for changes" will be `false` unless
251    /// the `watch` cargo feature is set. `watch` can be enabled manually, or it will be automatically enabled if a specific watcher
252    /// like `file_watcher` is enabled.
253    ///
254    /// Most use cases should leave this set to [`None`] and enable a specific watcher feature such as `file_watcher` to enable
255    /// watching for dev-scenarios.
256    pub watch_for_changes_override: Option<bool>,
257    /// The [`AssetMode`] to use for this server.
258    pub mode: AssetMode,
259    /// How/If asset meta files should be checked.
260    pub meta_check: AssetMetaCheck,
261    /// How to handle load requests of files that are outside the approved directories.
262    ///
263    /// Approved folders are [`AssetPlugin::file_path`] and the folder of each
264    /// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
265    pub unapproved_path_mode: UnapprovedPathMode,
266}
267
268/// Determines how to react to attempts to load assets not inside the approved folders.
269///
270/// Approved folders are [`AssetPlugin::file_path`] and the folder of each
271/// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
272///
273/// It is strongly discouraged to use [`Allow`](UnapprovedPathMode::Allow) if your
274/// app will include scripts or modding support, as it could allow allow arbitrary file
275/// access for malicious code.
276///
277/// See [`AssetPath::is_unapproved`](crate::AssetPath::is_unapproved)
278#[derive(Clone, Default)]
279pub enum UnapprovedPathMode {
280    /// Unapproved asset loading is allowed. This is strongly discouraged.
281    Allow,
282    /// Fails to load any asset that is is unapproved, unless an override method is used, like
283    /// [`AssetServer::load_override`].
284    Deny,
285    /// Fails to load any asset that is is unapproved.
286    #[default]
287    Forbid,
288}
289
290/// Controls whether or not assets are pre-processed before being loaded.
291///
292/// This setting is controlled by setting [`AssetPlugin::mode`].
293///
294/// When building on web, asset preprocessing can cause problems due to the lack of filesystem access.
295/// See [bevy#10157](https://github.com/bevyengine/bevy/issues/10157) for context.
296#[derive(Debug)]
297pub enum AssetMode {
298    /// Loads assets from their [`AssetSource`]'s default [`AssetReader`] without any "preprocessing".
299    ///
300    /// [`AssetReader`]: io::AssetReader
301    /// [`AssetSource`]: io::AssetSource
302    Unprocessed,
303    /// Assets will be "pre-processed". This enables assets to be imported / converted / optimized ahead of time.
304    ///
305    /// Assets will be read from their unprocessed [`AssetSource`] (defaults to the `assets` folder),
306    /// processed according to their [`AssetMeta`], and written to their processed [`AssetSource`] (defaults to the `imported_assets/Default` folder).
307    ///
308    /// By default, this assumes the processor _has already been run_. It will load assets from their final processed [`AssetReader`].
309    ///
310    /// When developing an app, you should enable the `asset_processor` cargo feature, which will run the asset processor at startup. This should generally
311    /// be used in combination with the `file_watcher` cargo feature, which enables hot-reloading of assets that have changed. When both features are enabled,
312    /// 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.
313    ///
314    /// [`AssetMeta`]: meta::AssetMeta
315    /// [`AssetSource`]: io::AssetSource
316    /// [`AssetReader`]: io::AssetReader
317    Processed,
318}
319
320/// Configures how / if meta files will be checked. If an asset's meta file is not checked, the default meta for the asset
321/// will be used.
322#[derive(Debug, Default, Clone)]
323pub enum AssetMetaCheck {
324    /// Always check if assets have meta files. If the meta does not exist, the default meta will be used.
325    #[default]
326    Always,
327    /// Only look up meta files for the provided paths. The default meta will be used for any paths not contained in this set.
328    Paths(HashSet<AssetPath<'static>>),
329    /// 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.
330    Never,
331}
332
333impl Default for AssetPlugin {
334    fn default() -> Self {
335        Self {
336            mode: AssetMode::Unprocessed,
337            file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(),
338            processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
339            watch_for_changes_override: None,
340            meta_check: AssetMetaCheck::default(),
341            unapproved_path_mode: UnapprovedPathMode::default(),
342        }
343    }
344}
345
346impl AssetPlugin {
347    const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets";
348    /// NOTE: this is in the Default sub-folder to make this forward compatible with "import profiles"
349    /// and to allow us to put the "processor transaction log" at `imported_assets/log`
350    const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default";
351}
352
353impl Plugin for AssetPlugin {
354    fn build(&self, app: &mut App) {
355        let embedded = EmbeddedAssetRegistry::default();
356        {
357            let mut sources = app
358                .world_mut()
359                .get_resource_or_init::<AssetSourceBuilders>();
360            sources.init_default_source(
361                &self.file_path,
362                (!matches!(self.mode, AssetMode::Unprocessed))
363                    .then_some(self.processed_file_path.as_str()),
364            );
365            embedded.register_source(&mut sources);
366        }
367        {
368            let mut watch = cfg!(feature = "watch");
369            if let Some(watch_override) = self.watch_for_changes_override {
370                watch = watch_override;
371            }
372            match self.mode {
373                AssetMode::Unprocessed => {
374                    let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
375                    let sources = builders.build_sources(watch, false);
376
377                    app.insert_resource(AssetServer::new_with_meta_check(
378                        sources,
379                        AssetServerMode::Unprocessed,
380                        self.meta_check.clone(),
381                        watch,
382                        self.unapproved_path_mode.clone(),
383                    ));
384                }
385                AssetMode::Processed => {
386                    #[cfg(feature = "asset_processor")]
387                    {
388                        let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
389                        let processor = AssetProcessor::new(&mut builders);
390                        let mut sources = builders.build_sources(false, watch);
391                        sources.gate_on_processor(processor.data.clone());
392                        // the main asset server shares loaders with the processor asset server
393                        app.insert_resource(AssetServer::new_with_loaders(
394                            sources,
395                            processor.server().data.loaders.clone(),
396                            AssetServerMode::Processed,
397                            AssetMetaCheck::Always,
398                            watch,
399                            self.unapproved_path_mode.clone(),
400                        ))
401                        .insert_resource(processor)
402                        .add_systems(bevy_app::Startup, AssetProcessor::start);
403                    }
404                    #[cfg(not(feature = "asset_processor"))]
405                    {
406                        let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
407                        let sources = builders.build_sources(false, watch);
408                        app.insert_resource(AssetServer::new_with_meta_check(
409                            sources,
410                            AssetServerMode::Processed,
411                            AssetMetaCheck::Always,
412                            watch,
413                            self.unapproved_path_mode.clone(),
414                        ));
415                    }
416                }
417            }
418        }
419        app.insert_resource(embedded)
420            .init_asset::<LoadedFolder>()
421            .init_asset::<LoadedUntypedAsset>()
422            .init_asset::<()>()
423            .add_event::<UntypedAssetLoadFailedEvent>()
424            .configure_sets(PreUpdate, TrackAssets.after(handle_internal_asset_events))
425            // `handle_internal_asset_events` requires the use of `&mut World`,
426            // and as a result has ambiguous system ordering with all other systems in `PreUpdate`.
427            // This is virtually never a real problem: asset loading is async and so anything that interacts directly with it
428            // needs to be robust to stochastic delays anyways.
429            .add_systems(PreUpdate, handle_internal_asset_events.ambiguous_with_all())
430            .register_type::<AssetPath>();
431    }
432}
433
434/// Declares that this type is an asset,
435/// which can be loaded and managed by the [`AssetServer`] and stored in [`Assets`] collections.
436///
437/// Generally, assets are large, complex, and/or expensive to load from disk, and are often authored by artists or designers.
438///
439/// [`TypePath`] is largely used for diagnostic purposes, and should almost always be implemented by deriving [`Reflect`] on your type.
440/// [`VisitAssetDependencies`] is used to track asset dependencies, and an implementation is automatically generated when deriving [`Asset`].
441#[diagnostic::on_unimplemented(
442    message = "`{Self}` is not an `Asset`",
443    label = "invalid `Asset`",
444    note = "consider annotating `{Self}` with `#[derive(Asset)]`"
445)]
446pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {}
447
448/// A trait for components that can be used as asset identifiers, e.g. handle wrappers.
449pub trait AsAssetId: Component {
450    /// The underlying asset type.
451    type Asset: Asset;
452
453    /// Retrieves the asset id from this component.
454    fn as_asset_id(&self) -> AssetId<Self::Asset>;
455}
456
457/// This trait defines how to visit the dependencies of an asset.
458/// For example, a 3D model might require both textures and meshes to be loaded.
459///
460/// Note that this trait is automatically implemented when deriving [`Asset`].
461pub trait VisitAssetDependencies {
462    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId));
463}
464
465impl<A: Asset> VisitAssetDependencies for Handle<A> {
466    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
467        visit(self.id().untyped());
468    }
469}
470
471impl<A: Asset> VisitAssetDependencies for Option<Handle<A>> {
472    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
473        if let Some(handle) = self {
474            visit(handle.id().untyped());
475        }
476    }
477}
478
479impl VisitAssetDependencies for UntypedHandle {
480    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
481        visit(self.id());
482    }
483}
484
485impl VisitAssetDependencies for Option<UntypedHandle> {
486    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
487        if let Some(handle) = self {
488            visit(handle.id());
489        }
490    }
491}
492
493impl<A: Asset> VisitAssetDependencies for Vec<Handle<A>> {
494    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
495        for dependency in self {
496            visit(dependency.id().untyped());
497        }
498    }
499}
500
501impl VisitAssetDependencies for Vec<UntypedHandle> {
502    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
503        for dependency in self {
504            visit(dependency.id());
505        }
506    }
507}
508
509/// Adds asset-related builder methods to [`App`].
510pub trait AssetApp {
511    /// Registers the given `loader` in the [`App`]'s [`AssetServer`].
512    fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self;
513    /// Registers the given `processor` in the [`App`]'s [`AssetProcessor`].
514    fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self;
515    /// Registers the given [`AssetSourceBuilder`] with the given `id`.
516    ///
517    /// Note that asset sources must be registered before adding [`AssetPlugin`] to your application,
518    /// since registered asset sources are built at that point and not after.
519    fn register_asset_source(
520        &mut self,
521        id: impl Into<AssetSourceId<'static>>,
522        source: AssetSourceBuilder,
523    ) -> &mut Self;
524    /// Sets the default asset processor for the given `extension`.
525    fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self;
526    /// Initializes the given loader in the [`App`]'s [`AssetServer`].
527    fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self;
528    /// Initializes the given [`Asset`] in the [`App`] by:
529    /// * Registering the [`Asset`] in the [`AssetServer`]
530    /// * Initializing the [`AssetEvent`] resource for the [`Asset`]
531    /// * Adding other relevant systems and resources for the [`Asset`]
532    /// * Ignoring schedule ambiguities in [`Assets`] resource. Any time a system takes
533    ///   mutable access to this resource this causes a conflict, but they rarely actually
534    ///   modify the same underlying asset.
535    fn init_asset<A: Asset>(&mut self) -> &mut Self;
536    /// Registers the asset type `T` using `[App::register]`,
537    /// and adds [`ReflectAsset`] type data to `T` and [`ReflectHandle`] type data to [`Handle<T>`] in the type registry.
538    ///
539    /// This enables reflection code to access assets. For detailed information, see the docs on [`ReflectAsset`] and [`ReflectHandle`].
540    fn register_asset_reflect<A>(&mut self) -> &mut Self
541    where
542        A: Asset + Reflect + FromReflect + GetTypeRegistration;
543    /// Preregisters a loader for the given extensions, that will block asset loads until a real loader
544    /// is registered.
545    fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self;
546}
547
548impl AssetApp for App {
549    fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
550        self.world()
551            .resource::<AssetServer>()
552            .register_loader(loader);
553        self
554    }
555
556    fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self {
557        if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
558            asset_processor.register_processor(processor);
559        }
560        self
561    }
562
563    fn register_asset_source(
564        &mut self,
565        id: impl Into<AssetSourceId<'static>>,
566        source: AssetSourceBuilder,
567    ) -> &mut Self {
568        let id = AssetSourceId::from_static(id);
569        if self.world().get_resource::<AssetServer>().is_some() {
570            error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id);
571        }
572
573        {
574            let mut sources = self
575                .world_mut()
576                .get_resource_or_init::<AssetSourceBuilders>();
577            sources.insert(id, source);
578        }
579
580        self
581    }
582
583    fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self {
584        if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
585            asset_processor.set_default_processor::<P>(extension);
586        }
587        self
588    }
589
590    fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self {
591        let loader = L::from_world(self.world_mut());
592        self.register_asset_loader(loader)
593    }
594
595    fn init_asset<A: Asset>(&mut self) -> &mut Self {
596        let assets = Assets::<A>::default();
597        self.world()
598            .resource::<AssetServer>()
599            .register_asset(&assets);
600        if self.world().contains_resource::<AssetProcessor>() {
601            let processor = self.world().resource::<AssetProcessor>();
602            // The processor should have its own handle provider separate from the Asset storage
603            // to ensure the id spaces are entirely separate. Not _strictly_ necessary, but
604            // desirable.
605            processor
606                .server()
607                .register_handle_provider(AssetHandleProvider::new(
608                    TypeId::of::<A>(),
609                    Arc::new(AssetIndexAllocator::default()),
610                ));
611        }
612        self.insert_resource(assets)
613            .allow_ambiguous_resource::<Assets<A>>()
614            .add_event::<AssetEvent<A>>()
615            .add_event::<AssetLoadFailedEvent<A>>()
616            .register_type::<Handle<A>>()
617            .add_systems(
618                PostUpdate,
619                Assets::<A>::asset_events
620                    .run_if(Assets::<A>::asset_events_condition)
621                    .in_set(AssetEvents),
622            )
623            .add_systems(PreUpdate, Assets::<A>::track_assets.in_set(TrackAssets))
624    }
625
626    fn register_asset_reflect<A>(&mut self) -> &mut Self
627    where
628        A: Asset + Reflect + FromReflect + GetTypeRegistration,
629    {
630        let type_registry = self.world().resource::<AppTypeRegistry>();
631        {
632            let mut type_registry = type_registry.write();
633
634            type_registry.register::<A>();
635            type_registry.register::<Handle<A>>();
636            type_registry.register_type_data::<A, ReflectAsset>();
637            type_registry.register_type_data::<Handle<A>, ReflectHandle>();
638        }
639
640        self
641    }
642
643    fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self {
644        self.world_mut()
645            .resource_mut::<AssetServer>()
646            .preregister_loader::<L>(extensions);
647        self
648    }
649}
650
651/// A system set that holds all "track asset" operations.
652#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
653pub struct TrackAssets;
654
655/// A system set where events accumulated in [`Assets`] are applied to the [`AssetEvent`] [`Events`] resource.
656///
657/// [`Events`]: bevy_ecs::event::Events
658#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
659pub struct AssetEvents;
660
661#[cfg(test)]
662mod tests {
663    use crate::{
664        folder::LoadedFolder,
665        handle::Handle,
666        io::{
667            gated::{GateOpener, GatedReader},
668            memory::{Dir, MemoryAssetReader},
669            AssetReader, AssetReaderError, AssetSource, AssetSourceId, Reader,
670        },
671        loader::{AssetLoader, LoadContext},
672        Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
673        AssetPlugin, AssetServer, Assets, LoadState, UnapprovedPathMode,
674    };
675    use alloc::{
676        boxed::Box,
677        format,
678        string::{String, ToString},
679        sync::Arc,
680        vec,
681        vec::Vec,
682    };
683    use bevy_app::{App, TaskPoolPlugin, Update};
684    use bevy_ecs::{
685        event::EventCursor,
686        prelude::*,
687        schedule::{LogLevel, ScheduleBuildSettings},
688    };
689    use bevy_platform::collections::HashMap;
690    use bevy_reflect::TypePath;
691    use core::time::Duration;
692    use serde::{Deserialize, Serialize};
693    use std::path::Path;
694    use thiserror::Error;
695
696    #[derive(Asset, TypePath, Debug, Default)]
697    pub struct CoolText {
698        pub text: String,
699        pub embedded: String,
700        #[dependency]
701        pub dependencies: Vec<Handle<CoolText>>,
702        #[dependency]
703        pub sub_texts: Vec<Handle<SubText>>,
704    }
705
706    #[derive(Asset, TypePath, Debug)]
707    pub struct SubText {
708        text: String,
709    }
710
711    #[derive(Serialize, Deserialize)]
712    pub struct CoolTextRon {
713        text: String,
714        dependencies: Vec<String>,
715        embedded_dependencies: Vec<String>,
716        sub_texts: Vec<String>,
717    }
718
719    #[derive(Default)]
720    pub struct CoolTextLoader;
721
722    #[derive(Error, Debug)]
723    pub enum CoolTextLoaderError {
724        #[error("Could not load dependency: {dependency}")]
725        CannotLoadDependency { dependency: AssetPath<'static> },
726        #[error("A RON error occurred during loading")]
727        RonSpannedError(#[from] ron::error::SpannedError),
728        #[error("An IO error occurred during loading")]
729        Io(#[from] std::io::Error),
730    }
731
732    impl AssetLoader for CoolTextLoader {
733        type Asset = CoolText;
734
735        type Settings = ();
736
737        type Error = CoolTextLoaderError;
738
739        async fn load(
740            &self,
741            reader: &mut dyn Reader,
742            _settings: &Self::Settings,
743            load_context: &mut LoadContext<'_>,
744        ) -> Result<Self::Asset, Self::Error> {
745            let mut bytes = Vec::new();
746            reader.read_to_end(&mut bytes).await?;
747            let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
748            let mut embedded = String::new();
749            for dep in ron.embedded_dependencies {
750                let loaded = load_context
751                    .loader()
752                    .immediate()
753                    .load::<CoolText>(&dep)
754                    .await
755                    .map_err(|_| Self::Error::CannotLoadDependency {
756                        dependency: dep.into(),
757                    })?;
758                let cool = loaded.get();
759                embedded.push_str(&cool.text);
760            }
761            Ok(CoolText {
762                text: ron.text,
763                embedded,
764                dependencies: ron
765                    .dependencies
766                    .iter()
767                    .map(|p| load_context.load(p))
768                    .collect(),
769                sub_texts: ron
770                    .sub_texts
771                    .drain(..)
772                    .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text }))
773                    .collect(),
774            })
775        }
776
777        fn extensions(&self) -> &[&str] {
778            &["cool.ron"]
779        }
780    }
781
782    /// A dummy [`CoolText`] asset reader that only succeeds after `failure_count` times it's read from for each asset.
783    #[derive(Default, Clone)]
784    pub struct UnstableMemoryAssetReader {
785        pub attempt_counters: Arc<std::sync::Mutex<HashMap<Box<Path>, usize>>>,
786        pub load_delay: Duration,
787        memory_reader: MemoryAssetReader,
788        failure_count: usize,
789    }
790
791    impl UnstableMemoryAssetReader {
792        pub fn new(root: Dir, failure_count: usize) -> Self {
793            Self {
794                load_delay: Duration::from_millis(10),
795                memory_reader: MemoryAssetReader { root },
796                attempt_counters: Default::default(),
797                failure_count,
798            }
799        }
800    }
801
802    impl AssetReader for UnstableMemoryAssetReader {
803        async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
804            self.memory_reader.is_directory(path).await
805        }
806        async fn read_directory<'a>(
807            &'a self,
808            path: &'a Path,
809        ) -> Result<Box<bevy_asset::io::PathStream>, AssetReaderError> {
810            self.memory_reader.read_directory(path).await
811        }
812        async fn read_meta<'a>(
813            &'a self,
814            path: &'a Path,
815        ) -> Result<impl Reader + 'a, AssetReaderError> {
816            self.memory_reader.read_meta(path).await
817        }
818        async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
819            let attempt_number = {
820                let mut attempt_counters = self.attempt_counters.lock().unwrap();
821                if let Some(existing) = attempt_counters.get_mut(path) {
822                    *existing += 1;
823                    *existing
824                } else {
825                    attempt_counters.insert(path.into(), 1);
826                    1
827                }
828            };
829
830            if attempt_number <= self.failure_count {
831                let io_error = std::io::Error::new(
832                    std::io::ErrorKind::ConnectionRefused,
833                    format!(
834                        "Simulated failure {attempt_number} of {}",
835                        self.failure_count
836                    ),
837                );
838                let wait = self.load_delay;
839                return async move {
840                    std::thread::sleep(wait);
841                    Err(AssetReaderError::Io(io_error.into()))
842                }
843                .await;
844            }
845
846            self.memory_reader.read(path).await
847        }
848    }
849
850    fn test_app(dir: Dir) -> (App, GateOpener) {
851        let mut app = App::new();
852        let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir });
853        app.register_asset_source(
854            AssetSourceId::Default,
855            AssetSource::build().with_reader(move || Box::new(gated_memory_reader.clone())),
856        )
857        .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()));
858        (app, gate_opener)
859    }
860
861    pub fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) {
862        for _ in 0..LARGE_ITERATION_COUNT {
863            app.update();
864            if predicate(app.world_mut()).is_some() {
865                return;
866            }
867        }
868
869        panic!("Ran out of loops to return `Some` from `predicate`");
870    }
871
872    const LARGE_ITERATION_COUNT: usize = 10000;
873
874    fn get<A: Asset>(world: &World, id: AssetId<A>) -> Option<&A> {
875        world.resource::<Assets<A>>().get(id)
876    }
877
878    #[derive(Resource, Default)]
879    struct StoredEvents(Vec<AssetEvent<CoolText>>);
880
881    fn store_asset_events(
882        mut reader: EventReader<AssetEvent<CoolText>>,
883        mut storage: ResMut<StoredEvents>,
884    ) {
885        storage.0.extend(reader.read().cloned());
886    }
887
888    #[test]
889    fn load_dependencies() {
890        // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded
891        #[cfg(not(feature = "multi_threaded"))]
892        panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
893
894        let dir = Dir::default();
895
896        let a_path = "a.cool.ron";
897        let a_ron = r#"
898(
899    text: "a",
900    dependencies: [
901        "foo/b.cool.ron",
902        "c.cool.ron",
903    ],
904    embedded_dependencies: [],
905    sub_texts: [],
906)"#;
907        let b_path = "foo/b.cool.ron";
908        let b_ron = r#"
909(
910    text: "b",
911    dependencies: [],
912    embedded_dependencies: [],
913    sub_texts: [],
914)"#;
915
916        let c_path = "c.cool.ron";
917        let c_ron = r#"
918(
919    text: "c",
920    dependencies: [
921        "d.cool.ron",
922    ],
923    embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"],
924    sub_texts: ["hello"],
925)"#;
926
927        let d_path = "d.cool.ron";
928        let d_ron = r#"
929(
930    text: "d",
931    dependencies: [],
932    embedded_dependencies: [],
933    sub_texts: [],
934)"#;
935
936        dir.insert_asset_text(Path::new(a_path), a_ron);
937        dir.insert_asset_text(Path::new(b_path), b_ron);
938        dir.insert_asset_text(Path::new(c_path), c_ron);
939        dir.insert_asset_text(Path::new(d_path), d_ron);
940
941        #[derive(Resource)]
942        struct IdResults {
943            b_id: AssetId<CoolText>,
944            c_id: AssetId<CoolText>,
945            d_id: AssetId<CoolText>,
946        }
947
948        let (mut app, gate_opener) = test_app(dir);
949        app.init_asset::<CoolText>()
950            .init_asset::<SubText>()
951            .init_resource::<StoredEvents>()
952            .register_asset_loader(CoolTextLoader)
953            .add_systems(Update, store_asset_events);
954        let asset_server = app.world().resource::<AssetServer>().clone();
955        let handle: Handle<CoolText> = asset_server.load(a_path);
956        let a_id = handle.id();
957        app.update();
958        {
959            let a_text = get::<CoolText>(app.world(), a_id);
960            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
961            assert!(a_text.is_none(), "a's asset should not exist yet");
962            assert!(a_load.is_loading());
963            assert!(a_deps.is_loading());
964            assert!(a_rec_deps.is_loading());
965        }
966
967        // Allow "a" to load ... wait for it to finish loading and validate results
968        // Dependencies are still gated so they should not be loaded yet
969        gate_opener.open(a_path);
970        run_app_until(&mut app, |world| {
971            let a_text = get::<CoolText>(world, a_id)?;
972            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
973            assert_eq!(a_text.text, "a");
974            assert_eq!(a_text.dependencies.len(), 2);
975            assert!(a_load.is_loaded());
976            assert!(a_deps.is_loading());
977            assert!(a_rec_deps.is_loading());
978
979            let b_id = a_text.dependencies[0].id();
980            let b_text = get::<CoolText>(world, b_id);
981            let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
982            assert!(b_text.is_none(), "b component should not exist yet");
983            assert!(b_load.is_loading());
984            assert!(b_deps.is_loading());
985            assert!(b_rec_deps.is_loading());
986
987            let c_id = a_text.dependencies[1].id();
988            let c_text = get::<CoolText>(world, c_id);
989            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
990            assert!(c_text.is_none(), "c component should not exist yet");
991            assert!(c_load.is_loading());
992            assert!(c_deps.is_loading());
993            assert!(c_rec_deps.is_loading());
994            Some(())
995        });
996
997        // Allow "b" to load ... wait for it to finish loading and validate results
998        // "c" should not be loaded yet
999        gate_opener.open(b_path);
1000        run_app_until(&mut app, |world| {
1001            let a_text = get::<CoolText>(world, a_id)?;
1002            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1003            assert_eq!(a_text.text, "a");
1004            assert_eq!(a_text.dependencies.len(), 2);
1005            assert!(a_load.is_loaded());
1006            assert!(a_deps.is_loading());
1007            assert!(a_rec_deps.is_loading());
1008
1009            let b_id = a_text.dependencies[0].id();
1010            let b_text = get::<CoolText>(world, b_id)?;
1011            let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1012            assert_eq!(b_text.text, "b");
1013            assert!(b_load.is_loaded());
1014            assert!(b_deps.is_loaded());
1015            assert!(b_rec_deps.is_loaded());
1016
1017            let c_id = a_text.dependencies[1].id();
1018            let c_text = get::<CoolText>(world, c_id);
1019            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1020            assert!(c_text.is_none(), "c component should not exist yet");
1021            assert!(c_load.is_loading());
1022            assert!(c_deps.is_loading());
1023            assert!(c_rec_deps.is_loading());
1024            Some(())
1025        });
1026
1027        // Allow "c" to load ... wait for it to finish loading and validate results
1028        // all "a" dependencies should be loaded now
1029        gate_opener.open(c_path);
1030
1031        // Re-open a and b gates to allow c to load embedded deps (gates are closed after each load)
1032        gate_opener.open(a_path);
1033        gate_opener.open(b_path);
1034        run_app_until(&mut app, |world| {
1035            let a_text = get::<CoolText>(world, a_id)?;
1036            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1037            assert_eq!(a_text.text, "a");
1038            assert_eq!(a_text.embedded, "");
1039            assert_eq!(a_text.dependencies.len(), 2);
1040            assert!(a_load.is_loaded());
1041
1042            let b_id = a_text.dependencies[0].id();
1043            let b_text = get::<CoolText>(world, b_id)?;
1044            let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1045            assert_eq!(b_text.text, "b");
1046            assert_eq!(b_text.embedded, "");
1047            assert!(b_load.is_loaded());
1048            assert!(b_deps.is_loaded());
1049            assert!(b_rec_deps.is_loaded());
1050
1051            let c_id = a_text.dependencies[1].id();
1052            let c_text = get::<CoolText>(world, c_id)?;
1053            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1054            assert_eq!(c_text.text, "c");
1055            assert_eq!(c_text.embedded, "ab");
1056            assert!(c_load.is_loaded());
1057            assert!(
1058                c_deps.is_loading(),
1059                "c deps should not be loaded yet because d has not loaded"
1060            );
1061            assert!(
1062                c_rec_deps.is_loading(),
1063                "c rec deps should not be loaded yet because d has not loaded"
1064            );
1065
1066            let sub_text_id = c_text.sub_texts[0].id();
1067            let sub_text = get::<SubText>(world, sub_text_id)
1068                .expect("subtext should exist if c exists. it came from the same loader");
1069            assert_eq!(sub_text.text, "hello");
1070            let (sub_text_load, sub_text_deps, sub_text_rec_deps) =
1071                asset_server.get_load_states(sub_text_id).unwrap();
1072            assert!(sub_text_load.is_loaded());
1073            assert!(sub_text_deps.is_loaded());
1074            assert!(sub_text_rec_deps.is_loaded());
1075
1076            let d_id = c_text.dependencies[0].id();
1077            let d_text = get::<CoolText>(world, d_id);
1078            let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1079            assert!(d_text.is_none(), "d component should not exist yet");
1080            assert!(d_load.is_loading());
1081            assert!(d_deps.is_loading());
1082            assert!(d_rec_deps.is_loading());
1083
1084            assert!(
1085                a_deps.is_loaded(),
1086                "If c has been loaded, the a deps should all be considered loaded"
1087            );
1088            assert!(
1089                a_rec_deps.is_loading(),
1090                "d is not loaded, so a's recursive deps should still be loading"
1091            );
1092            world.insert_resource(IdResults { b_id, c_id, d_id });
1093            Some(())
1094        });
1095
1096        gate_opener.open(d_path);
1097        run_app_until(&mut app, |world| {
1098            let a_text = get::<CoolText>(world, a_id)?;
1099            let (_a_load, _a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1100            let c_id = a_text.dependencies[1].id();
1101            let c_text = get::<CoolText>(world, c_id)?;
1102            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1103            assert_eq!(c_text.text, "c");
1104            assert_eq!(c_text.embedded, "ab");
1105
1106            let d_id = c_text.dependencies[0].id();
1107            let d_text = get::<CoolText>(world, d_id)?;
1108            let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1109            assert_eq!(d_text.text, "d");
1110            assert_eq!(d_text.embedded, "");
1111
1112            assert!(c_load.is_loaded());
1113            assert!(c_deps.is_loaded());
1114            assert!(c_rec_deps.is_loaded());
1115
1116            assert!(d_load.is_loaded());
1117            assert!(d_deps.is_loaded());
1118            assert!(d_rec_deps.is_loaded());
1119
1120            assert!(
1121                a_rec_deps.is_loaded(),
1122                "d is loaded, so a's recursive deps should be loaded"
1123            );
1124            Some(())
1125        });
1126
1127        {
1128            let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1129            let a = texts.get_mut(a_id).unwrap();
1130            a.text = "Changed".to_string();
1131        }
1132
1133        drop(handle);
1134
1135        app.update();
1136        assert_eq!(
1137            app.world().resource::<Assets<CoolText>>().len(),
1138            0,
1139            "CoolText asset entities should be despawned when no more handles exist"
1140        );
1141        app.update();
1142        // this requires a second update because the parent asset was freed in the previous app.update()
1143        assert_eq!(
1144            app.world().resource::<Assets<SubText>>().len(),
1145            0,
1146            "SubText asset entities should be despawned when no more handles exist"
1147        );
1148        let events = app.world_mut().remove_resource::<StoredEvents>().unwrap();
1149        let id_results = app.world_mut().remove_resource::<IdResults>().unwrap();
1150        let expected_events = vec![
1151            AssetEvent::Added { id: a_id },
1152            AssetEvent::LoadedWithDependencies {
1153                id: id_results.b_id,
1154            },
1155            AssetEvent::Added {
1156                id: id_results.b_id,
1157            },
1158            AssetEvent::Added {
1159                id: id_results.c_id,
1160            },
1161            AssetEvent::LoadedWithDependencies {
1162                id: id_results.d_id,
1163            },
1164            AssetEvent::LoadedWithDependencies {
1165                id: id_results.c_id,
1166            },
1167            AssetEvent::LoadedWithDependencies { id: a_id },
1168            AssetEvent::Added {
1169                id: id_results.d_id,
1170            },
1171            AssetEvent::Modified { id: a_id },
1172            AssetEvent::Unused { id: a_id },
1173            AssetEvent::Removed { id: a_id },
1174            AssetEvent::Unused {
1175                id: id_results.b_id,
1176            },
1177            AssetEvent::Removed {
1178                id: id_results.b_id,
1179            },
1180            AssetEvent::Unused {
1181                id: id_results.c_id,
1182            },
1183            AssetEvent::Removed {
1184                id: id_results.c_id,
1185            },
1186            AssetEvent::Unused {
1187                id: id_results.d_id,
1188            },
1189            AssetEvent::Removed {
1190                id: id_results.d_id,
1191            },
1192        ];
1193        assert_eq!(events.0, expected_events);
1194    }
1195
1196    #[test]
1197    fn failure_load_states() {
1198        // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded
1199        #[cfg(not(feature = "multi_threaded"))]
1200        panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1201
1202        let dir = Dir::default();
1203
1204        let a_path = "a.cool.ron";
1205        let a_ron = r#"
1206(
1207    text: "a",
1208    dependencies: [
1209        "b.cool.ron",
1210        "c.cool.ron",
1211    ],
1212    embedded_dependencies: [],
1213    sub_texts: []
1214)"#;
1215        let b_path = "b.cool.ron";
1216        let b_ron = r#"
1217(
1218    text: "b",
1219    dependencies: [],
1220    embedded_dependencies: [],
1221    sub_texts: []
1222)"#;
1223
1224        let c_path = "c.cool.ron";
1225        let c_ron = r#"
1226(
1227    text: "c",
1228    dependencies: [
1229        "d.cool.ron",
1230    ],
1231    embedded_dependencies: [],
1232    sub_texts: []
1233)"#;
1234
1235        let d_path = "d.cool.ron";
1236        let d_ron = r#"
1237(
1238    text: "d",
1239    dependencies: [],
1240    OH NO THIS ASSET IS MALFORMED
1241    embedded_dependencies: [],
1242    sub_texts: []
1243)"#;
1244
1245        dir.insert_asset_text(Path::new(a_path), a_ron);
1246        dir.insert_asset_text(Path::new(b_path), b_ron);
1247        dir.insert_asset_text(Path::new(c_path), c_ron);
1248        dir.insert_asset_text(Path::new(d_path), d_ron);
1249
1250        let (mut app, gate_opener) = test_app(dir);
1251        app.init_asset::<CoolText>()
1252            .register_asset_loader(CoolTextLoader);
1253        let asset_server = app.world().resource::<AssetServer>().clone();
1254        let handle: Handle<CoolText> = asset_server.load(a_path);
1255        let a_id = handle.id();
1256        {
1257            let other_handle: Handle<CoolText> = asset_server.load(a_path);
1258            assert_eq!(
1259                other_handle, handle,
1260                "handles from consecutive load calls should be equal"
1261            );
1262            assert_eq!(
1263                other_handle.id(),
1264                handle.id(),
1265                "handle ids from consecutive load calls should be equal"
1266            );
1267        }
1268
1269        gate_opener.open(a_path);
1270        gate_opener.open(b_path);
1271        gate_opener.open(c_path);
1272        gate_opener.open(d_path);
1273
1274        run_app_until(&mut app, |world| {
1275            let a_text = get::<CoolText>(world, a_id)?;
1276            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1277
1278            let b_id = a_text.dependencies[0].id();
1279            let b_text = get::<CoolText>(world, b_id)?;
1280            let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1281
1282            let c_id = a_text.dependencies[1].id();
1283            let c_text = get::<CoolText>(world, c_id)?;
1284            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1285
1286            let d_id = c_text.dependencies[0].id();
1287            let d_text = get::<CoolText>(world, d_id);
1288            let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1289
1290            if !d_load.is_failed() {
1291                // wait until d has exited the loading state
1292                return None;
1293            }
1294
1295            assert!(d_text.is_none());
1296            assert!(d_load.is_failed());
1297            assert!(d_deps.is_failed());
1298            assert!(d_rec_deps.is_failed());
1299
1300            assert_eq!(a_text.text, "a");
1301            assert!(a_load.is_loaded());
1302            assert!(a_deps.is_loaded());
1303            assert!(a_rec_deps.is_failed());
1304
1305            assert_eq!(b_text.text, "b");
1306            assert!(b_load.is_loaded());
1307            assert!(b_deps.is_loaded());
1308            assert!(b_rec_deps.is_loaded());
1309
1310            assert_eq!(c_text.text, "c");
1311            assert!(c_load.is_loaded());
1312            assert!(c_deps.is_failed());
1313            assert!(c_rec_deps.is_failed());
1314
1315            assert!(asset_server.load_state(a_id).is_loaded());
1316            assert!(asset_server.dependency_load_state(a_id).is_loaded());
1317            assert!(asset_server
1318                .recursive_dependency_load_state(a_id)
1319                .is_failed());
1320
1321            assert!(asset_server.is_loaded(a_id));
1322            assert!(asset_server.is_loaded_with_direct_dependencies(a_id));
1323            assert!(!asset_server.is_loaded_with_dependencies(a_id));
1324
1325            Some(())
1326        });
1327    }
1328
1329    #[test]
1330    fn dependency_load_states() {
1331        // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded
1332        #[cfg(not(feature = "multi_threaded"))]
1333        panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1334
1335        let a_path = "a.cool.ron";
1336        let a_ron = r#"
1337(
1338    text: "a",
1339    dependencies: [
1340        "b.cool.ron",
1341        "c.cool.ron",
1342    ],
1343    embedded_dependencies: [],
1344    sub_texts: []
1345)"#;
1346        let b_path = "b.cool.ron";
1347        let b_ron = r#"
1348(
1349    text: "b",
1350    dependencies: [],
1351    MALFORMED
1352    embedded_dependencies: [],
1353    sub_texts: []
1354)"#;
1355
1356        let c_path = "c.cool.ron";
1357        let c_ron = r#"
1358(
1359    text: "c",
1360    dependencies: [],
1361    embedded_dependencies: [],
1362    sub_texts: []
1363)"#;
1364
1365        let dir = Dir::default();
1366        dir.insert_asset_text(Path::new(a_path), a_ron);
1367        dir.insert_asset_text(Path::new(b_path), b_ron);
1368        dir.insert_asset_text(Path::new(c_path), c_ron);
1369
1370        let (mut app, gate_opener) = test_app(dir);
1371        app.init_asset::<CoolText>()
1372            .register_asset_loader(CoolTextLoader);
1373        let asset_server = app.world().resource::<AssetServer>().clone();
1374        let handle: Handle<CoolText> = asset_server.load(a_path);
1375        let a_id = handle.id();
1376
1377        gate_opener.open(a_path);
1378        run_app_until(&mut app, |world| {
1379            let _a_text = get::<CoolText>(world, a_id)?;
1380            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1381            assert!(a_load.is_loaded());
1382            assert!(a_deps.is_loading());
1383            assert!(a_rec_deps.is_loading());
1384            Some(())
1385        });
1386
1387        gate_opener.open(b_path);
1388        run_app_until(&mut app, |world| {
1389            let a_text = get::<CoolText>(world, a_id)?;
1390            let b_id = a_text.dependencies[0].id();
1391
1392            let (b_load, _b_deps, _b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1393            if !b_load.is_failed() {
1394                // wait until b fails
1395                return None;
1396            }
1397
1398            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1399            assert!(a_load.is_loaded());
1400            assert!(a_deps.is_failed());
1401            assert!(a_rec_deps.is_failed());
1402            Some(())
1403        });
1404
1405        gate_opener.open(c_path);
1406        run_app_until(&mut app, |world| {
1407            let a_text = get::<CoolText>(world, a_id)?;
1408            let c_id = a_text.dependencies[1].id();
1409            // wait until c loads
1410            let _c_text = get::<CoolText>(world, c_id)?;
1411
1412            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1413            assert!(a_load.is_loaded());
1414            assert!(
1415                a_deps.is_failed(),
1416                "Successful dependency load should not overwrite a previous failure"
1417            );
1418            assert!(
1419                a_rec_deps.is_failed(),
1420                "Successful dependency load should not overwrite a previous failure"
1421            );
1422            Some(())
1423        });
1424    }
1425
1426    const SIMPLE_TEXT: &str = r#"
1427(
1428    text: "dep",
1429    dependencies: [],
1430    embedded_dependencies: [],
1431    sub_texts: [],
1432)"#;
1433    #[test]
1434    fn keep_gotten_strong_handles() {
1435        let dir = Dir::default();
1436        dir.insert_asset_text(Path::new("dep.cool.ron"), SIMPLE_TEXT);
1437
1438        let (mut app, _) = test_app(dir);
1439        app.init_asset::<CoolText>()
1440            .init_asset::<SubText>()
1441            .init_resource::<StoredEvents>()
1442            .register_asset_loader(CoolTextLoader)
1443            .add_systems(Update, store_asset_events);
1444
1445        let id = {
1446            let handle = {
1447                let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1448                let handle = texts.add(CoolText::default());
1449                texts.get_strong_handle(handle.id()).unwrap()
1450            };
1451
1452            app.update();
1453
1454            {
1455                let text = app.world().resource::<Assets<CoolText>>().get(&handle);
1456                assert!(text.is_some());
1457            }
1458            handle.id()
1459        };
1460        // handle is dropped
1461        app.update();
1462        assert!(
1463            app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1464            "asset has no handles, so it should have been dropped last update"
1465        );
1466    }
1467
1468    #[test]
1469    fn manual_asset_management() {
1470        // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded
1471        #[cfg(not(feature = "multi_threaded"))]
1472        panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1473
1474        let dir = Dir::default();
1475        let dep_path = "dep.cool.ron";
1476
1477        dir.insert_asset_text(Path::new(dep_path), SIMPLE_TEXT);
1478
1479        let (mut app, gate_opener) = test_app(dir);
1480        app.init_asset::<CoolText>()
1481            .init_asset::<SubText>()
1482            .init_resource::<StoredEvents>()
1483            .register_asset_loader(CoolTextLoader)
1484            .add_systems(Update, store_asset_events);
1485
1486        let hello = "hello".to_string();
1487        let empty = "".to_string();
1488
1489        let id = {
1490            let handle = {
1491                let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1492                texts.add(CoolText {
1493                    text: hello.clone(),
1494                    embedded: empty.clone(),
1495                    dependencies: vec![],
1496                    sub_texts: Vec::new(),
1497                })
1498            };
1499
1500            app.update();
1501
1502            {
1503                let text = app
1504                    .world()
1505                    .resource::<Assets<CoolText>>()
1506                    .get(&handle)
1507                    .unwrap();
1508                assert_eq!(text.text, hello);
1509            }
1510            handle.id()
1511        };
1512        // handle is dropped
1513        app.update();
1514        assert!(
1515            app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1516            "asset has no handles, so it should have been dropped last update"
1517        );
1518        // remove event is emitted
1519        app.update();
1520        let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1521        let expected_events = vec![
1522            AssetEvent::Added { id },
1523            AssetEvent::Unused { id },
1524            AssetEvent::Removed { id },
1525        ];
1526        assert_eq!(events, expected_events);
1527
1528        let dep_handle = app.world().resource::<AssetServer>().load(dep_path);
1529        let a = CoolText {
1530            text: "a".to_string(),
1531            embedded: empty,
1532            // this dependency is behind a manual load gate, which should prevent 'a' from emitting a LoadedWithDependencies event
1533            dependencies: vec![dep_handle.clone()],
1534            sub_texts: Vec::new(),
1535        };
1536        let a_handle = app.world().resource::<AssetServer>().load_asset(a);
1537        app.update();
1538        // TODO: ideally it doesn't take two updates for the added event to emit
1539        app.update();
1540
1541        let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1542        let expected_events = vec![AssetEvent::Added { id: a_handle.id() }];
1543        assert_eq!(events, expected_events);
1544
1545        gate_opener.open(dep_path);
1546        loop {
1547            app.update();
1548            let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1549            if events.is_empty() {
1550                continue;
1551            }
1552            let expected_events = vec![
1553                AssetEvent::LoadedWithDependencies {
1554                    id: dep_handle.id(),
1555                },
1556                AssetEvent::LoadedWithDependencies { id: a_handle.id() },
1557            ];
1558            assert_eq!(events, expected_events);
1559            break;
1560        }
1561        app.update();
1562        let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1563        let expected_events = vec![AssetEvent::Added {
1564            id: dep_handle.id(),
1565        }];
1566        assert_eq!(events, expected_events);
1567    }
1568
1569    #[test]
1570    fn load_folder() {
1571        // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded
1572        #[cfg(not(feature = "multi_threaded"))]
1573        panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
1574
1575        let dir = Dir::default();
1576
1577        let a_path = "text/a.cool.ron";
1578        let a_ron = r#"
1579(
1580    text: "a",
1581    dependencies: [
1582        "b.cool.ron",
1583    ],
1584    embedded_dependencies: [],
1585    sub_texts: [],
1586)"#;
1587        let b_path = "b.cool.ron";
1588        let b_ron = r#"
1589(
1590    text: "b",
1591    dependencies: [],
1592    embedded_dependencies: [],
1593    sub_texts: [],
1594)"#;
1595
1596        let c_path = "text/c.cool.ron";
1597        let c_ron = r#"
1598(
1599    text: "c",
1600    dependencies: [
1601    ],
1602    embedded_dependencies: [],
1603    sub_texts: [],
1604)"#;
1605        dir.insert_asset_text(Path::new(a_path), a_ron);
1606        dir.insert_asset_text(Path::new(b_path), b_ron);
1607        dir.insert_asset_text(Path::new(c_path), c_ron);
1608
1609        let (mut app, gate_opener) = test_app(dir);
1610        app.init_asset::<CoolText>()
1611            .init_asset::<SubText>()
1612            .register_asset_loader(CoolTextLoader);
1613        let asset_server = app.world().resource::<AssetServer>().clone();
1614        let handle: Handle<LoadedFolder> = asset_server.load_folder("text");
1615        gate_opener.open(a_path);
1616        gate_opener.open(b_path);
1617        gate_opener.open(c_path);
1618
1619        let mut reader = EventCursor::default();
1620        run_app_until(&mut app, |world| {
1621            let events = world.resource::<Events<AssetEvent<LoadedFolder>>>();
1622            let asset_server = world.resource::<AssetServer>();
1623            let loaded_folders = world.resource::<Assets<LoadedFolder>>();
1624            let cool_texts = world.resource::<Assets<CoolText>>();
1625            for event in reader.read(events) {
1626                if let AssetEvent::LoadedWithDependencies { id } = event {
1627                    if *id == handle.id() {
1628                        let loaded_folder = loaded_folders.get(&handle).unwrap();
1629                        let a_handle: Handle<CoolText> =
1630                            asset_server.get_handle("text/a.cool.ron").unwrap();
1631                        let c_handle: Handle<CoolText> =
1632                            asset_server.get_handle("text/c.cool.ron").unwrap();
1633
1634                        let mut found_a = false;
1635                        let mut found_c = false;
1636                        for asset_handle in &loaded_folder.handles {
1637                            if asset_handle.id() == a_handle.id().untyped() {
1638                                found_a = true;
1639                            } else if asset_handle.id() == c_handle.id().untyped() {
1640                                found_c = true;
1641                            }
1642                        }
1643                        assert!(found_a);
1644                        assert!(found_c);
1645                        assert_eq!(loaded_folder.handles.len(), 2);
1646
1647                        let a_text = cool_texts.get(&a_handle).unwrap();
1648                        let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();
1649                        let c_text = cool_texts.get(&c_handle).unwrap();
1650
1651                        assert_eq!("a", a_text.text);
1652                        assert_eq!("b", b_text.text);
1653                        assert_eq!("c", c_text.text);
1654
1655                        return Some(());
1656                    }
1657                }
1658            }
1659            None
1660        });
1661    }
1662
1663    /// Tests that `AssetLoadFailedEvent<A>` events are emitted and can be used to retry failed assets.
1664    #[test]
1665    fn load_error_events() {
1666        #[derive(Resource, Default)]
1667        struct ErrorTracker {
1668            tick: u64,
1669            failures: usize,
1670            queued_retries: Vec<(AssetPath<'static>, AssetId<CoolText>, u64)>,
1671            finished_asset: Option<AssetId<CoolText>>,
1672        }
1673
1674        fn asset_event_handler(
1675            mut events: EventReader<AssetEvent<CoolText>>,
1676            mut tracker: ResMut<ErrorTracker>,
1677        ) {
1678            for event in events.read() {
1679                if let AssetEvent::LoadedWithDependencies { id } = event {
1680                    tracker.finished_asset = Some(*id);
1681                }
1682            }
1683        }
1684
1685        fn asset_load_error_event_handler(
1686            server: Res<AssetServer>,
1687            mut errors: EventReader<AssetLoadFailedEvent<CoolText>>,
1688            mut tracker: ResMut<ErrorTracker>,
1689        ) {
1690            // In the real world, this would refer to time (not ticks)
1691            tracker.tick += 1;
1692
1693            // Retry loading past failed items
1694            let now = tracker.tick;
1695            tracker
1696                .queued_retries
1697                .retain(|(path, old_id, retry_after)| {
1698                    if now > *retry_after {
1699                        let new_handle = server.load::<CoolText>(path);
1700                        assert_eq!(&new_handle.id(), old_id);
1701                        false
1702                    } else {
1703                        true
1704                    }
1705                });
1706
1707            // Check what just failed
1708            for error in errors.read() {
1709                let (load_state, _, _) = server.get_load_states(error.id).unwrap();
1710                assert!(load_state.is_failed());
1711                assert_eq!(*error.path.source(), AssetSourceId::Name("unstable".into()));
1712                match &error.error {
1713                    AssetLoadError::AssetReaderError(read_error) => match read_error {
1714                        AssetReaderError::Io(_) => {
1715                            tracker.failures += 1;
1716                            if tracker.failures <= 2 {
1717                                // Retry in 10 ticks
1718                                tracker.queued_retries.push((
1719                                    error.path.clone(),
1720                                    error.id,
1721                                    now + 10,
1722                                ));
1723                            } else {
1724                                panic!(
1725                                    "Unexpected failure #{} (expected only 2)",
1726                                    tracker.failures
1727                                );
1728                            }
1729                        }
1730                        _ => panic!("Unexpected error type {}", read_error),
1731                    },
1732                    _ => panic!("Unexpected error type {}", error.error),
1733                }
1734            }
1735        }
1736
1737        let a_path = "text/a.cool.ron";
1738        let a_ron = r#"
1739(
1740    text: "a",
1741    dependencies: [],
1742    embedded_dependencies: [],
1743    sub_texts: [],
1744)"#;
1745
1746        let dir = Dir::default();
1747        dir.insert_asset_text(Path::new(a_path), a_ron);
1748        let unstable_reader = UnstableMemoryAssetReader::new(dir, 2);
1749
1750        let mut app = App::new();
1751        app.register_asset_source(
1752            "unstable",
1753            AssetSource::build().with_reader(move || Box::new(unstable_reader.clone())),
1754        )
1755        .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
1756        .init_asset::<CoolText>()
1757        .register_asset_loader(CoolTextLoader)
1758        .init_resource::<ErrorTracker>()
1759        .add_systems(
1760            Update,
1761            (asset_event_handler, asset_load_error_event_handler).chain(),
1762        );
1763
1764        let asset_server = app.world().resource::<AssetServer>().clone();
1765        let a_path = format!("unstable://{a_path}");
1766        let a_handle: Handle<CoolText> = asset_server.load(a_path);
1767        let a_id = a_handle.id();
1768
1769        run_app_until(&mut app, |world| {
1770            let tracker = world.resource::<ErrorTracker>();
1771            match tracker.finished_asset {
1772                Some(asset_id) => {
1773                    assert_eq!(asset_id, a_id);
1774                    let assets = world.resource::<Assets<CoolText>>();
1775                    let result = assets.get(asset_id).unwrap();
1776                    assert_eq!(result.text, "a");
1777                    Some(())
1778                }
1779                None => None,
1780            }
1781        });
1782    }
1783
1784    #[test]
1785    fn ignore_system_ambiguities_on_assets() {
1786        let mut app = App::new();
1787        app.add_plugins(AssetPlugin::default())
1788            .init_asset::<CoolText>();
1789
1790        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1791        app.add_systems(Update, (uses_assets, uses_assets));
1792        app.edit_schedule(Update, |s| {
1793            s.set_build_settings(ScheduleBuildSettings {
1794                ambiguity_detection: LogLevel::Error,
1795                ..Default::default()
1796            });
1797        });
1798
1799        // running schedule does not error on ambiguity between the 2 uses_assets systems
1800        app.world_mut().run_schedule(Update);
1801    }
1802
1803    // This test is not checking a requirement, but documenting a current limitation. We simply are
1804    // not capable of loading subassets when doing nested immediate loads.
1805    #[test]
1806    fn error_on_nested_immediate_load_of_subasset() {
1807        let mut app = App::new();
1808
1809        let dir = Dir::default();
1810        dir.insert_asset_text(
1811            Path::new("a.cool.ron"),
1812            r#"(
1813    text: "b",
1814    dependencies: [],
1815    embedded_dependencies: [],
1816    sub_texts: ["A"],
1817)"#,
1818        );
1819        dir.insert_asset_text(Path::new("empty.txt"), "");
1820
1821        app.register_asset_source(
1822            AssetSourceId::Default,
1823            AssetSource::build()
1824                .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() })),
1825        )
1826        .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()));
1827
1828        app.init_asset::<CoolText>()
1829            .init_asset::<SubText>()
1830            .register_asset_loader(CoolTextLoader);
1831
1832        struct NestedLoadOfSubassetLoader;
1833
1834        impl AssetLoader for NestedLoadOfSubassetLoader {
1835            type Asset = TestAsset;
1836            type Error = crate::loader::LoadDirectError;
1837            type Settings = ();
1838
1839            async fn load(
1840                &self,
1841                _: &mut dyn Reader,
1842                _: &Self::Settings,
1843                load_context: &mut LoadContext<'_>,
1844            ) -> Result<Self::Asset, Self::Error> {
1845                // We expect this load to fail.
1846                load_context
1847                    .loader()
1848                    .immediate()
1849                    .load::<SubText>("a.cool.ron#A")
1850                    .await?;
1851                Ok(TestAsset)
1852            }
1853
1854            fn extensions(&self) -> &[&str] {
1855                &["txt"]
1856            }
1857        }
1858
1859        app.init_asset::<TestAsset>()
1860            .register_asset_loader(NestedLoadOfSubassetLoader);
1861
1862        let asset_server = app.world().resource::<AssetServer>().clone();
1863        let handle = asset_server.load::<TestAsset>("empty.txt");
1864
1865        run_app_until(&mut app, |_world| match asset_server.load_state(&handle) {
1866            LoadState::Loading => None,
1867            LoadState::Failed(err) => {
1868                let error_message = format!("{err}");
1869                assert!(error_message.contains("Requested to load an asset path (a.cool.ron#A) with a subasset, but this is unsupported"), "what? \"{error_message}\"");
1870                Some(())
1871            }
1872            state => panic!("Unexpected asset state: {state:?}"),
1873        });
1874    }
1875
1876    // validate the Asset derive macro for various asset types
1877    #[derive(Asset, TypePath)]
1878    pub struct TestAsset;
1879
1880    #[derive(Asset, TypePath)]
1881    #[expect(
1882        dead_code,
1883        reason = "This exists to ensure that `#[derive(Asset)]` works on enums. The inner variants are known not to be used."
1884    )]
1885    pub enum EnumTestAsset {
1886        Unnamed(#[dependency] Handle<TestAsset>),
1887        Named {
1888            #[dependency]
1889            handle: Handle<TestAsset>,
1890            #[dependency]
1891            vec_handles: Vec<Handle<TestAsset>>,
1892            #[dependency]
1893            embedded: TestAsset,
1894        },
1895        StructStyle(#[dependency] TestAsset),
1896        Empty,
1897    }
1898
1899    #[derive(Asset, TypePath)]
1900    pub struct StructTestAsset {
1901        #[dependency]
1902        handle: Handle<TestAsset>,
1903        #[dependency]
1904        embedded: TestAsset,
1905    }
1906
1907    #[derive(Asset, TypePath)]
1908    pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);
1909
1910    fn unapproved_path_setup(mode: UnapprovedPathMode) -> App {
1911        let dir = Dir::default();
1912        let a_path = "../a.cool.ron";
1913        let a_ron = r#"
1914(
1915    text: "a",
1916    dependencies: [],
1917    embedded_dependencies: [],
1918    sub_texts: [],
1919)"#;
1920
1921        dir.insert_asset_text(Path::new(a_path), a_ron);
1922
1923        let mut app = App::new();
1924        let memory_reader = MemoryAssetReader { root: dir };
1925        app.register_asset_source(
1926            AssetSourceId::Default,
1927            AssetSource::build().with_reader(move || Box::new(memory_reader.clone())),
1928        )
1929        .add_plugins((
1930            TaskPoolPlugin::default(),
1931            AssetPlugin {
1932                unapproved_path_mode: mode,
1933                ..Default::default()
1934            },
1935        ));
1936        app.init_asset::<CoolText>();
1937
1938        app
1939    }
1940
1941    fn load_a_asset(assets: Res<AssetServer>) {
1942        let a = assets.load::<CoolText>("../a.cool.ron");
1943        if a == Handle::default() {
1944            panic!()
1945        }
1946    }
1947
1948    fn load_a_asset_override(assets: Res<AssetServer>) {
1949        let a = assets.load_override::<CoolText>("../a.cool.ron");
1950        if a == Handle::default() {
1951            panic!()
1952        }
1953    }
1954
1955    #[test]
1956    #[should_panic]
1957    fn unapproved_path_forbid_should_panic() {
1958        let mut app = unapproved_path_setup(UnapprovedPathMode::Forbid);
1959
1960        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1961        app.add_systems(Update, (uses_assets, load_a_asset_override));
1962
1963        app.world_mut().run_schedule(Update);
1964    }
1965
1966    #[test]
1967    #[should_panic]
1968    fn unapproved_path_deny_should_panic() {
1969        let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);
1970
1971        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1972        app.add_systems(Update, (uses_assets, load_a_asset));
1973
1974        app.world_mut().run_schedule(Update);
1975    }
1976
1977    #[test]
1978    fn unapproved_path_deny_should_finish() {
1979        let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);
1980
1981        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1982        app.add_systems(Update, (uses_assets, load_a_asset_override));
1983
1984        app.world_mut().run_schedule(Update);
1985    }
1986
1987    #[test]
1988    fn unapproved_path_allow_should_finish() {
1989        let mut app = unapproved_path_setup(UnapprovedPathMode::Allow);
1990
1991        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1992        app.add_systems(Update, (uses_assets, load_a_asset));
1993
1994        app.world_mut().run_schedule(Update);
1995    }
1996}