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