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