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;
191use bevy_diagnostic::{Diagnostic, DiagnosticsStore, RegisterDiagnostic};
192pub use direct_access_ext::DirectAssetAccessExt;
193pub use event::*;
194pub use folder::*;
195pub use futures_lite::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
196pub use handle::*;
197pub use id::*;
198pub use loader::*;
199pub use loader_builders::{
200    Deferred, DynamicTyped, Immediate, NestedLoader, StaticTyped, UnknownTyped,
201};
202pub use path::*;
203pub use reflect::*;
204pub use render_asset::*;
205pub use server::*;
206
207pub use uuid;
208
209use crate::{
210    io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId},
211    processor::{AssetProcessor, Process},
212};
213use alloc::{
214    string::{String, ToString},
215    sync::Arc,
216    vec::Vec,
217};
218use bevy_app::{App, Plugin, PostUpdate, PreUpdate};
219use bevy_ecs::{prelude::Component, schedule::common_conditions::resource_exists};
220use bevy_ecs::{
221    reflect::AppTypeRegistry,
222    schedule::{IntoScheduleConfigs, SystemSet},
223    world::FromWorld,
224};
225use bevy_platform::collections::HashSet;
226use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath};
227use core::any::TypeId;
228use tracing::error;
229
230/// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetSource`],
231/// which can be something like a filesystem, a network, etc.
232///
233/// Supports flexible "modes", such as [`AssetMode::Processed`] and
234/// [`AssetMode::Unprocessed`] that enable using the asset workflow that best suits your project.
235///
236/// [`AssetSource`]: io::AssetSource
237pub struct AssetPlugin {
238    /// The default file path to use (relative to the project root) for unprocessed assets.
239    pub file_path: String,
240    /// The default file path to use (relative to the project root) for processed assets.
241    pub processed_file_path: String,
242    /// If set, will override the default "watch for changes" setting. By default "watch for changes" will be `false` unless
243    /// the `watch` cargo feature is set. `watch` can be enabled manually, or it will be automatically enabled if a specific watcher
244    /// like `file_watcher` is enabled.
245    ///
246    /// Most use cases should leave this set to [`None`] and enable a specific watcher feature such as `file_watcher` to enable
247    /// watching for dev-scenarios.
248    pub watch_for_changes_override: Option<bool>,
249    /// If set, will override the default "use asset processor" setting. By default "use asset
250    /// processor" will be `false` unless the `asset_processor` cargo feature is set.
251    ///
252    /// Most use cases should leave this set to [`None`] and enable the `asset_processor` cargo
253    /// feature.
254    pub use_asset_processor_override: Option<bool>,
255    /// The [`AssetMode`] to use for this server.
256    pub mode: AssetMode,
257    /// How/If asset meta files should be checked.
258    pub meta_check: AssetMetaCheck,
259    /// How to handle load requests of files that are outside the approved directories.
260    ///
261    /// Approved folders are [`AssetPlugin::file_path`] and the folder of each
262    /// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
263    pub unapproved_path_mode: UnapprovedPathMode,
264}
265
266/// Determines how to react to attempts to load assets not inside the approved folders.
267///
268/// Approved folders are [`AssetPlugin::file_path`] and the folder of each
269/// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
270///
271/// It is strongly discouraged to use [`Allow`](UnapprovedPathMode::Allow) if your
272/// app will include scripts or modding support, as it could allow arbitrary file
273/// access for malicious code.
274///
275/// The default value is [`Forbid`](UnapprovedPathMode::Forbid).
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 unapproved, unless an override method is used, like
283    /// [`AssetServer::load_override`].
284    Deny,
285    /// Fails to load any asset that 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            use_asset_processor_override: None,
341            meta_check: AssetMetaCheck::default(),
342            unapproved_path_mode: UnapprovedPathMode::default(),
343        }
344    }
345}
346
347impl AssetPlugin {
348    const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets";
349    /// NOTE: this is in the Default sub-folder to make this forward compatible with "import profiles"
350    /// and to allow us to put the "processor transaction log" at `imported_assets/log`
351    const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default";
352}
353
354impl Plugin for AssetPlugin {
355    fn build(&self, app: &mut App) {
356        let embedded = EmbeddedAssetRegistry::default();
357        {
358            let mut sources = app
359                .world_mut()
360                .get_resource_or_init::<AssetSourceBuilders>();
361            sources.init_default_source(
362                &self.file_path,
363                (!matches!(self.mode, AssetMode::Unprocessed))
364                    .then_some(self.processed_file_path.as_str()),
365            );
366            embedded.register_source(&mut sources);
367        }
368        {
369            let watch = self
370                .watch_for_changes_override
371                .unwrap_or(cfg!(feature = "watch"));
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                        Arc::new(sources),
379                        AssetServerMode::Unprocessed,
380                        self.meta_check.clone(),
381                        watch,
382                        self.unapproved_path_mode.clone(),
383                    ));
384                }
385                AssetMode::Processed => {
386                    let use_asset_processor = self
387                        .use_asset_processor_override
388                        .unwrap_or(cfg!(feature = "asset_processor"));
389                    if use_asset_processor {
390                        let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
391                        let (processor, sources) = AssetProcessor::new(&mut builders, watch);
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                    } else {
404                        let mut builders = app.world_mut().resource_mut::<AssetSourceBuilders>();
405                        let sources = builders.build_sources(false, watch);
406                        app.insert_resource(AssetServer::new_with_meta_check(
407                            Arc::new(sources),
408                            AssetServerMode::Processed,
409                            AssetMetaCheck::Always,
410                            watch,
411                            self.unapproved_path_mode.clone(),
412                        ));
413                    }
414                }
415            }
416        }
417        app.insert_resource(embedded)
418            .init_asset::<LoadedFolder>()
419            .init_asset::<LoadedUntypedAsset>()
420            .init_asset::<()>()
421            .add_message::<UntypedAssetLoadFailedEvent>()
422            .configure_sets(
423                PreUpdate,
424                AssetTrackingSystems.after(handle_internal_asset_events),
425            )
426            // `handle_internal_asset_events` requires the use of `&mut World`,
427            // and as a result has ambiguous system ordering with all other systems in `PreUpdate`.
428            // This is virtually never a real problem: asset loading is async and so anything that interacts directly with it
429            // needs to be robust to stochastic delays anyways.
430            .add_systems(
431                PreUpdate,
432                (
433                    handle_internal_asset_events.ambiguous_with_all(),
434                    // TODO: Remove the run condition and use `If` once
435                    // https://github.com/bevyengine/bevy/issues/21549 is resolved.
436                    publish_asset_server_diagnostics.run_if(resource_exists::<DiagnosticsStore>),
437                )
438                    .chain(),
439            )
440            .register_diagnostic(Diagnostic::new(AssetServer::STARTED_LOAD_COUNT));
441    }
442}
443
444/// Declares that this type is an asset,
445/// which can be loaded and managed by the [`AssetServer`] and stored in [`Assets`] collections.
446///
447/// Generally, assets are large, complex, and/or expensive to load from disk, and are often authored by artists or designers.
448///
449/// [`TypePath`] is largely used for diagnostic purposes, and should almost always be implemented by deriving [`Reflect`] on your type.
450/// [`VisitAssetDependencies`] is used to track asset dependencies, and an implementation is automatically generated when deriving [`Asset`].
451#[diagnostic::on_unimplemented(
452    message = "`{Self}` is not an `Asset`",
453    label = "invalid `Asset`",
454    note = "consider annotating `{Self}` with `#[derive(Asset)]`"
455)]
456pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {}
457
458/// A trait for components that can be used as asset identifiers, e.g. handle wrappers.
459pub trait AsAssetId: Component {
460    /// The underlying asset type.
461    type Asset: Asset;
462
463    /// Retrieves the asset id from this component.
464    fn as_asset_id(&self) -> AssetId<Self::Asset>;
465}
466
467/// This trait defines how to visit the dependencies of an asset.
468/// For example, a 3D model might require both textures and meshes to be loaded.
469///
470/// Note that this trait is automatically implemented when deriving [`Asset`].
471pub trait VisitAssetDependencies {
472    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId));
473}
474
475impl<A: Asset> VisitAssetDependencies for Handle<A> {
476    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
477        visit(self.id().untyped());
478    }
479}
480
481impl<A: Asset> VisitAssetDependencies for Option<Handle<A>> {
482    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
483        if let Some(handle) = self {
484            visit(handle.id().untyped());
485        }
486    }
487}
488
489impl VisitAssetDependencies for UntypedHandle {
490    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
491        visit(self.id());
492    }
493}
494
495impl VisitAssetDependencies for Option<UntypedHandle> {
496    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
497        if let Some(handle) = self {
498            visit(handle.id());
499        }
500    }
501}
502
503impl<A: Asset, const N: usize> VisitAssetDependencies for [Handle<A>; N] {
504    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
505        for dependency in self {
506            visit(dependency.id().untyped());
507        }
508    }
509}
510
511impl<const N: usize> VisitAssetDependencies for [UntypedHandle; N] {
512    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
513        for dependency in self {
514            visit(dependency.id());
515        }
516    }
517}
518
519impl<A: Asset> VisitAssetDependencies for Vec<Handle<A>> {
520    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
521        for dependency in self {
522            visit(dependency.id().untyped());
523        }
524    }
525}
526
527impl VisitAssetDependencies for Vec<UntypedHandle> {
528    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
529        for dependency in self {
530            visit(dependency.id());
531        }
532    }
533}
534
535impl<A: Asset> VisitAssetDependencies for HashSet<Handle<A>> {
536    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
537        for dependency in self {
538            visit(dependency.id().untyped());
539        }
540    }
541}
542
543impl VisitAssetDependencies for HashSet<UntypedHandle> {
544    fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
545        for dependency in self {
546            visit(dependency.id());
547        }
548    }
549}
550
551/// Adds asset-related builder methods to [`App`].
552pub trait AssetApp {
553    /// Registers the given `loader` in the [`App`]'s [`AssetServer`].
554    fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self;
555    /// Registers the given `processor` in the [`App`]'s [`AssetProcessor`].
556    fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self;
557    /// Registers the given [`AssetSourceBuilder`] with the given `id`.
558    ///
559    /// Note that asset sources must be registered before adding [`AssetPlugin`] to your application,
560    /// since registered asset sources are built at that point and not after.
561    fn register_asset_source(
562        &mut self,
563        id: impl Into<AssetSourceId<'static>>,
564        source: AssetSourceBuilder,
565    ) -> &mut Self;
566    /// Sets the default asset processor for the given `extension`.
567    fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self;
568    /// Initializes the given loader in the [`App`]'s [`AssetServer`].
569    fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self;
570    /// Initializes the given [`Asset`] in the [`App`] by:
571    /// * Registering the [`Asset`] in the [`AssetServer`]
572    /// * Initializing the [`AssetEvent`] resource for the [`Asset`]
573    /// * Adding other relevant systems and resources for the [`Asset`]
574    /// * Ignoring schedule ambiguities in [`Assets`] resource. Any time a system takes
575    ///   mutable access to this resource this causes a conflict, but they rarely actually
576    ///   modify the same underlying asset.
577    fn init_asset<A: Asset>(&mut self) -> &mut Self;
578    /// Registers the asset type `T` using `[App::register]`,
579    /// and adds [`ReflectAsset`] type data to `T` and [`ReflectHandle`] type data to [`Handle<T>`] in the type registry.
580    ///
581    /// This enables reflection code to access assets. For detailed information, see the docs on [`ReflectAsset`] and [`ReflectHandle`].
582    fn register_asset_reflect<A>(&mut self) -> &mut Self
583    where
584        A: Asset + Reflect + FromReflect + GetTypeRegistration;
585    /// Preregisters a loader for the given extensions, that will block asset loads until a real loader
586    /// is registered.
587    fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self;
588}
589
590impl AssetApp for App {
591    fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
592        self.world()
593            .resource::<AssetServer>()
594            .register_loader(loader);
595        self
596    }
597
598    fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self {
599        if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
600            asset_processor.register_processor(processor);
601        }
602        self
603    }
604
605    fn register_asset_source(
606        &mut self,
607        id: impl Into<AssetSourceId<'static>>,
608        source: AssetSourceBuilder,
609    ) -> &mut Self {
610        let id = id.into();
611        if self.world().get_resource::<AssetServer>().is_some() {
612            error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id);
613        }
614
615        {
616            let mut sources = self
617                .world_mut()
618                .get_resource_or_init::<AssetSourceBuilders>();
619            sources.insert(id, source);
620        }
621
622        self
623    }
624
625    fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self {
626        if let Some(asset_processor) = self.world().get_resource::<AssetProcessor>() {
627            asset_processor.set_default_processor::<P>(extension);
628        }
629        self
630    }
631
632    fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self {
633        let loader = L::from_world(self.world_mut());
634        self.register_asset_loader(loader)
635    }
636
637    fn init_asset<A: Asset>(&mut self) -> &mut Self {
638        let assets = Assets::<A>::default();
639        self.world()
640            .resource::<AssetServer>()
641            .register_asset(&assets);
642        if self.world().contains_resource::<AssetProcessor>() {
643            let processor = self.world().resource::<AssetProcessor>();
644            // The processor should have its own handle provider separate from the Asset storage
645            // to ensure the id spaces are entirely separate. Not _strictly_ necessary, but
646            // desirable.
647            processor
648                .server()
649                .register_handle_provider(AssetHandleProvider::new(
650                    TypeId::of::<A>(),
651                    Arc::new(AssetIndexAllocator::default()),
652                ));
653        }
654        self.insert_resource(assets)
655            .allow_ambiguous_resource::<Assets<A>>()
656            .add_message::<AssetEvent<A>>()
657            .add_message::<AssetLoadFailedEvent<A>>()
658            .register_type::<Handle<A>>()
659            .add_systems(
660                PostUpdate,
661                Assets::<A>::asset_events
662                    .run_if(Assets::<A>::asset_events_condition)
663                    .in_set(AssetEventSystems),
664            )
665            .add_systems(
666                PreUpdate,
667                Assets::<A>::track_assets.in_set(AssetTrackingSystems),
668            )
669    }
670
671    fn register_asset_reflect<A>(&mut self) -> &mut Self
672    where
673        A: Asset + Reflect + FromReflect + GetTypeRegistration,
674    {
675        let type_registry = self.world().resource::<AppTypeRegistry>();
676        {
677            let mut type_registry = type_registry.write();
678
679            type_registry.register::<A>();
680            type_registry.register::<Handle<A>>();
681            type_registry.register_type_data::<A, ReflectAsset>();
682            type_registry.register_type_data::<Handle<A>, ReflectHandle>();
683        }
684
685        self
686    }
687
688    fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self {
689        self.world_mut()
690            .resource_mut::<AssetServer>()
691            .preregister_loader::<L>(extensions);
692        self
693    }
694}
695
696/// A system set that holds all "track asset" operations.
697#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
698pub struct AssetTrackingSystems;
699
700/// A system set where events accumulated in [`Assets`] are applied to the [`AssetEvent`] [`Messages`] resource.
701///
702/// [`Messages`]: bevy_ecs::message::Messages
703#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
704pub struct AssetEventSystems;
705
706#[cfg(test)]
707mod tests {
708    use crate::{
709        folder::LoadedFolder,
710        handle::Handle,
711        io::{
712            gated::{GateOpener, GatedReader},
713            memory::{Dir, MemoryAssetReader, MemoryAssetWriter},
714            AssetReader, AssetReaderError, AssetSourceBuilder, AssetSourceEvent, AssetSourceId,
715            AssetWatcher, Reader,
716        },
717        loader::{AssetLoader, LoadContext},
718        Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
719        AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, LoadedAsset,
720        UnapprovedPathMode, UntypedHandle, WriteDefaultMetaError,
721    };
722    use alloc::{
723        boxed::Box,
724        format,
725        string::{String, ToString},
726        sync::Arc,
727        vec,
728        vec::Vec,
729    };
730    use async_channel::{Receiver, Sender};
731    use bevy_app::{App, TaskPoolPlugin, Update};
732    use bevy_diagnostic::{DiagnosticsPlugin, DiagnosticsStore};
733    use bevy_ecs::{
734        message::MessageCursor,
735        prelude::*,
736        schedule::{LogLevel, ScheduleBuildSettings},
737    };
738    use bevy_platform::{
739        collections::{HashMap, HashSet},
740        sync::Mutex,
741    };
742    use bevy_reflect::TypePath;
743    use core::time::Duration;
744    use futures_lite::AsyncReadExt;
745    use serde::{Deserialize, Serialize};
746    use std::path::{Path, PathBuf};
747    use thiserror::Error;
748
749    #[derive(Asset, TypePath, Debug, Default)]
750    pub struct CoolText {
751        pub text: String,
752        pub embedded: String,
753        #[dependency]
754        pub dependencies: Vec<Handle<CoolText>>,
755        #[dependency]
756        pub sub_texts: Vec<Handle<SubText>>,
757    }
758
759    #[derive(Asset, TypePath, Debug)]
760    pub struct SubText {
761        pub text: String,
762    }
763
764    #[derive(Serialize, Deserialize, Default)]
765    pub struct CoolTextRon {
766        pub text: String,
767        pub dependencies: Vec<String>,
768        pub embedded_dependencies: Vec<String>,
769        pub sub_texts: Vec<String>,
770    }
771
772    #[derive(Default, TypePath)]
773    pub struct CoolTextLoader;
774
775    #[derive(Error, Debug)]
776    pub enum CoolTextLoaderError {
777        #[error("Could not load dependency: {dependency}")]
778        CannotLoadDependency { dependency: AssetPath<'static> },
779        #[error("A RON error occurred during loading")]
780        RonSpannedError(#[from] ron::error::SpannedError),
781        #[error("An IO error occurred during loading")]
782        Io(#[from] std::io::Error),
783    }
784
785    impl AssetLoader for CoolTextLoader {
786        type Asset = CoolText;
787
788        type Settings = ();
789
790        type Error = CoolTextLoaderError;
791
792        async fn load(
793            &self,
794            reader: &mut dyn Reader,
795            _settings: &Self::Settings,
796            load_context: &mut LoadContext<'_>,
797        ) -> Result<Self::Asset, Self::Error> {
798            let mut bytes = Vec::new();
799            reader.read_to_end(&mut bytes).await?;
800            let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
801            let mut embedded = String::new();
802            for dep in ron.embedded_dependencies {
803                let loaded = load_context
804                    .loader()
805                    .immediate()
806                    .load::<CoolText>(&dep)
807                    .await
808                    .map_err(|_| Self::Error::CannotLoadDependency {
809                        dependency: dep.into(),
810                    })?;
811                let cool = loaded.get();
812                embedded.push_str(&cool.text);
813            }
814            Ok(CoolText {
815                text: ron.text,
816                embedded,
817                dependencies: ron
818                    .dependencies
819                    .iter()
820                    .map(|p| load_context.load(p))
821                    .collect(),
822                sub_texts: ron
823                    .sub_texts
824                    .drain(..)
825                    .map(|text| load_context.add_labeled_asset(text.clone(), SubText { text }))
826                    .collect(),
827            })
828        }
829
830        fn extensions(&self) -> &[&str] {
831            &["cool.ron"]
832        }
833    }
834
835    /// A dummy [`CoolText`] asset reader that only succeeds after `failure_count` times it's read from for each asset.
836    #[derive(Default, Clone)]
837    pub struct UnstableMemoryAssetReader {
838        pub attempt_counters: Arc<Mutex<HashMap<Box<Path>, usize>>>,
839        pub load_delay: Duration,
840        memory_reader: MemoryAssetReader,
841        failure_count: usize,
842    }
843
844    impl UnstableMemoryAssetReader {
845        pub fn new(root: Dir, failure_count: usize) -> Self {
846            Self {
847                load_delay: Duration::from_millis(10),
848                memory_reader: MemoryAssetReader { root },
849                attempt_counters: Default::default(),
850                failure_count,
851            }
852        }
853    }
854
855    impl AssetReader for UnstableMemoryAssetReader {
856        async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
857            self.memory_reader.is_directory(path).await
858        }
859        async fn read_directory<'a>(
860            &'a self,
861            path: &'a Path,
862        ) -> Result<Box<bevy_asset::io::PathStream>, AssetReaderError> {
863            self.memory_reader.read_directory(path).await
864        }
865        async fn read_meta<'a>(
866            &'a self,
867            path: &'a Path,
868        ) -> Result<impl Reader + 'a, AssetReaderError> {
869            self.memory_reader.read_meta(path).await
870        }
871        async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
872            let attempt_number = {
873                let mut attempt_counters = self.attempt_counters.lock().unwrap();
874                if let Some(existing) = attempt_counters.get_mut(path) {
875                    *existing += 1;
876                    *existing
877                } else {
878                    attempt_counters.insert(path.into(), 1);
879                    1
880                }
881            };
882
883            if attempt_number <= self.failure_count {
884                let io_error = std::io::Error::new(
885                    std::io::ErrorKind::ConnectionRefused,
886                    format!(
887                        "Simulated failure {attempt_number} of {}",
888                        self.failure_count
889                    ),
890                );
891                let wait = self.load_delay;
892                return async move {
893                    std::thread::sleep(wait);
894                    Err(AssetReaderError::Io(io_error.into()))
895                }
896                .await;
897            }
898
899            self.memory_reader.read(path).await
900        }
901    }
902
903    /// Creates a basic asset app and an in-memory file system.
904    fn create_app() -> (App, Dir) {
905        let mut app = App::new();
906        let dir = Dir::default();
907        let dir_clone = dir.clone();
908        let dir_clone2 = dir.clone();
909        app.register_asset_source(
910            AssetSourceId::Default,
911            AssetSourceBuilder::new(move || {
912                Box::new(MemoryAssetReader {
913                    root: dir_clone.clone(),
914                })
915            })
916            .with_writer(move |_| {
917                Some(Box::new(MemoryAssetWriter {
918                    root: dir_clone2.clone(),
919                }))
920            }),
921        )
922        .add_plugins((
923            TaskPoolPlugin::default(),
924            AssetPlugin {
925                watch_for_changes_override: Some(false),
926                use_asset_processor_override: Some(false),
927                ..Default::default()
928            },
929            DiagnosticsPlugin,
930        ));
931        (app, dir)
932    }
933
934    fn create_app_with_gate(dir: Dir) -> (App, GateOpener) {
935        let mut app = App::new();
936        let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir });
937        app.register_asset_source(
938            AssetSourceId::Default,
939            AssetSourceBuilder::new(move || Box::new(gated_memory_reader.clone())),
940        )
941        .add_plugins((
942            TaskPoolPlugin::default(),
943            AssetPlugin::default(),
944            DiagnosticsPlugin,
945        ));
946        (app, gate_opener)
947    }
948
949    pub fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) {
950        for _ in 0..LARGE_ITERATION_COUNT {
951            app.update();
952            if predicate(app.world_mut()).is_some() {
953                return;
954            }
955        }
956
957        panic!("Ran out of loops to return `Some` from `predicate`");
958    }
959
960    const LARGE_ITERATION_COUNT: usize = 10000;
961
962    fn get<A: Asset>(world: &World, id: AssetId<A>) -> Option<&A> {
963        world.resource::<Assets<A>>().get(id)
964    }
965
966    fn get_started_load_count(world: &World) -> usize {
967        world
968            .resource::<DiagnosticsStore>()
969            .get_measurement(&AssetServer::STARTED_LOAD_COUNT)
970            .map(|measurement| measurement.value as _)
971            .unwrap_or_default()
972    }
973
974    #[derive(Resource, Default)]
975    struct StoredEvents(Vec<AssetEvent<CoolText>>);
976
977    fn store_asset_events(
978        mut reader: MessageReader<AssetEvent<CoolText>>,
979        mut storage: ResMut<StoredEvents>,
980    ) {
981        storage.0.extend(reader.read().cloned());
982    }
983
984    #[test]
985    fn load_dependencies() {
986        let dir = Dir::default();
987
988        let a_path = "a.cool.ron";
989        let a_ron = r#"
990(
991    text: "a",
992    dependencies: [
993        "foo/b.cool.ron",
994        "c.cool.ron",
995    ],
996    embedded_dependencies: [],
997    sub_texts: [],
998)"#;
999        let b_path = "foo/b.cool.ron";
1000        let b_ron = r#"
1001(
1002    text: "b",
1003    dependencies: [],
1004    embedded_dependencies: [],
1005    sub_texts: [],
1006)"#;
1007
1008        let c_path = "c.cool.ron";
1009        let c_ron = r#"
1010(
1011    text: "c",
1012    dependencies: [
1013        "d.cool.ron",
1014    ],
1015    embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"],
1016    sub_texts: ["hello"],
1017)"#;
1018
1019        let d_path = "d.cool.ron";
1020        let d_ron = r#"
1021(
1022    text: "d",
1023    dependencies: [],
1024    embedded_dependencies: [],
1025    sub_texts: [],
1026)"#;
1027
1028        dir.insert_asset_text(Path::new(a_path), a_ron);
1029        dir.insert_asset_text(Path::new(b_path), b_ron);
1030        dir.insert_asset_text(Path::new(c_path), c_ron);
1031        dir.insert_asset_text(Path::new(d_path), d_ron);
1032
1033        #[derive(Resource)]
1034        struct IdResults {
1035            b_id: AssetId<CoolText>,
1036            c_id: AssetId<CoolText>,
1037            d_id: AssetId<CoolText>,
1038        }
1039
1040        let (mut app, gate_opener) = create_app_with_gate(dir);
1041        app.init_asset::<CoolText>()
1042            .init_asset::<SubText>()
1043            .init_resource::<StoredEvents>()
1044            .register_asset_loader(CoolTextLoader)
1045            .add_systems(Update, store_asset_events);
1046        let asset_server = app.world().resource::<AssetServer>().clone();
1047        let handle: Handle<CoolText> = asset_server.load(a_path);
1048        let a_id = handle.id();
1049        app.update();
1050        assert_eq!(get_started_load_count(app.world()), 1);
1051
1052        {
1053            let a_text = get::<CoolText>(app.world(), a_id);
1054            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1055            assert!(a_text.is_none(), "a's asset should not exist yet");
1056            assert!(a_load.is_loading());
1057            assert!(a_deps.is_loading());
1058            assert!(a_rec_deps.is_loading());
1059        }
1060
1061        // Allow "a" to load ... wait for it to finish loading and validate results
1062        // Dependencies are still gated so they should not be loaded yet
1063        gate_opener.open(a_path);
1064        run_app_until(&mut app, |world| {
1065            let a_text = get::<CoolText>(world, a_id)?;
1066            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1067            assert_eq!(a_text.text, "a");
1068            assert_eq!(a_text.dependencies.len(), 2);
1069            assert!(a_load.is_loaded());
1070            assert!(a_deps.is_loading());
1071            assert!(a_rec_deps.is_loading());
1072
1073            let b_id = a_text.dependencies[0].id();
1074            let b_text = get::<CoolText>(world, b_id);
1075            let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1076            assert!(b_text.is_none(), "b component should not exist yet");
1077            assert!(b_load.is_loading());
1078            assert!(b_deps.is_loading());
1079            assert!(b_rec_deps.is_loading());
1080
1081            let c_id = a_text.dependencies[1].id();
1082            let c_text = get::<CoolText>(world, c_id);
1083            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1084            assert!(c_text.is_none(), "c component should not exist yet");
1085            assert!(c_load.is_loading());
1086            assert!(c_deps.is_loading());
1087            assert!(c_rec_deps.is_loading());
1088            Some(())
1089        });
1090        assert_eq!(get_started_load_count(app.world()), 3);
1091
1092        // Allow "b" to load ... wait for it to finish loading and validate results
1093        // "c" should not be loaded yet
1094        gate_opener.open(b_path);
1095        run_app_until(&mut app, |world| {
1096            let a_text = get::<CoolText>(world, a_id)?;
1097            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1098            assert_eq!(a_text.text, "a");
1099            assert_eq!(a_text.dependencies.len(), 2);
1100            assert!(a_load.is_loaded());
1101            assert!(a_deps.is_loading());
1102            assert!(a_rec_deps.is_loading());
1103
1104            let b_id = a_text.dependencies[0].id();
1105            let b_text = get::<CoolText>(world, b_id)?;
1106            let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1107            assert_eq!(b_text.text, "b");
1108            assert!(b_load.is_loaded());
1109            assert!(b_deps.is_loaded());
1110            assert!(b_rec_deps.is_loaded());
1111
1112            let c_id = a_text.dependencies[1].id();
1113            let c_text = get::<CoolText>(world, c_id);
1114            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1115            assert!(c_text.is_none(), "c component should not exist yet");
1116            assert!(c_load.is_loading());
1117            assert!(c_deps.is_loading());
1118            assert!(c_rec_deps.is_loading());
1119            Some(())
1120        });
1121        assert_eq!(get_started_load_count(app.world()), 3);
1122
1123        // Allow "c" to load ... wait for it to finish loading and validate results
1124        // all "a" dependencies should be loaded now
1125        gate_opener.open(c_path);
1126
1127        // Re-open a and b gates to allow c to load embedded deps (gates are closed after each load)
1128        gate_opener.open(a_path);
1129        gate_opener.open(b_path);
1130        run_app_until(&mut app, |world| {
1131            let a_text = get::<CoolText>(world, a_id)?;
1132            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1133            assert_eq!(a_text.text, "a");
1134            assert_eq!(a_text.embedded, "");
1135            assert_eq!(a_text.dependencies.len(), 2);
1136            assert!(a_load.is_loaded());
1137
1138            let b_id = a_text.dependencies[0].id();
1139            let b_text = get::<CoolText>(world, b_id)?;
1140            let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1141            assert_eq!(b_text.text, "b");
1142            assert_eq!(b_text.embedded, "");
1143            assert!(b_load.is_loaded());
1144            assert!(b_deps.is_loaded());
1145            assert!(b_rec_deps.is_loaded());
1146
1147            let c_id = a_text.dependencies[1].id();
1148            let c_text = get::<CoolText>(world, c_id)?;
1149            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1150            assert_eq!(c_text.text, "c");
1151            assert_eq!(c_text.embedded, "ab");
1152            assert!(c_load.is_loaded());
1153            assert!(
1154                c_deps.is_loading(),
1155                "c deps should not be loaded yet because d has not loaded"
1156            );
1157            assert!(
1158                c_rec_deps.is_loading(),
1159                "c rec deps should not be loaded yet because d has not loaded"
1160            );
1161
1162            let sub_text_id = c_text.sub_texts[0].id();
1163            let sub_text = get::<SubText>(world, sub_text_id)
1164                .expect("subtext should exist if c exists. it came from the same loader");
1165            assert_eq!(sub_text.text, "hello");
1166            let (sub_text_load, sub_text_deps, sub_text_rec_deps) =
1167                asset_server.get_load_states(sub_text_id).unwrap();
1168            assert!(sub_text_load.is_loaded());
1169            assert!(sub_text_deps.is_loaded());
1170            assert!(sub_text_rec_deps.is_loaded());
1171
1172            let d_id = c_text.dependencies[0].id();
1173            let d_text = get::<CoolText>(world, d_id);
1174            let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1175            assert!(d_text.is_none(), "d component should not exist yet");
1176            assert!(d_load.is_loading());
1177            assert!(d_deps.is_loading());
1178            assert!(d_rec_deps.is_loading());
1179
1180            assert!(
1181                a_deps.is_loaded(),
1182                "If c has been loaded, the a deps should all be considered loaded"
1183            );
1184            assert!(
1185                a_rec_deps.is_loading(),
1186                "d is not loaded, so a's recursive deps should still be loading"
1187            );
1188            world.insert_resource(IdResults { b_id, c_id, d_id });
1189            Some(())
1190        });
1191        assert_eq!(get_started_load_count(app.world()), 6);
1192
1193        gate_opener.open(d_path);
1194        run_app_until(&mut app, |world| {
1195            let a_text = get::<CoolText>(world, a_id)?;
1196            let (_a_load, _a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1197            let c_id = a_text.dependencies[1].id();
1198            let c_text = get::<CoolText>(world, c_id)?;
1199            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1200            assert_eq!(c_text.text, "c");
1201            assert_eq!(c_text.embedded, "ab");
1202
1203            let d_id = c_text.dependencies[0].id();
1204            let d_text = get::<CoolText>(world, d_id)?;
1205            let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1206            assert_eq!(d_text.text, "d");
1207            assert_eq!(d_text.embedded, "");
1208
1209            assert!(c_load.is_loaded());
1210            assert!(c_deps.is_loaded());
1211            assert!(c_rec_deps.is_loaded());
1212
1213            assert!(d_load.is_loaded());
1214            assert!(d_deps.is_loaded());
1215            assert!(d_rec_deps.is_loaded());
1216
1217            assert!(
1218                a_rec_deps.is_loaded(),
1219                "d is loaded, so a's recursive deps should be loaded"
1220            );
1221            Some(())
1222        });
1223
1224        assert_eq!(get_started_load_count(app.world()), 6);
1225
1226        {
1227            let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1228            let a = texts.get_mut(a_id).unwrap();
1229            a.text = "Changed".to_string();
1230        }
1231
1232        drop(handle);
1233
1234        app.update();
1235        assert_eq!(
1236            app.world().resource::<Assets<CoolText>>().len(),
1237            0,
1238            "CoolText asset entities should be despawned when no more handles exist"
1239        );
1240        app.update();
1241        // this requires a second update because the parent asset was freed in the previous app.update()
1242        assert_eq!(
1243            app.world().resource::<Assets<SubText>>().len(),
1244            0,
1245            "SubText asset entities should be despawned when no more handles exist"
1246        );
1247        let events = app.world_mut().remove_resource::<StoredEvents>().unwrap();
1248        let id_results = app.world_mut().remove_resource::<IdResults>().unwrap();
1249        let expected_events = vec![
1250            AssetEvent::Added { id: a_id },
1251            AssetEvent::LoadedWithDependencies {
1252                id: id_results.b_id,
1253            },
1254            AssetEvent::Added {
1255                id: id_results.b_id,
1256            },
1257            AssetEvent::Added {
1258                id: id_results.c_id,
1259            },
1260            AssetEvent::LoadedWithDependencies {
1261                id: id_results.d_id,
1262            },
1263            AssetEvent::LoadedWithDependencies {
1264                id: id_results.c_id,
1265            },
1266            AssetEvent::LoadedWithDependencies { id: a_id },
1267            AssetEvent::Added {
1268                id: id_results.d_id,
1269            },
1270            AssetEvent::Modified { id: a_id },
1271            AssetEvent::Unused { id: a_id },
1272            AssetEvent::Removed { id: a_id },
1273            AssetEvent::Unused {
1274                id: id_results.b_id,
1275            },
1276            AssetEvent::Removed {
1277                id: id_results.b_id,
1278            },
1279            AssetEvent::Unused {
1280                id: id_results.c_id,
1281            },
1282            AssetEvent::Removed {
1283                id: id_results.c_id,
1284            },
1285            AssetEvent::Unused {
1286                id: id_results.d_id,
1287            },
1288            AssetEvent::Removed {
1289                id: id_results.d_id,
1290            },
1291        ];
1292        assert_eq!(events.0, expected_events);
1293    }
1294
1295    #[test]
1296    fn failure_load_states() {
1297        let dir = Dir::default();
1298
1299        let a_path = "a.cool.ron";
1300        let a_ron = r#"
1301(
1302    text: "a",
1303    dependencies: [
1304        "b.cool.ron",
1305        "c.cool.ron",
1306    ],
1307    embedded_dependencies: [],
1308    sub_texts: []
1309)"#;
1310        let b_path = "b.cool.ron";
1311        let b_ron = r#"
1312(
1313    text: "b",
1314    dependencies: [],
1315    embedded_dependencies: [],
1316    sub_texts: []
1317)"#;
1318
1319        let c_path = "c.cool.ron";
1320        let c_ron = r#"
1321(
1322    text: "c",
1323    dependencies: [
1324        "d.cool.ron",
1325    ],
1326    embedded_dependencies: [],
1327    sub_texts: []
1328)"#;
1329
1330        let d_path = "d.cool.ron";
1331        let d_ron = r#"
1332(
1333    text: "d",
1334    dependencies: [],
1335    OH NO THIS ASSET IS MALFORMED
1336    embedded_dependencies: [],
1337    sub_texts: []
1338)"#;
1339
1340        dir.insert_asset_text(Path::new(a_path), a_ron);
1341        dir.insert_asset_text(Path::new(b_path), b_ron);
1342        dir.insert_asset_text(Path::new(c_path), c_ron);
1343        dir.insert_asset_text(Path::new(d_path), d_ron);
1344
1345        let (mut app, gate_opener) = create_app_with_gate(dir);
1346        app.init_asset::<CoolText>()
1347            .register_asset_loader(CoolTextLoader);
1348        let asset_server = app.world().resource::<AssetServer>().clone();
1349        let handle: Handle<CoolText> = asset_server.load(a_path);
1350        let a_id = handle.id();
1351
1352        app.update();
1353        assert_eq!(get_started_load_count(app.world()), 1);
1354        {
1355            let other_handle: Handle<CoolText> = asset_server.load(a_path);
1356            assert_eq!(
1357                other_handle, handle,
1358                "handles from consecutive load calls should be equal"
1359            );
1360            assert_eq!(
1361                other_handle.id(),
1362                handle.id(),
1363                "handle ids from consecutive load calls should be equal"
1364            );
1365
1366            app.update();
1367            // Only one load still!
1368            assert_eq!(get_started_load_count(app.world()), 1);
1369        }
1370
1371        gate_opener.open(a_path);
1372        gate_opener.open(b_path);
1373        gate_opener.open(c_path);
1374        gate_opener.open(d_path);
1375
1376        run_app_until(&mut app, |world| {
1377            let a_text = get::<CoolText>(world, a_id)?;
1378            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1379
1380            let b_id = a_text.dependencies[0].id();
1381            let b_text = get::<CoolText>(world, b_id)?;
1382            let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1383
1384            let c_id = a_text.dependencies[1].id();
1385            let c_text = get::<CoolText>(world, c_id)?;
1386            let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
1387
1388            let d_id = c_text.dependencies[0].id();
1389            let d_text = get::<CoolText>(world, d_id);
1390            let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
1391
1392            if !d_load.is_failed() {
1393                // wait until d has exited the loading state
1394                return None;
1395            }
1396
1397            assert!(d_text.is_none());
1398            assert!(d_load.is_failed());
1399            assert!(d_deps.is_failed());
1400            assert!(d_rec_deps.is_failed());
1401
1402            assert_eq!(a_text.text, "a");
1403            assert!(a_load.is_loaded());
1404            assert!(a_deps.is_loaded());
1405            assert!(a_rec_deps.is_failed());
1406
1407            assert_eq!(b_text.text, "b");
1408            assert!(b_load.is_loaded());
1409            assert!(b_deps.is_loaded());
1410            assert!(b_rec_deps.is_loaded());
1411
1412            assert_eq!(c_text.text, "c");
1413            assert!(c_load.is_loaded());
1414            assert!(c_deps.is_failed());
1415            assert!(c_rec_deps.is_failed());
1416
1417            assert!(asset_server.load_state(a_id).is_loaded());
1418            assert!(asset_server.dependency_load_state(a_id).is_loaded());
1419            assert!(asset_server
1420                .recursive_dependency_load_state(a_id)
1421                .is_failed());
1422
1423            assert!(asset_server.is_loaded(a_id));
1424            assert!(asset_server.is_loaded_with_direct_dependencies(a_id));
1425            assert!(!asset_server.is_loaded_with_dependencies(a_id));
1426
1427            Some(())
1428        });
1429
1430        assert_eq!(get_started_load_count(app.world()), 4);
1431    }
1432
1433    #[test]
1434    fn dependency_load_states() {
1435        let a_path = "a.cool.ron";
1436        let a_ron = r#"
1437(
1438    text: "a",
1439    dependencies: [
1440        "b.cool.ron",
1441        "c.cool.ron",
1442    ],
1443    embedded_dependencies: [],
1444    sub_texts: []
1445)"#;
1446        let b_path = "b.cool.ron";
1447        let b_ron = r#"
1448(
1449    text: "b",
1450    dependencies: [],
1451    MALFORMED
1452    embedded_dependencies: [],
1453    sub_texts: []
1454)"#;
1455
1456        let c_path = "c.cool.ron";
1457        let c_ron = r#"
1458(
1459    text: "c",
1460    dependencies: [],
1461    embedded_dependencies: [],
1462    sub_texts: []
1463)"#;
1464
1465        let dir = Dir::default();
1466        dir.insert_asset_text(Path::new(a_path), a_ron);
1467        dir.insert_asset_text(Path::new(b_path), b_ron);
1468        dir.insert_asset_text(Path::new(c_path), c_ron);
1469
1470        let (mut app, gate_opener) = create_app_with_gate(dir);
1471        app.init_asset::<CoolText>()
1472            .register_asset_loader(CoolTextLoader);
1473        let asset_server = app.world().resource::<AssetServer>().clone();
1474        let handle: Handle<CoolText> = asset_server.load(a_path);
1475        let a_id = handle.id();
1476
1477        app.update();
1478        assert_eq!(get_started_load_count(app.world()), 1);
1479
1480        gate_opener.open(a_path);
1481        run_app_until(&mut app, |world| {
1482            let _a_text = get::<CoolText>(world, a_id)?;
1483            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1484            assert!(a_load.is_loaded());
1485            assert!(a_deps.is_loading());
1486            assert!(a_rec_deps.is_loading());
1487            Some(())
1488        });
1489
1490        assert_eq!(get_started_load_count(app.world()), 3);
1491
1492        gate_opener.open(b_path);
1493        run_app_until(&mut app, |world| {
1494            let a_text = get::<CoolText>(world, a_id)?;
1495            let b_id = a_text.dependencies[0].id();
1496
1497            let (b_load, _b_deps, _b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
1498            if !b_load.is_failed() {
1499                // wait until b fails
1500                return None;
1501            }
1502
1503            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1504            assert!(a_load.is_loaded());
1505            assert!(a_deps.is_failed());
1506            assert!(a_rec_deps.is_failed());
1507            Some(())
1508        });
1509
1510        assert_eq!(get_started_load_count(app.world()), 3);
1511
1512        gate_opener.open(c_path);
1513        run_app_until(&mut app, |world| {
1514            let a_text = get::<CoolText>(world, a_id)?;
1515            let c_id = a_text.dependencies[1].id();
1516            // wait until c loads
1517            let _c_text = get::<CoolText>(world, c_id)?;
1518
1519            let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
1520            assert!(a_load.is_loaded());
1521            assert!(
1522                a_deps.is_failed(),
1523                "Successful dependency load should not overwrite a previous failure"
1524            );
1525            assert!(
1526                a_rec_deps.is_failed(),
1527                "Successful dependency load should not overwrite a previous failure"
1528            );
1529            Some(())
1530        });
1531
1532        assert_eq!(get_started_load_count(app.world()), 3);
1533    }
1534
1535    const SIMPLE_TEXT: &str = r#"
1536(
1537    text: "dep",
1538    dependencies: [],
1539    embedded_dependencies: [],
1540    sub_texts: [],
1541)"#;
1542    #[test]
1543    fn keep_gotten_strong_handles() {
1544        let dir = Dir::default();
1545        dir.insert_asset_text(Path::new("dep.cool.ron"), SIMPLE_TEXT);
1546
1547        let (mut app, _) = create_app_with_gate(dir);
1548        app.init_asset::<CoolText>()
1549            .init_asset::<SubText>()
1550            .init_resource::<StoredEvents>()
1551            .register_asset_loader(CoolTextLoader)
1552            .add_systems(Update, store_asset_events);
1553
1554        let id = {
1555            let handle = {
1556                let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1557                let handle = texts.add(CoolText::default());
1558                texts.get_strong_handle(handle.id()).unwrap()
1559            };
1560
1561            app.update();
1562
1563            {
1564                let text = app.world().resource::<Assets<CoolText>>().get(&handle);
1565                assert!(text.is_some());
1566            }
1567            handle.id()
1568        };
1569        // handle is dropped
1570        app.update();
1571        assert!(
1572            app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1573            "asset has no handles, so it should have been dropped last update"
1574        );
1575    }
1576
1577    #[test]
1578    fn manual_asset_management() {
1579        let dir = Dir::default();
1580        let dep_path = "dep.cool.ron";
1581
1582        dir.insert_asset_text(Path::new(dep_path), SIMPLE_TEXT);
1583
1584        let (mut app, gate_opener) = create_app_with_gate(dir);
1585        app.init_asset::<CoolText>()
1586            .init_asset::<SubText>()
1587            .init_resource::<StoredEvents>()
1588            .register_asset_loader(CoolTextLoader)
1589            .add_systems(Update, store_asset_events);
1590
1591        let hello = "hello".to_string();
1592        let empty = "".to_string();
1593
1594        let id = {
1595            let handle = {
1596                let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
1597                texts.add(CoolText {
1598                    text: hello.clone(),
1599                    embedded: empty.clone(),
1600                    dependencies: vec![],
1601                    sub_texts: Vec::new(),
1602                })
1603            };
1604
1605            app.update();
1606
1607            {
1608                let text = app
1609                    .world()
1610                    .resource::<Assets<CoolText>>()
1611                    .get(&handle)
1612                    .unwrap();
1613                assert_eq!(text.text, hello);
1614            }
1615            handle.id()
1616        };
1617        // handle is dropped
1618        app.update();
1619        assert!(
1620            app.world().resource::<Assets<CoolText>>().get(id).is_none(),
1621            "asset has no handles, so it should have been dropped last update"
1622        );
1623        // remove event is emitted
1624        app.update();
1625        let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1626        let expected_events = vec![
1627            AssetEvent::Added { id },
1628            AssetEvent::Unused { id },
1629            AssetEvent::Removed { id },
1630        ];
1631
1632        // No loads have occurred yet.
1633        assert_eq!(get_started_load_count(app.world()), 0);
1634
1635        assert_eq!(events, expected_events);
1636
1637        let dep_handle = app.world().resource::<AssetServer>().load(dep_path);
1638
1639        app.update();
1640        assert_eq!(get_started_load_count(app.world()), 1);
1641
1642        let a = CoolText {
1643            text: "a".to_string(),
1644            embedded: empty,
1645            // this dependency is behind a manual load gate, which should prevent 'a' from emitting a LoadedWithDependencies event
1646            dependencies: vec![dep_handle.clone()],
1647            sub_texts: Vec::new(),
1648        };
1649        let a_handle = app.world().resource::<AssetServer>().load_asset(a);
1650
1651        // load_asset does not count as a load.
1652        assert_eq!(get_started_load_count(app.world()), 1);
1653
1654        app.update();
1655        // TODO: ideally it doesn't take two updates for the added event to emit
1656        app.update();
1657
1658        let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1659        let expected_events = vec![AssetEvent::Added { id: a_handle.id() }];
1660        assert_eq!(events, expected_events);
1661
1662        gate_opener.open(dep_path);
1663        loop {
1664            app.update();
1665            let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1666            if events.is_empty() {
1667                continue;
1668            }
1669            let expected_events = vec![
1670                AssetEvent::LoadedWithDependencies {
1671                    id: dep_handle.id(),
1672                },
1673                AssetEvent::LoadedWithDependencies { id: a_handle.id() },
1674            ];
1675            assert_eq!(events, expected_events);
1676            break;
1677        }
1678
1679        assert_eq!(get_started_load_count(app.world()), 1);
1680
1681        app.update();
1682        let events = core::mem::take(&mut app.world_mut().resource_mut::<StoredEvents>().0);
1683        let expected_events = vec![AssetEvent::Added {
1684            id: dep_handle.id(),
1685        }];
1686        assert_eq!(events, expected_events);
1687    }
1688
1689    #[test]
1690    fn load_folder() {
1691        let dir = Dir::default();
1692
1693        let a_path = "text/a.cool.ron";
1694        let a_ron = r#"
1695(
1696    text: "a",
1697    dependencies: [
1698        "b.cool.ron",
1699    ],
1700    embedded_dependencies: [],
1701    sub_texts: [],
1702)"#;
1703        let b_path = "b.cool.ron";
1704        let b_ron = r#"
1705(
1706    text: "b",
1707    dependencies: [],
1708    embedded_dependencies: [],
1709    sub_texts: [],
1710)"#;
1711
1712        let c_path = "text/c.cool.ron";
1713        let c_ron = r#"
1714(
1715    text: "c",
1716    dependencies: [
1717    ],
1718    embedded_dependencies: [],
1719    sub_texts: [],
1720)"#;
1721        dir.insert_asset_text(Path::new(a_path), a_ron);
1722        dir.insert_asset_text(Path::new(b_path), b_ron);
1723        dir.insert_asset_text(Path::new(c_path), c_ron);
1724
1725        let (mut app, gate_opener) = create_app_with_gate(dir);
1726        app.init_asset::<CoolText>()
1727            .init_asset::<SubText>()
1728            .register_asset_loader(CoolTextLoader);
1729        let asset_server = app.world().resource::<AssetServer>().clone();
1730        let handle: Handle<LoadedFolder> = asset_server.load_folder("text");
1731
1732        // The folder started loading. The task will also try to start loading the first asset in
1733        // the folder. With the multi_threaded feature this check is racing with the first load, so
1734        // allow 1 or 2 load tasks to start.
1735        app.update();
1736        let started_load_tasks = get_started_load_count(app.world());
1737        assert!((1..=2).contains(&started_load_tasks));
1738
1739        gate_opener.open(a_path);
1740        gate_opener.open(b_path);
1741        gate_opener.open(c_path);
1742
1743        let mut cursor = MessageCursor::default();
1744        run_app_until(&mut app, |world| {
1745            let events = world.resource::<Messages<AssetEvent<LoadedFolder>>>();
1746            let asset_server = world.resource::<AssetServer>();
1747            let loaded_folders = world.resource::<Assets<LoadedFolder>>();
1748            let cool_texts = world.resource::<Assets<CoolText>>();
1749            for event in cursor.read(events) {
1750                if let AssetEvent::LoadedWithDependencies { id } = event
1751                    && *id == handle.id()
1752                {
1753                    let loaded_folder = loaded_folders.get(&handle).unwrap();
1754                    let a_handle: Handle<CoolText> =
1755                        asset_server.get_handle("text/a.cool.ron").unwrap();
1756                    let c_handle: Handle<CoolText> =
1757                        asset_server.get_handle("text/c.cool.ron").unwrap();
1758
1759                    let mut found_a = false;
1760                    let mut found_c = false;
1761                    for asset_handle in &loaded_folder.handles {
1762                        if asset_handle.id() == a_handle.id().untyped() {
1763                            found_a = true;
1764                        } else if asset_handle.id() == c_handle.id().untyped() {
1765                            found_c = true;
1766                        }
1767                    }
1768                    assert!(found_a);
1769                    assert!(found_c);
1770                    assert_eq!(loaded_folder.handles.len(), 2);
1771
1772                    let a_text = cool_texts.get(&a_handle).unwrap();
1773                    let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();
1774                    let c_text = cool_texts.get(&c_handle).unwrap();
1775
1776                    assert_eq!("a", a_text.text);
1777                    assert_eq!("b", b_text.text);
1778                    assert_eq!("c", c_text.text);
1779
1780                    return Some(());
1781                }
1782            }
1783            None
1784        });
1785        assert_eq!(get_started_load_count(app.world()), 4);
1786    }
1787
1788    /// Tests that `AssetLoadFailedEvent<A>` events are emitted and can be used to retry failed assets.
1789    #[test]
1790    fn load_error_events() {
1791        #[derive(Resource, Default)]
1792        struct ErrorTracker {
1793            tick: u64,
1794            failures: usize,
1795            queued_retries: Vec<(AssetPath<'static>, AssetId<CoolText>, u64)>,
1796            finished_asset: Option<AssetId<CoolText>>,
1797        }
1798
1799        fn asset_event_handler(
1800            mut events: MessageReader<AssetEvent<CoolText>>,
1801            mut tracker: ResMut<ErrorTracker>,
1802        ) {
1803            for event in events.read() {
1804                if let AssetEvent::LoadedWithDependencies { id } = event {
1805                    tracker.finished_asset = Some(*id);
1806                }
1807            }
1808        }
1809
1810        fn asset_load_error_event_handler(
1811            server: Res<AssetServer>,
1812            mut errors: MessageReader<AssetLoadFailedEvent<CoolText>>,
1813            mut tracker: ResMut<ErrorTracker>,
1814        ) {
1815            // In the real world, this would refer to time (not ticks)
1816            tracker.tick += 1;
1817
1818            // Retry loading past failed items
1819            let now = tracker.tick;
1820            tracker
1821                .queued_retries
1822                .retain(|(path, old_id, retry_after)| {
1823                    if now > *retry_after {
1824                        let new_handle = server.load::<CoolText>(path);
1825                        assert_eq!(&new_handle.id(), old_id);
1826                        false
1827                    } else {
1828                        true
1829                    }
1830                });
1831
1832            // Check what just failed
1833            for error in errors.read() {
1834                let (load_state, _, _) = server.get_load_states(error.id).unwrap();
1835                assert!(load_state.is_failed());
1836                assert_eq!(*error.path.source(), AssetSourceId::Name("unstable".into()));
1837                match &error.error {
1838                    AssetLoadError::AssetReaderError(read_error) => match read_error {
1839                        AssetReaderError::Io(_) => {
1840                            tracker.failures += 1;
1841                            if tracker.failures <= 2 {
1842                                // Retry in 10 ticks
1843                                tracker.queued_retries.push((
1844                                    error.path.clone(),
1845                                    error.id,
1846                                    now + 10,
1847                                ));
1848                            } else {
1849                                panic!(
1850                                    "Unexpected failure #{} (expected only 2)",
1851                                    tracker.failures
1852                                );
1853                            }
1854                        }
1855                        _ => panic!("Unexpected error type {}", read_error),
1856                    },
1857                    _ => panic!("Unexpected error type {}", error.error),
1858                }
1859            }
1860        }
1861
1862        let a_path = "text/a.cool.ron";
1863        let a_ron = r#"
1864(
1865    text: "a",
1866    dependencies: [],
1867    embedded_dependencies: [],
1868    sub_texts: [],
1869)"#;
1870
1871        let dir = Dir::default();
1872        dir.insert_asset_text(Path::new(a_path), a_ron);
1873        let unstable_reader = UnstableMemoryAssetReader::new(dir, 2);
1874
1875        let mut app = App::new();
1876        app.register_asset_source(
1877            "unstable",
1878            AssetSourceBuilder::new(move || Box::new(unstable_reader.clone())),
1879        )
1880        .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
1881        .init_asset::<CoolText>()
1882        .register_asset_loader(CoolTextLoader)
1883        .init_resource::<ErrorTracker>()
1884        .add_systems(
1885            Update,
1886            (asset_event_handler, asset_load_error_event_handler).chain(),
1887        );
1888
1889        let asset_server = app.world().resource::<AssetServer>().clone();
1890        let a_path = format!("unstable://{a_path}");
1891        let a_handle: Handle<CoolText> = asset_server.load(a_path);
1892        let a_id = a_handle.id();
1893
1894        run_app_until(&mut app, |world| {
1895            let tracker = world.resource::<ErrorTracker>();
1896            match tracker.finished_asset {
1897                Some(asset_id) => {
1898                    assert_eq!(asset_id, a_id);
1899                    let assets = world.resource::<Assets<CoolText>>();
1900                    let result = assets.get(asset_id).unwrap();
1901                    assert_eq!(result.text, "a");
1902                    Some(())
1903                }
1904                None => None,
1905            }
1906        });
1907    }
1908
1909    #[test]
1910    fn ignore_system_ambiguities_on_assets() {
1911        let mut app = create_app().0;
1912        app.init_asset::<CoolText>();
1913
1914        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
1915        app.add_systems(Update, (uses_assets, uses_assets));
1916        app.edit_schedule(Update, |s| {
1917            s.set_build_settings(ScheduleBuildSettings {
1918                ambiguity_detection: LogLevel::Error,
1919                ..Default::default()
1920            });
1921        });
1922
1923        // running schedule does not error on ambiguity between the 2 uses_assets systems
1924        app.world_mut().run_schedule(Update);
1925    }
1926
1927    // This test is not checking a requirement, but documenting a current limitation. We simply are
1928    // not capable of loading subassets when doing nested immediate loads.
1929    #[test]
1930    fn error_on_nested_immediate_load_of_subasset() {
1931        let (mut app, dir) = create_app();
1932        dir.insert_asset_text(
1933            Path::new("a.cool.ron"),
1934            r#"(
1935    text: "b",
1936    dependencies: [],
1937    embedded_dependencies: [],
1938    sub_texts: ["A"],
1939)"#,
1940        );
1941        dir.insert_asset_text(Path::new("empty.txt"), "");
1942
1943        app.init_asset::<CoolText>()
1944            .init_asset::<SubText>()
1945            .register_asset_loader(CoolTextLoader);
1946
1947        #[derive(TypePath)]
1948        struct NestedLoadOfSubassetLoader;
1949
1950        impl AssetLoader for NestedLoadOfSubassetLoader {
1951            type Asset = TestAsset;
1952            type Error = crate::loader::LoadDirectError;
1953            type Settings = ();
1954
1955            async fn load(
1956                &self,
1957                _: &mut dyn Reader,
1958                _: &Self::Settings,
1959                load_context: &mut LoadContext<'_>,
1960            ) -> Result<Self::Asset, Self::Error> {
1961                // We expect this load to fail.
1962                load_context
1963                    .loader()
1964                    .immediate()
1965                    .load::<SubText>("a.cool.ron#A")
1966                    .await?;
1967                Ok(TestAsset)
1968            }
1969
1970            fn extensions(&self) -> &[&str] {
1971                &["txt"]
1972            }
1973        }
1974
1975        app.init_asset::<TestAsset>()
1976            .register_asset_loader(NestedLoadOfSubassetLoader);
1977
1978        let asset_server = app.world().resource::<AssetServer>().clone();
1979        let handle = asset_server.load::<TestAsset>("empty.txt");
1980
1981        run_app_until(&mut app, |_world| match asset_server.load_state(&handle) {
1982            LoadState::Loading => None,
1983            LoadState::Failed(err) => {
1984                let error_message = format!("{err}");
1985                assert!(error_message.contains("Requested to load an asset path (a.cool.ron#A) with a subasset, but this is unsupported"), "what? \"{error_message}\"");
1986                Some(())
1987            }
1988            state => panic!("Unexpected asset state: {state:?}"),
1989        });
1990    }
1991
1992    // validate the Asset derive macro for various asset types
1993    #[derive(Asset, TypePath)]
1994    pub struct TestAsset;
1995
1996    #[derive(Asset, TypePath)]
1997    #[expect(
1998        dead_code,
1999        reason = "This exists to ensure that `#[derive(Asset)]` works on enums. The inner variants are known not to be used."
2000    )]
2001    pub enum EnumTestAsset {
2002        Unnamed(#[dependency] Handle<TestAsset>),
2003        Named {
2004            #[dependency]
2005            handle: Handle<TestAsset>,
2006            #[dependency]
2007            vec_handles: Vec<Handle<TestAsset>>,
2008            #[dependency]
2009            embedded: TestAsset,
2010            #[dependency]
2011            set_handles: HashSet<Handle<TestAsset>>,
2012            #[dependency]
2013            untyped_set_handles: HashSet<UntypedHandle>,
2014        },
2015        StructStyle(#[dependency] TestAsset),
2016        Empty,
2017    }
2018
2019    #[expect(
2020        dead_code,
2021        reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed."
2022    )]
2023    #[derive(Asset, TypePath)]
2024    pub struct StructTestAsset {
2025        #[dependency]
2026        handle: Handle<TestAsset>,
2027        #[dependency]
2028        embedded: TestAsset,
2029        #[dependency]
2030        array_handles: [Handle<TestAsset>; 5],
2031        #[dependency]
2032        untyped_array_handles: [UntypedHandle; 5],
2033        #[dependency]
2034        set_handles: HashSet<Handle<TestAsset>>,
2035        #[dependency]
2036        untyped_set_handles: HashSet<UntypedHandle>,
2037    }
2038
2039    #[expect(
2040        dead_code,
2041        reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed."
2042    )]
2043    #[derive(Asset, TypePath)]
2044    pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);
2045
2046    fn unapproved_path_setup(mode: UnapprovedPathMode) -> App {
2047        let dir = Dir::default();
2048        let a_path = "../a.cool.ron";
2049        let a_ron = r#"
2050(
2051    text: "a",
2052    dependencies: [],
2053    embedded_dependencies: [],
2054    sub_texts: [],
2055)"#;
2056
2057        dir.insert_asset_text(Path::new(a_path), a_ron);
2058
2059        let mut app = App::new();
2060        let memory_reader = MemoryAssetReader { root: dir };
2061        app.register_asset_source(
2062            AssetSourceId::Default,
2063            AssetSourceBuilder::new(move || Box::new(memory_reader.clone())),
2064        )
2065        .add_plugins((
2066            TaskPoolPlugin::default(),
2067            AssetPlugin {
2068                unapproved_path_mode: mode,
2069                ..Default::default()
2070            },
2071        ));
2072        app.init_asset::<CoolText>();
2073
2074        app
2075    }
2076
2077    fn load_a_asset(assets: Res<AssetServer>) {
2078        let a = assets.load::<CoolText>("../a.cool.ron");
2079        if a == Handle::default() {
2080            panic!()
2081        }
2082    }
2083
2084    fn load_a_asset_override(assets: Res<AssetServer>) {
2085        let a = assets.load_override::<CoolText>("../a.cool.ron");
2086        if a == Handle::default() {
2087            panic!()
2088        }
2089    }
2090
2091    #[test]
2092    #[should_panic]
2093    fn unapproved_path_forbid_should_panic() {
2094        let mut app = unapproved_path_setup(UnapprovedPathMode::Forbid);
2095
2096        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
2097        app.add_systems(Update, (uses_assets, load_a_asset_override));
2098
2099        app.world_mut().run_schedule(Update);
2100    }
2101
2102    #[test]
2103    #[should_panic]
2104    fn unapproved_path_deny_should_panic() {
2105        let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);
2106
2107        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
2108        app.add_systems(Update, (uses_assets, load_a_asset));
2109
2110        app.world_mut().run_schedule(Update);
2111    }
2112
2113    #[test]
2114    fn unapproved_path_deny_should_finish() {
2115        let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);
2116
2117        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
2118        app.add_systems(Update, (uses_assets, load_a_asset_override));
2119
2120        app.world_mut().run_schedule(Update);
2121    }
2122
2123    #[test]
2124    fn unapproved_path_allow_should_finish() {
2125        let mut app = unapproved_path_setup(UnapprovedPathMode::Allow);
2126
2127        fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
2128        app.add_systems(Update, (uses_assets, load_a_asset));
2129
2130        app.world_mut().run_schedule(Update);
2131    }
2132
2133    #[test]
2134    fn insert_dropped_handle_returns_error() {
2135        let mut app = create_app().0;
2136
2137        app.init_asset::<TestAsset>();
2138
2139        let handle = app.world().resource::<Assets<TestAsset>>().reserve_handle();
2140        // We still have the asset ID, but we've dropped the handle so the asset is no longer live.
2141        let asset_id = handle.id();
2142        drop(handle);
2143
2144        // Allow `Assets` to detect the dropped handle.
2145        app.world_mut()
2146            .run_system_cached(Assets::<TestAsset>::track_assets)
2147            .unwrap();
2148
2149        let AssetId::Index { index, .. } = asset_id else {
2150            unreachable!("Reserving a handle always produces an index");
2151        };
2152
2153        // Try to insert an asset into the dropped handle's spot. This should not panic.
2154        assert_eq!(
2155            app.world_mut()
2156                .resource_mut::<Assets<TestAsset>>()
2157                .insert(asset_id, TestAsset),
2158            Err(InvalidGenerationError::Removed { index })
2159        );
2160    }
2161
2162    /// A loader that notifies a sender when the loader has started, and blocks on a receiver to
2163    /// simulate a long asset loader.
2164    // Note: we can't just use the GatedReader, since currently we hold the handle until after
2165    // we've selected the reader. The GatedReader blocks this process, so we need to wait until
2166    // we gate in the loader instead.
2167    #[derive(TypePath)]
2168    struct GatedLoader {
2169        in_loader_sender: Sender<()>,
2170        gate_receiver: Receiver<()>,
2171    }
2172
2173    impl AssetLoader for GatedLoader {
2174        type Asset = TestAsset;
2175        type Error = std::io::Error;
2176        type Settings = ();
2177
2178        async fn load(
2179            &self,
2180            _reader: &mut dyn Reader,
2181            _settings: &Self::Settings,
2182            _load_context: &mut LoadContext<'_>,
2183        ) -> Result<Self::Asset, Self::Error> {
2184            self.in_loader_sender.send_blocking(()).unwrap();
2185            let _ = self.gate_receiver.recv().await;
2186            Ok(TestAsset)
2187        }
2188
2189        fn extensions(&self) -> &[&str] {
2190            &["ron"]
2191        }
2192    }
2193
2194    #[test]
2195    fn dropping_handle_while_loading_cancels_load() {
2196        let (mut app, dir) = create_app();
2197
2198        let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2199        let (gate_sender, gate_receiver) = async_channel::bounded(1);
2200
2201        app.init_asset::<TestAsset>()
2202            .register_asset_loader(GatedLoader {
2203                in_loader_sender,
2204                gate_receiver,
2205            });
2206
2207        let path = Path::new("abc.ron");
2208        dir.insert_asset_text(path, "blah");
2209
2210        let asset_server = app.world().resource::<AssetServer>().clone();
2211
2212        // Start loading the asset. This load will get blocked by the gate.
2213        let handle = asset_server.load::<TestAsset>(path);
2214        assert!(asset_server.get_load_state(&handle).unwrap().is_loading());
2215        app.update();
2216
2217        // Make sure we are inside the loader before continuing.
2218        in_loader_receiver.recv_blocking().unwrap();
2219
2220        let asset_id = handle.id();
2221        // Dropping the handle and doing another update should result in the load being cancelled.
2222        drop(handle);
2223        app.update();
2224        assert!(asset_server.get_load_state(asset_id).is_none());
2225
2226        // Unblock the loader and then update a few times, showing that the asset never loads.
2227        gate_sender.send_blocking(()).unwrap();
2228        for _ in 0..10 {
2229            app.update();
2230            for message in app
2231                .world()
2232                .resource::<Messages<AssetEvent<TestAsset>>>()
2233                .iter_current_update_messages()
2234            {
2235                match message {
2236                    AssetEvent::Unused { .. } => {}
2237                    message => panic!("No asset events are allowed: {message:?}"),
2238                }
2239            }
2240        }
2241    }
2242
2243    #[test]
2244    fn dropping_subasset_handle_while_loading_cancels_load() {
2245        let (mut app, dir) = create_app();
2246
2247        let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2248        let (gate_sender, gate_receiver) = async_channel::bounded(1);
2249
2250        app.init_asset::<TestAsset>()
2251            .register_asset_loader(GatedLoader {
2252                in_loader_sender,
2253                gate_receiver,
2254            });
2255
2256        let path = Path::new("abc.ron");
2257        dir.insert_asset_text(path, "blah");
2258
2259        let asset_server = app.world().resource::<AssetServer>().clone();
2260
2261        // Start loading the subasset. This load will get blocked by the gate.
2262        // Note: it doesn't matter that the subasset doesn't actually end up existing, since the
2263        // asset system doesn't know that until after the load completes, which we cancel anyway.
2264        let handle = asset_server.load::<TestAsset>("abc.ron#sub");
2265        assert!(asset_server.get_load_state(&handle).unwrap().is_loading());
2266        app.update();
2267
2268        // Make sure we are inside the loader before continuing.
2269        in_loader_receiver.recv_blocking().unwrap();
2270
2271        let asset_id = handle.id();
2272        // Dropping the handle and doing another update should result in the load being cancelled.
2273        drop(handle);
2274        app.update();
2275        assert!(asset_server.get_load_state(asset_id).is_none());
2276
2277        // Unblock the loader and then update a few times, showing that the asset never loads.
2278        gate_sender.send_blocking(()).unwrap();
2279        for _ in 0..10 {
2280            app.update();
2281            for message in app
2282                .world()
2283                .resource::<Messages<AssetEvent<TestAsset>>>()
2284                .iter_current_update_messages()
2285            {
2286                match message {
2287                    AssetEvent::Unused { .. } => {}
2288                    message => panic!("No asset events are allowed: {message:?}"),
2289                }
2290            }
2291        }
2292    }
2293
2294    // Creates a basic app with the default asset source engineered to get back the asset event
2295    // sender.
2296    fn create_app_with_source_event_sender() -> (App, Dir, Sender<AssetSourceEvent>) {
2297        let mut app = App::new();
2298        let dir = Dir::default();
2299        let memory_reader = MemoryAssetReader { root: dir.clone() };
2300
2301        // Create a channel to pass the source event sender back to us.
2302        let (sender_sender, sender_receiver) = crossbeam_channel::bounded(1);
2303
2304        struct FakeWatcher;
2305        impl AssetWatcher for FakeWatcher {}
2306
2307        app.register_asset_source(
2308            AssetSourceId::Default,
2309            AssetSourceBuilder::new(move || Box::new(memory_reader.clone())).with_watcher(
2310                move |sender| {
2311                    sender_sender.send(sender).unwrap();
2312                    Some(Box::new(FakeWatcher))
2313                },
2314            ),
2315        )
2316        .add_plugins((
2317            TaskPoolPlugin::default(),
2318            AssetPlugin {
2319                watch_for_changes_override: Some(true),
2320                ..Default::default()
2321            },
2322        ));
2323
2324        let sender = sender_receiver.try_recv().unwrap();
2325
2326        (app, dir, sender)
2327    }
2328
2329    fn collect_asset_events<A: Asset>(world: &mut World) -> Vec<AssetEvent<A>> {
2330        world
2331            .resource_mut::<Messages<AssetEvent<A>>>()
2332            .drain()
2333            .collect()
2334    }
2335
2336    fn collect_asset_load_failed_events<A: Asset>(
2337        world: &mut World,
2338    ) -> Vec<AssetLoadFailedEvent<A>> {
2339        world
2340            .resource_mut::<Messages<AssetLoadFailedEvent<A>>>()
2341            .drain()
2342            .collect()
2343    }
2344
2345    #[test]
2346    fn reloads_asset_after_source_event() {
2347        let (mut app, dir, source_events) = create_app_with_source_event_sender();
2348        let asset_server = app.world().resource::<AssetServer>().clone();
2349
2350        dir.insert_asset_text(
2351            Path::new("abc.cool.ron"),
2352            r#"(
2353    text: "a",
2354    dependencies: [],
2355    embedded_dependencies: [],
2356    sub_texts: [],
2357)"#,
2358        );
2359
2360        app.init_asset::<CoolText>()
2361            .init_asset::<SubText>()
2362            .register_asset_loader(CoolTextLoader);
2363
2364        let handle: Handle<CoolText> = asset_server.load("abc.cool.ron");
2365        run_app_until(&mut app, |world| {
2366            let messages = collect_asset_events(world);
2367            if messages.is_empty() {
2368                return None;
2369            }
2370            assert_eq!(
2371                messages,
2372                [
2373                    AssetEvent::LoadedWithDependencies { id: handle.id() },
2374                    AssetEvent::Added { id: handle.id() },
2375                ]
2376            );
2377            Some(())
2378        });
2379
2380        // Sending an asset event should result in the asset being reloaded - resulting in a
2381        // "Modified" message.
2382        source_events
2383            .send_blocking(AssetSourceEvent::ModifiedAsset(PathBuf::from(
2384                "abc.cool.ron",
2385            )))
2386            .unwrap();
2387
2388        run_app_until(&mut app, |world| {
2389            let messages = collect_asset_events(world);
2390            if messages.is_empty() {
2391                return None;
2392            }
2393            assert_eq!(
2394                messages,
2395                [
2396                    AssetEvent::LoadedWithDependencies { id: handle.id() },
2397                    AssetEvent::Modified { id: handle.id() }
2398                ]
2399            );
2400            Some(())
2401        });
2402    }
2403
2404    #[test]
2405    fn added_asset_reloads_previously_missing_asset() {
2406        let (mut app, dir, source_events) = create_app_with_source_event_sender();
2407        let asset_server = app.world().resource::<AssetServer>().clone();
2408
2409        app.init_asset::<CoolText>()
2410            .init_asset::<SubText>()
2411            .register_asset_loader(CoolTextLoader);
2412
2413        let handle: Handle<CoolText> = asset_server.load("abc.cool.ron");
2414        run_app_until(&mut app, |world| {
2415            let failed_ids = collect_asset_load_failed_events(world)
2416                .drain(..)
2417                .map(|event| event.id)
2418                .collect::<Vec<_>>();
2419            if failed_ids.is_empty() {
2420                return None;
2421            }
2422            assert_eq!(failed_ids, [handle.id()]);
2423            Some(())
2424        });
2425
2426        // The asset has already been considered as failed to load. Now we add the asset data, and
2427        // send an AddedAsset event.
2428        dir.insert_asset_text(
2429            Path::new("abc.cool.ron"),
2430            r#"(
2431    text: "a",
2432    dependencies: [],
2433    embedded_dependencies: [],
2434    sub_texts: [],
2435)"#,
2436        );
2437        source_events
2438            .send_blocking(AssetSourceEvent::AddedAsset(PathBuf::from("abc.cool.ron")))
2439            .unwrap();
2440
2441        run_app_until(&mut app, |world| {
2442            let messages = collect_asset_events(world);
2443            if messages.is_empty() {
2444                return None;
2445            }
2446            assert_eq!(
2447                messages,
2448                [
2449                    AssetEvent::LoadedWithDependencies { id: handle.id() },
2450                    AssetEvent::Added { id: handle.id() }
2451                ]
2452            );
2453            Some(())
2454        });
2455    }
2456
2457    #[test]
2458    fn same_asset_different_settings() {
2459        // Test loading the same asset twice with different settings. This should
2460        // produce two distinct assets.
2461
2462        // First, implement an asset that's a single u8, whose value is copied from
2463        // the loader settings.
2464
2465        #[derive(Asset, TypePath)]
2466        struct U8Asset(u8);
2467
2468        #[derive(Serialize, Deserialize, Default)]
2469        struct U8LoaderSettings(u8);
2470
2471        #[derive(TypePath)]
2472        struct U8Loader;
2473
2474        impl AssetLoader for U8Loader {
2475            type Asset = U8Asset;
2476            type Settings = U8LoaderSettings;
2477            type Error = crate::loader::LoadDirectError;
2478
2479            async fn load(
2480                &self,
2481                _: &mut dyn Reader,
2482                settings: &Self::Settings,
2483                _: &mut LoadContext<'_>,
2484            ) -> Result<Self::Asset, Self::Error> {
2485                Ok(U8Asset(settings.0))
2486            }
2487
2488            fn extensions(&self) -> &[&str] {
2489                &["u8"]
2490            }
2491        }
2492
2493        // Create a test asset and setup the app.
2494
2495        let (mut app, dir) = create_app();
2496        dir.insert_asset(Path::new("test.u8"), &[]);
2497
2498        app.init_asset::<U8Asset>().register_asset_loader(U8Loader);
2499
2500        let asset_server = app.world().resource::<AssetServer>();
2501
2502        // Load the test asset twice but with different settings.
2503
2504        fn load(asset_server: &AssetServer, path: &'static str, value: u8) -> Handle<U8Asset> {
2505            asset_server.load_with_settings::<U8Asset, U8LoaderSettings>(
2506                path,
2507                move |s: &mut U8LoaderSettings| s.0 = value,
2508            )
2509        }
2510
2511        let handle_1 = load(asset_server, "test.u8", 1);
2512        let handle_2 = load(asset_server, "test.u8", 2);
2513
2514        // Handles should be different.
2515
2516        // These handles should be different, but due to
2517        // https://github.com/bevyengine/bevy/pull/21564, they are not. Once 21564 is fixed, we
2518        // should replace these expects.
2519        //
2520        // assert_ne!(handle_1, handle_2);
2521        assert_eq!(handle_1, handle_2);
2522
2523        run_app_until(&mut app, |world| {
2524            let (Some(asset_1), Some(asset_2)) = (
2525                world.resource::<Assets<U8Asset>>().get(&handle_1),
2526                world.resource::<Assets<U8Asset>>().get(&handle_2),
2527            ) else {
2528                return None;
2529            };
2530
2531            // Values should match the settings.
2532
2533            // These values should be different, but due to
2534            // https://github.com/bevyengine/bevy/pull/21564, they are not. Once 21564 is fixed, we
2535            // should replace these expects.
2536            //
2537            // assert_eq!(asset_1.0, 1);
2538            // assert_eq!(asset_2.0, 2);
2539            assert_eq!(asset_1.0, asset_2.0);
2540
2541            Some(())
2542        });
2543    }
2544
2545    #[test]
2546    fn loading_two_subassets_does_not_start_two_loads() {
2547        let (mut app, dir) = create_app();
2548        dir.insert_asset(Path::new("test.txt"), &[]);
2549
2550        #[derive(TypePath)]
2551        struct TwoSubassetLoader;
2552
2553        impl AssetLoader for TwoSubassetLoader {
2554            type Asset = TestAsset;
2555            type Settings = ();
2556            type Error = std::io::Error;
2557
2558            async fn load(
2559                &self,
2560                _reader: &mut dyn Reader,
2561                _settings: &Self::Settings,
2562                load_context: &mut LoadContext<'_>,
2563            ) -> Result<Self::Asset, Self::Error> {
2564                load_context.add_labeled_asset("A".into(), TestAsset);
2565                load_context.add_labeled_asset("B".into(), TestAsset);
2566                Ok(TestAsset)
2567            }
2568
2569            fn extensions(&self) -> &[&str] {
2570                &["txt"]
2571            }
2572        }
2573
2574        app.init_asset::<TestAsset>()
2575            .register_asset_loader(TwoSubassetLoader);
2576
2577        let asset_server = app.world().resource::<AssetServer>().clone();
2578        let _subasset_1: Handle<TestAsset> = asset_server.load("test.txt#A");
2579        let _subasset_2: Handle<TestAsset> = asset_server.load("test.txt#B");
2580
2581        app.update();
2582
2583        // Due to https://github.com/bevyengine/bevy/issues/12756, this expectation fails. Once
2584        // #12756 is fixed, we should swap these asserts.
2585        //
2586        // assert_eq!(get_started_load_count(app.world()), 1);
2587        assert_eq!(get_started_load_count(app.world()), 2);
2588    }
2589
2590    /// A loader that immediately returns a [`TestAsset`].
2591    #[derive(TypePath)]
2592    struct TrivialLoader;
2593
2594    impl AssetLoader for TrivialLoader {
2595        type Asset = TestAsset;
2596        type Settings = ();
2597        type Error = std::io::Error;
2598
2599        async fn load(
2600            &self,
2601            _reader: &mut dyn Reader,
2602            _settings: &Self::Settings,
2603            _load_context: &mut LoadContext<'_>,
2604        ) -> Result<Self::Asset, Self::Error> {
2605            Ok(TestAsset)
2606        }
2607
2608        fn extensions(&self) -> &[&str] {
2609            &["txt"]
2610        }
2611    }
2612
2613    #[test]
2614    fn get_strong_handle_prevents_reload_when_asset_still_alive() {
2615        let (mut app, dir) = create_app();
2616        dir.insert_asset(Path::new("test.txt"), &[]);
2617
2618        app.init_asset::<TestAsset>()
2619            .register_asset_loader(TrivialLoader);
2620
2621        let asset_server = app.world().resource::<AssetServer>().clone();
2622        let original_handle: Handle<TestAsset> = asset_server.load("test.txt");
2623
2624        // Wait for the asset to load.
2625        run_app_until(&mut app, |world| {
2626            world
2627                .resource::<Assets<TestAsset>>()
2628                .get(&original_handle)
2629                .map(|_| ())
2630        });
2631
2632        assert_eq!(get_started_load_count(app.world()), 1);
2633
2634        // Get a new strong handle from the original handle's ID.
2635        let new_handle = app
2636            .world_mut()
2637            .resource_mut::<Assets<TestAsset>>()
2638            .get_strong_handle(original_handle.id())
2639            .unwrap();
2640
2641        // Drop the original handle. This should still leave the asset alive.
2642        drop(original_handle);
2643
2644        app.update();
2645        assert!(app
2646            .world()
2647            .resource::<Assets<TestAsset>>()
2648            .get(&new_handle)
2649            .is_some());
2650
2651        let _other_handle: Handle<TestAsset> = asset_server.load("test.txt");
2652        app.update();
2653        // The asset server should **not** have started a new load, since the asset is still alive.
2654
2655        // Due to https://github.com/bevyengine/bevy/issues/20651, we do get a second load. Once
2656        // #20651 is fixed, we should swap these asserts.
2657        //
2658        // assert_eq!(get_started_load_count(app.world()), 1);
2659        assert_eq!(get_started_load_count(app.world()), 2);
2660    }
2661
2662    #[test]
2663    fn immediate_nested_asset_loads_dependency() {
2664        let (mut app, dir) = create_app();
2665
2666        /// This asset holds a handle to its dependency.
2667        #[derive(Asset, TypePath)]
2668        struct DeferredNested(Handle<TestAsset>);
2669
2670        #[derive(TypePath)]
2671        struct DeferredNestedLoader;
2672
2673        impl AssetLoader for DeferredNestedLoader {
2674            type Asset = DeferredNested;
2675            type Settings = ();
2676            type Error = std::io::Error;
2677
2678            async fn load(
2679                &self,
2680                reader: &mut dyn Reader,
2681                _: &Self::Settings,
2682                load_context: &mut LoadContext<'_>,
2683            ) -> Result<Self::Asset, Self::Error> {
2684                let mut nested_path = String::new();
2685                reader.read_to_string(&mut nested_path).await?;
2686                Ok(DeferredNested(load_context.load(nested_path)))
2687            }
2688
2689            fn extensions(&self) -> &[&str] {
2690                &["defer"]
2691            }
2692        }
2693
2694        /// This asset holds a handle a dependency of one of its dependencies.
2695        #[derive(Asset, TypePath)]
2696        struct ImmediateNested(Handle<TestAsset>);
2697
2698        #[derive(TypePath)]
2699        struct ImmediateNestedLoader;
2700
2701        impl AssetLoader for ImmediateNestedLoader {
2702            type Asset = ImmediateNested;
2703            type Settings = ();
2704            type Error = std::io::Error;
2705
2706            async fn load(
2707                &self,
2708                reader: &mut dyn Reader,
2709                _: &Self::Settings,
2710                load_context: &mut LoadContext<'_>,
2711            ) -> Result<Self::Asset, Self::Error> {
2712                let mut nested_path = String::new();
2713                reader.read_to_string(&mut nested_path).await?;
2714                let deferred_nested: LoadedAsset<DeferredNested> = load_context
2715                    .loader()
2716                    .immediate()
2717                    .load(nested_path)
2718                    .await
2719                    .unwrap();
2720                Ok(ImmediateNested(deferred_nested.get().0.clone()))
2721            }
2722
2723            fn extensions(&self) -> &[&str] {
2724                &["immediate"]
2725            }
2726        }
2727
2728        app.init_asset::<TestAsset>()
2729            .init_asset::<DeferredNested>()
2730            .init_asset::<ImmediateNested>()
2731            .register_asset_loader(TrivialLoader)
2732            .register_asset_loader(DeferredNestedLoader)
2733            .register_asset_loader(ImmediateNestedLoader);
2734
2735        dir.insert_asset_text(Path::new("a.immediate"), "b.defer");
2736        dir.insert_asset_text(Path::new("b.defer"), "c.txt");
2737        dir.insert_asset_text(Path::new("c.txt"), "hiya");
2738
2739        let server = app.world().resource::<AssetServer>().clone();
2740        let immediate_handle: Handle<ImmediateNested> = server.load("a.immediate");
2741
2742        run_app_until(&mut app, |world| {
2743            let immediate_assets = world.resource::<Assets<ImmediateNested>>();
2744            let immediate = immediate_assets.get(&immediate_handle)?;
2745
2746            let test_asset_handle = immediate.0.clone();
2747            world
2748                .resource::<Assets<TestAsset>>()
2749                .get(&test_asset_handle)?;
2750
2751            // The immediate asset is loaded, and the asset it got from its immediate load is also
2752            // loaded.
2753            Some(())
2754        });
2755    }
2756
2757    #[expect(dead_code, reason = "used by tests not backported to 0.18")]
2758    pub(crate) fn read_asset_as_string(dir: &Dir, path: &Path) -> String {
2759        let bytes = dir.get_asset(path).unwrap();
2760        str::from_utf8(bytes.value()).unwrap().to_string()
2761    }
2762
2763    pub(crate) fn read_meta_as_string(dir: &Dir, path: &Path) -> String {
2764        let bytes = dir.get_metadata(path).unwrap();
2765        str::from_utf8(bytes.value()).unwrap().to_string()
2766    }
2767
2768    #[test]
2769    fn write_default_meta_does_not_overwrite() {
2770        let (mut app, source) = create_app();
2771
2772        app.register_asset_loader(CoolTextLoader);
2773
2774        const ASSET_PATH: &str = "abc.cool.ron";
2775        source.insert_asset_text(Path::new(ASSET_PATH), "blah");
2776        const META_TEXT: &str = "hey i'm walkin here!";
2777        source.insert_meta_text(Path::new(ASSET_PATH), META_TEXT);
2778
2779        let asset_server = app.world().resource::<AssetServer>().clone();
2780        assert!(matches!(
2781            bevy_tasks::block_on(asset_server.write_default_loader_meta_file_for_path(ASSET_PATH)),
2782            Err(WriteDefaultMetaError::MetaAlreadyExists)
2783        ));
2784
2785        assert_eq!(
2786            read_meta_as_string(&source, Path::new(ASSET_PATH)),
2787            META_TEXT
2788        );
2789    }
2790
2791    #[test]
2792    fn asset_dependency_is_tracked_when_not_loaded() {
2793        let (mut app, dir) = create_app();
2794
2795        #[derive(Asset, TypePath)]
2796        struct AssetWithDep {
2797            #[dependency]
2798            dep: Handle<TestAsset>,
2799        }
2800
2801        #[derive(TypePath)]
2802        struct AssetWithDepLoader;
2803
2804        impl AssetLoader for AssetWithDepLoader {
2805            type Asset = TestAsset;
2806            type Settings = ();
2807            type Error = std::io::Error;
2808
2809            async fn load(
2810                &self,
2811                _reader: &mut dyn Reader,
2812                _settings: &Self::Settings,
2813                load_context: &mut LoadContext<'_>,
2814            ) -> Result<Self::Asset, Self::Error> {
2815                // Load the asset in the root context, but then put the handle in the subasset. So
2816                // the subasset's (internal) load context never loaded `dep`.
2817                let dep = load_context.load::<TestAsset>("abc.ron");
2818                load_context.add_labeled_asset("subasset".into(), AssetWithDep { dep });
2819                Ok(TestAsset)
2820            }
2821
2822            fn extensions(&self) -> &[&str] {
2823                &["with_deps"]
2824            }
2825        }
2826
2827        // Write some data so the loaders have something to load (even though they don't use the
2828        // data).
2829        dir.insert_asset_text(Path::new("abc.ron"), "");
2830        dir.insert_asset_text(Path::new("blah.with_deps"), "");
2831
2832        let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
2833        let (gate_sender, gate_receiver) = async_channel::bounded(1);
2834        app.init_asset::<TestAsset>()
2835            .init_asset::<AssetWithDep>()
2836            .register_asset_loader(GatedLoader {
2837                in_loader_sender,
2838                gate_receiver,
2839            })
2840            .register_asset_loader(AssetWithDepLoader);
2841
2842        let asset_server = app.world().resource::<AssetServer>().clone();
2843        let subasset_handle: Handle<AssetWithDep> = asset_server.load("blah.with_deps#subasset");
2844
2845        run_app_until(&mut app, |_| {
2846            asset_server.is_loaded(&subasset_handle).then_some(())
2847        });
2848        // Even though the subasset is loaded, and its load context never loaded its dependency, it
2849        // still depends on its dependency, so that is tracked correctly here.
2850        assert!(!asset_server.is_loaded_with_dependencies(&subasset_handle));
2851
2852        let dep_handle: Handle<TestAsset> = app
2853            .world()
2854            .resource::<Assets<AssetWithDep>>()
2855            .get(&subasset_handle)
2856            .unwrap()
2857            .dep
2858            .clone();
2859
2860        // Pass the gate in the dependency loader.
2861        in_loader_receiver.recv_blocking().unwrap();
2862        gate_sender.send_blocking(()).unwrap();
2863
2864        run_app_until(&mut app, |_| {
2865            asset_server.is_loaded(&dep_handle).then_some(())
2866        });
2867        // Now that the dependency is loaded, the subasset is counted as loaded with dependencies!
2868        assert!(asset_server.is_loaded_with_dependencies(&subasset_handle));
2869    }
2870}