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