#![allow(missing_docs)]
pub mod io;
pub mod meta;
pub mod processor;
pub mod saver;
pub mod transformer;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets, Handle,
UntypedHandle,
};
}
mod assets;
mod event;
mod folder;
mod handle;
mod id;
mod loader;
mod path;
mod reflect;
mod server;
pub use assets::*;
pub use bevy_asset_macros::Asset;
pub use event::*;
pub use folder::*;
pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
pub use handle::*;
pub use id::*;
pub use loader::*;
pub use path::*;
pub use reflect::*;
pub use server::*;
pub use bevy_utils::BoxedFuture;
pub use ron;
use crate::{
io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId},
processor::{AssetProcessor, Process},
};
use bevy_app::{App, First, MainScheduleOrder, Plugin, PostUpdate};
use bevy_ecs::{
reflect::AppTypeRegistry,
schedule::{IntoSystemConfigs, IntoSystemSetConfigs, ScheduleLabel, SystemSet},
system::Resource,
world::FromWorld,
};
use bevy_log::error;
use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath};
use bevy_utils::HashSet;
use std::{any::TypeId, sync::Arc};
#[cfg(all(feature = "file_watcher", not(feature = "multi-threaded")))]
compile_error!(
"The \"file_watcher\" feature for hot reloading requires the \
\"multi-threaded\" feature to be functional.\n\
Consider either disabling the \"file_watcher\" feature or enabling \"multi-threaded\""
);
pub struct AssetPlugin {
pub file_path: String,
pub processed_file_path: String,
pub watch_for_changes_override: Option<bool>,
pub mode: AssetMode,
}
#[derive(Debug)]
pub enum AssetMode {
Unprocessed,
Processed,
}
#[derive(Debug, Default, Clone, Resource)]
pub enum AssetMetaCheck {
#[default]
Always,
Paths(HashSet<AssetPath<'static>>),
Never,
}
impl Default for AssetPlugin {
fn default() -> Self {
Self {
mode: AssetMode::Unprocessed,
file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(),
processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
watch_for_changes_override: None,
}
}
}
impl AssetPlugin {
const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets";
const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default";
}
impl Plugin for AssetPlugin {
fn build(&self, app: &mut App) {
app.init_schedule(UpdateAssets).init_schedule(AssetEvents);
let embedded = EmbeddedAssetRegistry::default();
{
let mut sources = app
.world
.get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
sources.init_default_source(
&self.file_path,
(!matches!(self.mode, AssetMode::Unprocessed))
.then_some(self.processed_file_path.as_str()),
);
embedded.register_source(&mut sources);
}
{
let mut watch = cfg!(feature = "watch");
if let Some(watch_override) = self.watch_for_changes_override {
watch = watch_override;
}
match self.mode {
AssetMode::Unprocessed => {
let mut builders = app.world.resource_mut::<AssetSourceBuilders>();
let sources = builders.build_sources(watch, false);
let meta_check = app
.world
.get_resource::<AssetMetaCheck>()
.cloned()
.unwrap_or_else(AssetMetaCheck::default);
app.insert_resource(AssetServer::new_with_meta_check(
sources,
AssetServerMode::Unprocessed,
meta_check,
watch,
));
}
AssetMode::Processed => {
#[cfg(feature = "asset_processor")]
{
let mut builders = app.world.resource_mut::<AssetSourceBuilders>();
let processor = AssetProcessor::new(&mut builders);
let mut sources = builders.build_sources(false, watch);
sources.gate_on_processor(processor.data.clone());
app.insert_resource(AssetServer::new_with_loaders(
sources,
processor.server().data.loaders.clone(),
AssetServerMode::Processed,
AssetMetaCheck::Always,
watch,
))
.insert_resource(processor)
.add_systems(bevy_app::Startup, AssetProcessor::start);
}
#[cfg(not(feature = "asset_processor"))]
{
let mut builders = app.world.resource_mut::<AssetSourceBuilders>();
let sources = builders.build_sources(false, watch);
app.insert_resource(AssetServer::new_with_meta_check(
sources,
AssetServerMode::Processed,
AssetMetaCheck::Always,
watch,
));
}
}
}
}
app.insert_resource(embedded)
.init_asset::<LoadedFolder>()
.init_asset::<LoadedUntypedAsset>()
.init_asset::<()>()
.add_event::<UntypedAssetLoadFailedEvent>()
.configure_sets(
UpdateAssets,
TrackAssets.after(handle_internal_asset_events),
)
.add_systems(UpdateAssets, handle_internal_asset_events)
.register_type::<AssetPath>();
let mut order = app.world.resource_mut::<MainScheduleOrder>();
order.insert_after(First, UpdateAssets);
order.insert_after(PostUpdate, AssetEvents);
}
}
pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {}
pub trait VisitAssetDependencies {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId));
}
impl<A: Asset> VisitAssetDependencies for Handle<A> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
visit(self.id().untyped());
}
}
impl<A: Asset> VisitAssetDependencies for Option<Handle<A>> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
if let Some(handle) = self {
visit(handle.id().untyped());
}
}
}
impl VisitAssetDependencies for UntypedHandle {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
visit(self.id());
}
}
impl VisitAssetDependencies for Option<UntypedHandle> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
if let Some(handle) = self {
visit(handle.id());
}
}
}
impl<A: Asset> VisitAssetDependencies for Vec<Handle<A>> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
for dependency in self {
visit(dependency.id().untyped());
}
}
}
impl VisitAssetDependencies for Vec<UntypedHandle> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
for dependency in self {
visit(dependency.id());
}
}
}
pub trait AssetApp {
fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self;
fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self;
fn register_asset_source(
&mut self,
id: impl Into<AssetSourceId<'static>>,
source: AssetSourceBuilder,
) -> &mut Self;
fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self;
fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self;
fn init_asset<A: Asset>(&mut self) -> &mut Self;
fn register_asset_reflect<A>(&mut self) -> &mut Self
where
A: Asset + Reflect + FromReflect + GetTypeRegistration;
fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self;
}
impl AssetApp for App {
fn register_asset_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
self.world.resource::<AssetServer>().register_loader(loader);
self
}
fn register_asset_processor<P: Process>(&mut self, processor: P) -> &mut Self {
if let Some(asset_processor) = self.world.get_resource::<AssetProcessor>() {
asset_processor.register_processor(processor);
}
self
}
fn register_asset_source(
&mut self,
id: impl Into<AssetSourceId<'static>>,
source: AssetSourceBuilder,
) -> &mut Self {
let id = id.into();
if self.world.get_resource::<AssetServer>().is_some() {
error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id);
}
{
let mut sources = self
.world
.get_resource_or_insert_with(AssetSourceBuilders::default);
sources.insert(id, source);
}
self
}
fn set_default_asset_processor<P: Process>(&mut self, extension: &str) -> &mut Self {
if let Some(asset_processor) = self.world.get_resource::<AssetProcessor>() {
asset_processor.set_default_processor::<P>(extension);
}
self
}
fn init_asset_loader<L: AssetLoader + FromWorld>(&mut self) -> &mut Self {
let loader = L::from_world(&mut self.world);
self.register_asset_loader(loader)
}
fn init_asset<A: Asset>(&mut self) -> &mut Self {
let assets = Assets::<A>::default();
self.world.resource::<AssetServer>().register_asset(&assets);
if self.world.contains_resource::<AssetProcessor>() {
let processor = self.world.resource::<AssetProcessor>();
processor
.server()
.register_handle_provider(AssetHandleProvider::new(
TypeId::of::<A>(),
Arc::new(AssetIndexAllocator::default()),
));
}
self.insert_resource(assets)
.allow_ambiguous_resource::<Assets<A>>()
.add_event::<AssetEvent<A>>()
.add_event::<AssetLoadFailedEvent<A>>()
.register_type::<Handle<A>>()
.register_type::<AssetId<A>>()
.add_systems(
AssetEvents,
Assets::<A>::asset_events.run_if(Assets::<A>::asset_events_condition),
)
.add_systems(UpdateAssets, Assets::<A>::track_assets.in_set(TrackAssets))
}
fn register_asset_reflect<A>(&mut self) -> &mut Self
where
A: Asset + Reflect + FromReflect + GetTypeRegistration,
{
let type_registry = self.world.resource::<AppTypeRegistry>();
{
let mut type_registry = type_registry.write();
type_registry.register::<A>();
type_registry.register::<Handle<A>>();
type_registry.register_type_data::<A, ReflectAsset>();
type_registry.register_type_data::<Handle<A>, ReflectHandle>();
}
self
}
fn preregister_asset_loader<L: AssetLoader>(&mut self, extensions: &[&str]) -> &mut Self {
self.world
.resource_mut::<AssetServer>()
.preregister_loader::<L>(extensions);
self
}
}
#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
pub struct TrackAssets;
#[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
pub struct UpdateAssets;
#[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
pub struct AssetEvents;
#[cfg(test)]
mod tests {
use crate::{
self as bevy_asset,
folder::LoadedFolder,
handle::Handle,
io::{
gated::{GateOpener, GatedReader},
memory::{Dir, MemoryAssetReader},
AssetReader, AssetReaderError, AssetSource, AssetSourceId, Reader,
},
loader::{AssetLoader, LoadContext},
Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
AssetPlugin, AssetServer, Assets, DependencyLoadState, LoadState,
RecursiveDependencyLoadState,
};
use bevy_app::{App, Update};
use bevy_core::TaskPoolPlugin;
use bevy_ecs::prelude::*;
use bevy_ecs::{
event::ManualEventReader,
schedule::{LogLevel, ScheduleBuildSettings},
};
use bevy_log::LogPlugin;
use bevy_reflect::TypePath;
use bevy_utils::{BoxedFuture, Duration, HashMap};
use futures_lite::AsyncReadExt;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use thiserror::Error;
#[derive(Asset, TypePath, Debug)]
pub struct CoolText {
pub text: String,
pub embedded: String,
#[dependency]
pub dependencies: Vec<Handle<CoolText>>,
#[dependency]
pub sub_texts: Vec<Handle<SubText>>,
}
#[derive(Asset, TypePath, Debug)]
pub struct SubText {
text: String,
}
#[derive(Serialize, Deserialize)]
pub struct CoolTextRon {
text: String,
dependencies: Vec<String>,
embedded_dependencies: Vec<String>,
sub_texts: Vec<String>,
}
#[derive(Default)]
pub struct CoolTextLoader;
#[derive(Error, Debug)]
pub enum CoolTextLoaderError {
#[error("Could not load dependency: {dependency}")]
CannotLoadDependency { dependency: AssetPath<'static> },
#[error("A RON error occurred during loading")]
RonSpannedError(#[from] ron::error::SpannedError),
#[error("An IO error occurred during loading")]
Io(#[from] std::io::Error),
}
impl AssetLoader for CoolTextLoader {
type Asset = CoolText;
type Settings = ();
type Error = CoolTextLoaderError;
fn load<'a>(
&'a self,
reader: &'a mut Reader,
_settings: &'a Self::Settings,
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
Box::pin(async move {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
let mut embedded = String::new();
for dep in ron.embedded_dependencies {
let loaded = load_context.load_direct(&dep).await.map_err(|_| {
Self::Error::CannotLoadDependency {
dependency: dep.into(),
}
})?;
let cool = loaded.get::<CoolText>().unwrap();
embedded.push_str(&cool.text);
}
Ok(CoolText {
text: ron.text,
embedded,
dependencies: ron
.dependencies
.iter()
.map(|p| load_context.load(p))
.collect(),
sub_texts: ron
.sub_texts
.drain(..)
.map(|text| load_context.add_labeled_asset(text.clone(), SubText { text }))
.collect(),
})
})
}
fn extensions(&self) -> &[&str] {
&["cool.ron"]
}
}
#[derive(Default, Clone)]
pub struct UnstableMemoryAssetReader {
pub attempt_counters: Arc<std::sync::Mutex<HashMap<PathBuf, usize>>>,
pub load_delay: Duration,
memory_reader: MemoryAssetReader,
failure_count: usize,
}
impl UnstableMemoryAssetReader {
pub fn new(root: Dir, failure_count: usize) -> Self {
Self {
load_delay: Duration::from_millis(10),
memory_reader: MemoryAssetReader { root },
attempt_counters: Default::default(),
failure_count,
}
}
}
impl AssetReader for UnstableMemoryAssetReader {
fn is_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<bool, AssetReaderError>> {
self.memory_reader.is_directory(path)
}
fn read_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<bevy_asset::io::PathStream>, AssetReaderError>> {
self.memory_reader.read_directory(path)
}
fn read_meta<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<bevy_asset::io::Reader<'a>>, AssetReaderError>> {
self.memory_reader.read_meta(path)
}
fn read<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<
'a,
Result<Box<bevy_asset::io::Reader<'a>>, bevy_asset::io::AssetReaderError>,
> {
let attempt_number = {
let key = PathBuf::from(path);
let mut attempt_counters = self.attempt_counters.lock().unwrap();
if let Some(existing) = attempt_counters.get_mut(&key) {
*existing += 1;
*existing
} else {
attempt_counters.insert(key, 1);
1
}
};
if attempt_number <= self.failure_count {
let io_error = std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
format!(
"Simulated failure {attempt_number} of {}",
self.failure_count
),
);
let wait = self.load_delay;
return Box::pin(async move {
std::thread::sleep(wait);
Err(AssetReaderError::Io(io_error.into()))
});
}
self.memory_reader.read(path)
}
}
fn test_app(dir: Dir) -> (App, GateOpener) {
let mut app = App::new();
let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir });
app.register_asset_source(
AssetSourceId::Default,
AssetSource::build().with_reader(move || Box::new(gated_memory_reader.clone())),
)
.add_plugins((
TaskPoolPlugin::default(),
LogPlugin::default(),
AssetPlugin::default(),
));
(app, gate_opener)
}
pub fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) {
for _ in 0..LARGE_ITERATION_COUNT {
app.update();
if predicate(&mut app.world).is_some() {
return;
}
}
panic!("Ran out of loops to return `Some` from `predicate`");
}
const LARGE_ITERATION_COUNT: usize = 10000;
fn get<A: Asset>(world: &World, id: AssetId<A>) -> Option<&A> {
world.resource::<Assets<A>>().get(id)
}
#[derive(Resource, Default)]
struct StoredEvents(Vec<AssetEvent<CoolText>>);
fn store_asset_events(
mut reader: EventReader<AssetEvent<CoolText>>,
mut storage: ResMut<StoredEvents>,
) {
storage.0.extend(reader.read().cloned());
}
#[test]
fn load_dependencies() {
#[cfg(not(feature = "multi-threaded"))]
panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded");
let dir = Dir::default();
let a_path = "a.cool.ron";
let a_ron = r#"
(
text: "a",
dependencies: [
"foo/b.cool.ron",
"c.cool.ron",
],
embedded_dependencies: [],
sub_texts: [],
)"#;
let b_path = "foo/b.cool.ron";
let b_ron = r#"
(
text: "b",
dependencies: [],
embedded_dependencies: [],
sub_texts: [],
)"#;
let c_path = "c.cool.ron";
let c_ron = r#"
(
text: "c",
dependencies: [
"d.cool.ron",
],
embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"],
sub_texts: ["hello"],
)"#;
let d_path = "d.cool.ron";
let d_ron = r#"
(
text: "d",
dependencies: [],
embedded_dependencies: [],
sub_texts: [],
)"#;
dir.insert_asset_text(Path::new(a_path), a_ron);
dir.insert_asset_text(Path::new(b_path), b_ron);
dir.insert_asset_text(Path::new(c_path), c_ron);
dir.insert_asset_text(Path::new(d_path), d_ron);
#[derive(Resource)]
struct IdResults {
b_id: AssetId<CoolText>,
c_id: AssetId<CoolText>,
d_id: AssetId<CoolText>,
}
let (mut app, gate_opener) = test_app(dir);
app.init_asset::<CoolText>()
.init_asset::<SubText>()
.init_resource::<StoredEvents>()
.register_asset_loader(CoolTextLoader)
.add_systems(Update, store_asset_events);
let asset_server = app.world.resource::<AssetServer>().clone();
let handle: Handle<CoolText> = asset_server.load(a_path);
let a_id = handle.id();
let entity = app.world.spawn(handle).id();
app.update();
{
let a_text = get::<CoolText>(&app.world, a_id);
let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
assert!(a_text.is_none(), "a's asset should not exist yet");
assert_eq!(a_load, LoadState::Loading, "a should still be loading");
assert_eq!(
a_deps,
DependencyLoadState::Loading,
"a deps should still be loading"
);
assert_eq!(
a_rec_deps,
RecursiveDependencyLoadState::Loading,
"a recursive deps should still be loading"
);
}
gate_opener.open(a_path);
run_app_until(&mut app, |world| {
let a_text = get::<CoolText>(world, a_id)?;
let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
assert_eq!(a_text.text, "a");
assert_eq!(a_text.dependencies.len(), 2);
assert_eq!(a_load, LoadState::Loaded, "a is loaded");
assert_eq!(a_deps, DependencyLoadState::Loading);
assert_eq!(a_rec_deps, RecursiveDependencyLoadState::Loading);
let b_id = a_text.dependencies[0].id();
let b_text = get::<CoolText>(world, b_id);
let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
assert!(b_text.is_none(), "b component should not exist yet");
assert_eq!(b_load, LoadState::Loading);
assert_eq!(b_deps, DependencyLoadState::Loading);
assert_eq!(b_rec_deps, RecursiveDependencyLoadState::Loading);
let c_id = a_text.dependencies[1].id();
let c_text = get::<CoolText>(world, c_id);
let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
assert!(c_text.is_none(), "c component should not exist yet");
assert_eq!(c_load, LoadState::Loading);
assert_eq!(c_deps, DependencyLoadState::Loading);
assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Loading);
Some(())
});
gate_opener.open(b_path);
run_app_until(&mut app, |world| {
let a_text = get::<CoolText>(world, a_id)?;
let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
assert_eq!(a_text.text, "a");
assert_eq!(a_text.dependencies.len(), 2);
assert_eq!(a_load, LoadState::Loaded);
assert_eq!(a_deps, DependencyLoadState::Loading);
assert_eq!(a_rec_deps, RecursiveDependencyLoadState::Loading);
let b_id = a_text.dependencies[0].id();
let b_text = get::<CoolText>(world, b_id)?;
let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
assert_eq!(b_text.text, "b");
assert_eq!(b_load, LoadState::Loaded);
assert_eq!(b_deps, DependencyLoadState::Loaded);
assert_eq!(b_rec_deps, RecursiveDependencyLoadState::Loaded);
let c_id = a_text.dependencies[1].id();
let c_text = get::<CoolText>(world, c_id);
let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
assert!(c_text.is_none(), "c component should not exist yet");
assert_eq!(c_load, LoadState::Loading);
assert_eq!(c_deps, DependencyLoadState::Loading);
assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Loading);
Some(())
});
gate_opener.open(c_path);
gate_opener.open(a_path);
gate_opener.open(b_path);
run_app_until(&mut app, |world| {
let a_text = get::<CoolText>(world, a_id)?;
let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
assert_eq!(a_text.text, "a");
assert_eq!(a_text.embedded, "");
assert_eq!(a_text.dependencies.len(), 2);
assert_eq!(a_load, LoadState::Loaded);
let b_id = a_text.dependencies[0].id();
let b_text = get::<CoolText>(world, b_id)?;
let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
assert_eq!(b_text.text, "b");
assert_eq!(b_text.embedded, "");
assert_eq!(b_load, LoadState::Loaded);
assert_eq!(b_deps, DependencyLoadState::Loaded);
assert_eq!(b_rec_deps, RecursiveDependencyLoadState::Loaded);
let c_id = a_text.dependencies[1].id();
let c_text = get::<CoolText>(world, c_id)?;
let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
assert_eq!(c_text.text, "c");
assert_eq!(c_text.embedded, "ab");
assert_eq!(c_load, LoadState::Loaded);
assert_eq!(
c_deps,
DependencyLoadState::Loading,
"c deps should not be loaded yet because d has not loaded"
);
assert_eq!(
c_rec_deps,
RecursiveDependencyLoadState::Loading,
"c rec deps should not be loaded yet because d has not loaded"
);
let sub_text_id = c_text.sub_texts[0].id();
let sub_text = get::<SubText>(world, sub_text_id)
.expect("subtext should exist if c exists. it came from the same loader");
assert_eq!(sub_text.text, "hello");
let (sub_text_load, sub_text_deps, sub_text_rec_deps) =
asset_server.get_load_states(sub_text_id).unwrap();
assert_eq!(sub_text_load, LoadState::Loaded);
assert_eq!(sub_text_deps, DependencyLoadState::Loaded);
assert_eq!(sub_text_rec_deps, RecursiveDependencyLoadState::Loaded);
let d_id = c_text.dependencies[0].id();
let d_text = get::<CoolText>(world, d_id);
let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
assert!(d_text.is_none(), "d component should not exist yet");
assert_eq!(d_load, LoadState::Loading);
assert_eq!(d_deps, DependencyLoadState::Loading);
assert_eq!(d_rec_deps, RecursiveDependencyLoadState::Loading);
assert_eq!(
a_deps,
DependencyLoadState::Loaded,
"If c has been loaded, the a deps should all be considered loaded"
);
assert_eq!(
a_rec_deps,
RecursiveDependencyLoadState::Loading,
"d is not loaded, so a's recursive deps should still be loading"
);
world.insert_resource(IdResults { b_id, c_id, d_id });
Some(())
});
gate_opener.open(d_path);
run_app_until(&mut app, |world| {
let a_text = get::<CoolText>(world, a_id)?;
let (_a_load, _a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
let c_id = a_text.dependencies[1].id();
let c_text = get::<CoolText>(world, c_id)?;
let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
assert_eq!(c_text.text, "c");
assert_eq!(c_text.embedded, "ab");
let d_id = c_text.dependencies[0].id();
let d_text = get::<CoolText>(world, d_id)?;
let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
assert_eq!(d_text.text, "d");
assert_eq!(d_text.embedded, "");
assert_eq!(c_load, LoadState::Loaded);
assert_eq!(c_deps, DependencyLoadState::Loaded);
assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Loaded);
assert_eq!(d_load, LoadState::Loaded);
assert_eq!(d_deps, DependencyLoadState::Loaded);
assert_eq!(d_rec_deps, RecursiveDependencyLoadState::Loaded);
assert_eq!(
a_rec_deps,
RecursiveDependencyLoadState::Loaded,
"d is loaded, so a's recursive deps should be loaded"
);
Some(())
});
{
let mut texts = app.world.resource_mut::<Assets<CoolText>>();
let a = texts.get_mut(a_id).unwrap();
a.text = "Changed".to_string();
}
app.world.despawn(entity);
app.update();
assert_eq!(
app.world.resource::<Assets<CoolText>>().len(),
0,
"CoolText asset entities should be despawned when no more handles exist"
);
app.update();
assert_eq!(
app.world.resource::<Assets<SubText>>().len(),
0,
"SubText asset entities should be despawned when no more handles exist"
);
let events = app.world.remove_resource::<StoredEvents>().unwrap();
let id_results = app.world.remove_resource::<IdResults>().unwrap();
let expected_events = vec![
AssetEvent::Added { id: a_id },
AssetEvent::LoadedWithDependencies {
id: id_results.b_id,
},
AssetEvent::Added {
id: id_results.b_id,
},
AssetEvent::Added {
id: id_results.c_id,
},
AssetEvent::LoadedWithDependencies {
id: id_results.d_id,
},
AssetEvent::LoadedWithDependencies {
id: id_results.c_id,
},
AssetEvent::LoadedWithDependencies { id: a_id },
AssetEvent::Added {
id: id_results.d_id,
},
AssetEvent::Modified { id: a_id },
AssetEvent::Unused { id: a_id },
AssetEvent::Removed { id: a_id },
AssetEvent::Unused {
id: id_results.b_id,
},
AssetEvent::Removed {
id: id_results.b_id,
},
AssetEvent::Unused {
id: id_results.c_id,
},
AssetEvent::Removed {
id: id_results.c_id,
},
AssetEvent::Unused {
id: id_results.d_id,
},
AssetEvent::Removed {
id: id_results.d_id,
},
];
assert_eq!(events.0, expected_events);
}
#[test]
fn failure_load_states() {
#[cfg(not(feature = "multi-threaded"))]
panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded");
let dir = Dir::default();
let a_path = "a.cool.ron";
let a_ron = r#"
(
text: "a",
dependencies: [
"b.cool.ron",
"c.cool.ron",
],
embedded_dependencies: [],
sub_texts: []
)"#;
let b_path = "b.cool.ron";
let b_ron = r#"
(
text: "b",
dependencies: [],
embedded_dependencies: [],
sub_texts: []
)"#;
let c_path = "c.cool.ron";
let c_ron = r#"
(
text: "c",
dependencies: [
"d.cool.ron",
],
embedded_dependencies: [],
sub_texts: []
)"#;
let d_path = "d.cool.ron";
let d_ron = r#"
(
text: "d",
dependencies: [],
OH NO THIS ASSET IS MALFORMED
embedded_dependencies: [],
sub_texts: []
)"#;
dir.insert_asset_text(Path::new(a_path), a_ron);
dir.insert_asset_text(Path::new(b_path), b_ron);
dir.insert_asset_text(Path::new(c_path), c_ron);
dir.insert_asset_text(Path::new(d_path), d_ron);
let (mut app, gate_opener) = test_app(dir);
app.init_asset::<CoolText>()
.register_asset_loader(CoolTextLoader);
let asset_server = app.world.resource::<AssetServer>().clone();
let handle: Handle<CoolText> = asset_server.load(a_path);
let a_id = handle.id();
{
let other_handle: Handle<CoolText> = asset_server.load(a_path);
assert_eq!(
other_handle, handle,
"handles from consecutive load calls should be equal"
);
assert_eq!(
other_handle.id(),
handle.id(),
"handle ids from consecutive load calls should be equal"
);
}
app.world.spawn(handle);
gate_opener.open(a_path);
gate_opener.open(b_path);
gate_opener.open(c_path);
gate_opener.open(d_path);
run_app_until(&mut app, |world| {
let a_text = get::<CoolText>(world, a_id)?;
let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
let b_id = a_text.dependencies[0].id();
let b_text = get::<CoolText>(world, b_id)?;
let (b_load, b_deps, b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
let c_id = a_text.dependencies[1].id();
let c_text = get::<CoolText>(world, c_id)?;
let (c_load, c_deps, c_rec_deps) = asset_server.get_load_states(c_id).unwrap();
let d_id = c_text.dependencies[0].id();
let d_text = get::<CoolText>(world, d_id);
let (d_load, d_deps, d_rec_deps) = asset_server.get_load_states(d_id).unwrap();
if d_load != LoadState::Failed {
return None;
}
assert!(d_text.is_none());
assert_eq!(d_load, LoadState::Failed);
assert_eq!(d_deps, DependencyLoadState::Failed);
assert_eq!(d_rec_deps, RecursiveDependencyLoadState::Failed);
assert_eq!(a_text.text, "a");
assert_eq!(a_load, LoadState::Loaded);
assert_eq!(a_deps, DependencyLoadState::Loaded);
assert_eq!(a_rec_deps, RecursiveDependencyLoadState::Failed);
assert_eq!(b_text.text, "b");
assert_eq!(b_load, LoadState::Loaded);
assert_eq!(b_deps, DependencyLoadState::Loaded);
assert_eq!(b_rec_deps, RecursiveDependencyLoadState::Loaded);
assert_eq!(c_text.text, "c");
assert_eq!(c_load, LoadState::Loaded);
assert_eq!(c_deps, DependencyLoadState::Failed);
assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Failed);
Some(())
});
}
#[test]
fn manual_asset_management() {
#[cfg(not(feature = "multi-threaded"))]
panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded");
let dir = Dir::default();
let dep_path = "dep.cool.ron";
let dep_ron = r#"
(
text: "dep",
dependencies: [],
embedded_dependencies: [],
sub_texts: [],
)"#;
dir.insert_asset_text(Path::new(dep_path), dep_ron);
let (mut app, gate_opener) = test_app(dir);
app.init_asset::<CoolText>()
.init_asset::<SubText>()
.init_resource::<StoredEvents>()
.register_asset_loader(CoolTextLoader)
.add_systems(Update, store_asset_events);
let hello = "hello".to_string();
let empty = "".to_string();
let id = {
let handle = {
let mut texts = app.world.resource_mut::<Assets<CoolText>>();
texts.add(CoolText {
text: hello.clone(),
embedded: empty.clone(),
dependencies: vec![],
sub_texts: Vec::new(),
})
};
app.update();
{
let text = app
.world
.resource::<Assets<CoolText>>()
.get(&handle)
.unwrap();
assert_eq!(text.text, hello);
}
handle.id()
};
app.update();
assert!(
app.world.resource::<Assets<CoolText>>().get(id).is_none(),
"asset has no handles, so it should have been dropped last update"
);
app.update();
let events = std::mem::take(&mut app.world.resource_mut::<StoredEvents>().0);
let expected_events = vec![
AssetEvent::Added { id },
AssetEvent::Unused { id },
AssetEvent::Removed { id },
];
assert_eq!(events, expected_events);
let dep_handle = app.world.resource::<AssetServer>().load(dep_path);
let a = CoolText {
text: "a".to_string(),
embedded: empty,
dependencies: vec![dep_handle.clone()],
sub_texts: Vec::new(),
};
let a_handle = app.world.resource::<AssetServer>().load_asset(a);
app.update();
app.update();
let events = std::mem::take(&mut app.world.resource_mut::<StoredEvents>().0);
let expected_events = vec![AssetEvent::Added { id: a_handle.id() }];
assert_eq!(events, expected_events);
gate_opener.open(dep_path);
loop {
app.update();
let events = std::mem::take(&mut app.world.resource_mut::<StoredEvents>().0);
if events.is_empty() {
continue;
}
let expected_events = vec![
AssetEvent::LoadedWithDependencies {
id: dep_handle.id(),
},
AssetEvent::LoadedWithDependencies { id: a_handle.id() },
];
assert_eq!(events, expected_events);
break;
}
app.update();
let events = std::mem::take(&mut app.world.resource_mut::<StoredEvents>().0);
let expected_events = vec![AssetEvent::Added {
id: dep_handle.id(),
}];
assert_eq!(events, expected_events);
}
#[test]
fn load_folder() {
#[cfg(not(feature = "multi-threaded"))]
panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded");
let dir = Dir::default();
let a_path = "text/a.cool.ron";
let a_ron = r#"
(
text: "a",
dependencies: [
"b.cool.ron",
],
embedded_dependencies: [],
sub_texts: [],
)"#;
let b_path = "b.cool.ron";
let b_ron = r#"
(
text: "b",
dependencies: [],
embedded_dependencies: [],
sub_texts: [],
)"#;
let c_path = "text/c.cool.ron";
let c_ron = r#"
(
text: "c",
dependencies: [
],
embedded_dependencies: [],
sub_texts: [],
)"#;
dir.insert_asset_text(Path::new(a_path), a_ron);
dir.insert_asset_text(Path::new(b_path), b_ron);
dir.insert_asset_text(Path::new(c_path), c_ron);
let (mut app, gate_opener) = test_app(dir);
app.init_asset::<CoolText>()
.init_asset::<SubText>()
.register_asset_loader(CoolTextLoader);
let asset_server = app.world.resource::<AssetServer>().clone();
let handle: Handle<LoadedFolder> = asset_server.load_folder("text");
gate_opener.open(a_path);
gate_opener.open(b_path);
gate_opener.open(c_path);
let mut reader = ManualEventReader::default();
run_app_until(&mut app, |world| {
let events = world.resource::<Events<AssetEvent<LoadedFolder>>>();
let asset_server = world.resource::<AssetServer>();
let loaded_folders = world.resource::<Assets<LoadedFolder>>();
let cool_texts = world.resource::<Assets<CoolText>>();
for event in reader.read(events) {
if let AssetEvent::LoadedWithDependencies { id } = event {
if *id == handle.id() {
let loaded_folder = loaded_folders.get(&handle).unwrap();
let a_handle: Handle<CoolText> =
asset_server.get_handle("text/a.cool.ron").unwrap();
let c_handle: Handle<CoolText> =
asset_server.get_handle("text/c.cool.ron").unwrap();
let mut found_a = false;
let mut found_c = false;
for asset_handle in &loaded_folder.handles {
if asset_handle.id() == a_handle.id().untyped() {
found_a = true;
} else if asset_handle.id() == c_handle.id().untyped() {
found_c = true;
}
}
assert!(found_a);
assert!(found_c);
assert_eq!(loaded_folder.handles.len(), 2);
let a_text = cool_texts.get(&a_handle).unwrap();
let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();
let c_text = cool_texts.get(&c_handle).unwrap();
assert_eq!("a", a_text.text);
assert_eq!("b", b_text.text);
assert_eq!("c", c_text.text);
return Some(());
}
}
}
None
});
}
#[test]
fn load_error_events() {
#[derive(Resource, Default)]
struct ErrorTracker {
tick: u64,
failures: usize,
queued_retries: Vec<(AssetPath<'static>, AssetId<CoolText>, u64)>,
finished_asset: Option<AssetId<CoolText>>,
}
fn asset_event_handler(
mut events: EventReader<AssetEvent<CoolText>>,
mut tracker: ResMut<ErrorTracker>,
) {
for event in events.read() {
if let AssetEvent::LoadedWithDependencies { id } = event {
tracker.finished_asset = Some(*id);
}
}
}
fn asset_load_error_event_handler(
server: Res<AssetServer>,
mut errors: EventReader<AssetLoadFailedEvent<CoolText>>,
mut tracker: ResMut<ErrorTracker>,
) {
tracker.tick += 1;
let now = tracker.tick;
tracker
.queued_retries
.retain(|(path, old_id, retry_after)| {
if now > *retry_after {
let new_handle = server.load::<CoolText>(path);
assert_eq!(&new_handle.id(), old_id);
false
} else {
true
}
});
for error in errors.read() {
let (load_state, _, _) = server.get_load_states(error.id).unwrap();
assert_eq!(load_state, LoadState::Failed);
assert_eq!(*error.path.source(), AssetSourceId::Name("unstable".into()));
match &error.error {
AssetLoadError::AssetReaderError(read_error) => match read_error {
AssetReaderError::Io(_) => {
tracker.failures += 1;
if tracker.failures <= 2 {
tracker.queued_retries.push((
error.path.clone(),
error.id,
now + 10,
));
} else {
panic!(
"Unexpected failure #{} (expected only 2)",
tracker.failures
);
}
}
_ => panic!("Unexpected error type {:?}", read_error),
},
_ => panic!("Unexpected error type {:?}", error.error),
}
}
}
let a_path = "text/a.cool.ron";
let a_ron = r#"
(
text: "a",
dependencies: [],
embedded_dependencies: [],
sub_texts: [],
)"#;
let dir = Dir::default();
dir.insert_asset_text(Path::new(a_path), a_ron);
let unstable_reader = UnstableMemoryAssetReader::new(dir, 2);
let mut app = App::new();
app.register_asset_source(
"unstable",
AssetSource::build().with_reader(move || Box::new(unstable_reader.clone())),
)
.add_plugins((
TaskPoolPlugin::default(),
LogPlugin::default(),
AssetPlugin::default(),
))
.init_asset::<CoolText>()
.register_asset_loader(CoolTextLoader)
.init_resource::<ErrorTracker>()
.add_systems(
Update,
(asset_event_handler, asset_load_error_event_handler).chain(),
);
let asset_server = app.world.resource::<AssetServer>().clone();
let a_path = format!("unstable://{a_path}");
let a_handle: Handle<CoolText> = asset_server.load(a_path);
let a_id = a_handle.id();
app.world.spawn(a_handle);
run_app_until(&mut app, |world| {
let tracker = world.resource::<ErrorTracker>();
match tracker.finished_asset {
Some(asset_id) => {
assert_eq!(asset_id, a_id);
let assets = world.resource::<Assets<CoolText>>();
let result = assets.get(asset_id).unwrap();
assert_eq!(result.text, "a");
Some(())
}
None => None,
}
});
}
#[test]
fn ignore_system_ambiguities_on_assets() {
let mut app = App::new();
app.add_plugins(AssetPlugin::default())
.init_asset::<CoolText>();
fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
app.add_systems(Update, (uses_assets, uses_assets));
app.edit_schedule(Update, |s| {
s.set_build_settings(ScheduleBuildSettings {
ambiguity_detection: LogLevel::Error,
..Default::default()
});
});
app.world.run_schedule(Update);
}
#[derive(Asset, TypePath)]
pub struct TestAsset;
#[allow(dead_code)]
#[derive(Asset, TypePath)]
pub enum EnumTestAsset {
Unnamed(#[dependency] Handle<TestAsset>),
Named {
#[dependency]
handle: Handle<TestAsset>,
#[dependency]
vec_handles: Vec<Handle<TestAsset>>,
#[dependency]
embedded: TestAsset,
},
StructStyle(#[dependency] TestAsset),
Empty,
}
#[derive(Asset, TypePath)]
pub struct StructTestAsset {
#[dependency]
handle: Handle<TestAsset>,
#[dependency]
embedded: TestAsset,
}
#[derive(Asset, TypePath)]
pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);
}