diff --git a/crates/bevy_asset/src/event.rs b/crates/bevy_asset/src/event.rs index a7c1ce422b73a..f049751bb79c3 100644 --- a/crates/bevy_asset/src/event.rs +++ b/crates/bevy_asset/src/event.rs @@ -1,8 +1,47 @@ -use crate::{Asset, AssetId}; +use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId}; use bevy_ecs::event::Event; use std::fmt::Debug; -/// Events that occur for a specific [`Asset`], such as "value changed" events and "dependency" events. +/// An event emitted when a specific [`Asset`] fails to load. +/// +/// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`]. +#[derive(Event, Clone, Debug)] +pub struct AssetLoadFailedEvent { + pub id: AssetId, + /// The asset path that was attempted. + pub path: AssetPath<'static>, + /// Why the asset failed to load. + pub error: AssetLoadError, +} + +impl AssetLoadFailedEvent { + /// Converts this to an "untyped" / "generic-less" asset error event that stores the type information. + pub fn untyped(&self) -> UntypedAssetLoadFailedEvent { + self.into() + } +} + +/// An untyped version of [`AssetLoadFailedEvent`]. +#[derive(Event, Clone, Debug)] +pub struct UntypedAssetLoadFailedEvent { + pub id: UntypedAssetId, + /// The asset path that was attempted. + pub path: AssetPath<'static>, + /// Why the asset failed to load. + pub error: AssetLoadError, +} + +impl From<&AssetLoadFailedEvent> for UntypedAssetLoadFailedEvent { + fn from(value: &AssetLoadFailedEvent) -> Self { + UntypedAssetLoadFailedEvent { + id: value.id.untyped(), + path: value.path.clone(), + error: value.error.clone(), + } + } +} + +/// Events that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. #[derive(Event)] pub enum AssetEvent { /// Emitted whenever an [`Asset`] is added. diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 742dd40b8b071..c03afdabf100d 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -27,20 +27,32 @@ use futures_lite::{ready, Stream}; use std::{ path::{Path, PathBuf}, pin::Pin, + sync::Arc, task::Poll, }; use thiserror::Error; /// Errors that occur while loading assets. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum AssetReaderError { /// Path not found. - #[error("path not found: {0}")] + #[error("Path not found: {0}")] NotFound(PathBuf), /// Encountered an I/O error while loading an asset. - #[error("encountered an io error while loading asset: {0}")] - Io(#[from] std::io::Error), + #[error("Encountered an I/O error while loading asset: {0}")] + Io(Arc), + + /// The HTTP request completed but returned an unhandled [HTTP response status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status). + /// If the request fails before getting a status code (e.g. request timeout, interrupted connection, etc), expect [`AssetReaderError::Io`]. + #[error("Encountered HTTP status {0:?} when loading asset")] + HttpError(u16), +} + +impl From for AssetReaderError { + fn from(value: std::io::Error) -> Self { + Self::Io(Arc::new(value)) + } } pub type Reader<'a> = dyn AsyncRead + Unpin + Send + Sync + 'a; diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index b16dafb5bfc6c..a9f8adb8c17f3 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -569,22 +569,22 @@ impl AssetSources { } /// An error returned when an [`AssetSource`] does not exist for a given id. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Asset Source '{0}' does not exist")] pub struct MissingAssetSourceError(AssetSourceId<'static>); /// An error returned when an [`AssetWriter`] does not exist for a given id. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Asset Source '{0}' does not have an AssetWriter.")] pub struct MissingAssetWriterError(AssetSourceId<'static>); /// An error returned when a processed [`AssetReader`] does not exist for a given id. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Asset Source '{0}' does not have a processed AssetReader.")] pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>); /// An error returned when a processed [`AssetWriter`] does not exist for a given id. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Asset Source '{0}' does not have a processed AssetWriter.")] pub struct MissingProcessedAssetWriterError(AssetSourceId<'static>); diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index 2636419299f81..7ee60ecfdb8c9 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -53,10 +53,7 @@ impl HttpWasmAssetReader { Ok(reader) } 404 => Err(AssetReaderError::NotFound(path)), - status => Err(AssetReaderError::Io(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Encountered unexpected HTTP status {status}"), - ))), + status => Err(AssetReaderError::HttpError(status as u16)), } } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index f85102f0c88ca..aadccccf54fed 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -213,6 +213,7 @@ impl Plugin for AssetPlugin { .init_asset::() .init_asset::() .init_asset::<()>() + .add_event::() .configure_sets( UpdateAssets, TrackAssets.after(handle_internal_asset_events), @@ -377,6 +378,7 @@ impl AssetApp for App { self.insert_resource(assets) .allow_ambiguous_resource::>() .add_event::>() + .add_event::>() .register_type::>() .register_type::>() .add_systems(AssetEvents, Assets::::asset_events) @@ -431,11 +433,12 @@ mod tests { io::{ gated::{GateOpener, GatedReader}, memory::{Dir, MemoryAssetReader}, - AssetSource, AssetSourceId, Reader, + AssetReader, AssetReaderError, AssetSource, AssetSourceId, Reader, }, loader::{AssetLoader, LoadContext}, - Asset, AssetApp, AssetEvent, AssetId, AssetPath, AssetPlugin, AssetServer, Assets, - DependencyLoadState, LoadState, RecursiveDependencyLoadState, + Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath, + AssetPlugin, AssetServer, Assets, DependencyLoadState, LoadState, + RecursiveDependencyLoadState, }; use bevy_app::{App, Update}; use bevy_core::TaskPoolPlugin; @@ -446,20 +449,23 @@ mod tests { }; use bevy_log::LogPlugin; use bevy_reflect::TypePath; - use bevy_utils::BoxedFuture; + use bevy_utils::{BoxedFuture, Duration, HashMap}; use futures_lite::AsyncReadExt; use serde::{Deserialize, Serialize}; - use std::path::Path; + use std::{ + path::{Path, PathBuf}, + sync::Arc, + }; use thiserror::Error; #[derive(Asset, TypePath, Debug)] pub struct CoolText { - text: String, - embedded: String, + pub text: String, + pub embedded: String, #[dependency] - dependencies: Vec>, + pub dependencies: Vec>, #[dependency] - sub_texts: Vec>, + pub sub_texts: Vec>, } #[derive(Asset, TypePath, Debug)] @@ -476,10 +482,10 @@ mod tests { } #[derive(Default)] - struct CoolTextLoader; + pub struct CoolTextLoader; #[derive(Error, Debug)] - enum CoolTextLoaderError { + pub enum CoolTextLoaderError { #[error("Could not load dependency: {dependency}")] CannotLoadDependency { dependency: AssetPath<'static> }, #[error("A RON error occurred during loading")] @@ -537,6 +543,83 @@ mod tests { } } + /// A dummy [`CoolText`] asset reader that only succeeds after `failure_count` times it's read from for each asset. + #[derive(Default, Clone)] + pub struct UnstableMemoryAssetReader { + pub attempt_counters: Arc>>, + 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> { + self.memory_reader.is_directory(path) + } + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetReaderError>> { + self.memory_reader.read_directory(path) + } + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + self.memory_reader.read_meta(path) + } + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture< + 'a, + Result>, 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 }); @@ -552,7 +635,7 @@ mod tests { (app, gate_opener) } - fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) { + 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() { @@ -581,6 +664,10 @@ mod tests { #[test] fn load_dependencies() { + // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded + #[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"; @@ -898,6 +985,10 @@ mod tests { #[test] fn failure_load_states() { + // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded + #[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"; @@ -1017,6 +1108,10 @@ mod tests { #[test] fn manual_asset_management() { + // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded + #[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"; @@ -1122,6 +1217,10 @@ mod tests { #[test] fn load_folder() { + // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded + #[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"; @@ -1210,6 +1309,133 @@ mod tests { }); } + /// Tests that `AssetLoadFailedEvent` events are emitted and can be used to retry failed assets. + #[test] + fn load_error_events() { + #[derive(Resource, Default)] + struct ErrorTracker { + tick: u64, + failures: usize, + queued_retries: Vec<(AssetPath<'static>, AssetId, u64)>, + finished_asset: Option>, + } + + fn asset_event_handler( + mut events: EventReader>, + mut tracker: ResMut, + ) { + for event in events.read() { + if let AssetEvent::LoadedWithDependencies { id } = event { + tracker.finished_asset = Some(*id); + } + } + } + + fn asset_load_error_event_handler( + server: Res, + mut errors: EventReader>, + mut tracker: ResMut, + ) { + // In the real world, this would refer to time (not ticks) + tracker.tick += 1; + + // Retry loading past failed items + let now = tracker.tick; + tracker + .queued_retries + .retain(|(path, old_id, retry_after)| { + if now > *retry_after { + let new_handle = server.load::(path); + assert_eq!(&new_handle.id(), old_id); + false + } else { + true + } + }); + + // Check what just failed + 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 { + // Retry in 10 ticks + 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::() + .register_asset_loader(CoolTextLoader) + .init_resource::() + .add_systems( + Update, + (asset_event_handler, asset_load_error_event_handler).chain(), + ); + + let asset_server = app.world.resource::().clone(); + let a_path = format!("unstable://{a_path}"); + let a_handle: Handle = 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::(); + match tracker.finished_asset { + Some(asset_id) => { + assert_eq!(asset_id, a_id); + let assets = world.resource::>(); + 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(); diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 895d6b123a2a9..da3468201e7a0 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -254,7 +254,7 @@ pub struct LoadDirectError { } /// An error that occurs while deserializing [`AssetMeta`]. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum DeserializeMetaError { #[error("Failed to deserialize asset meta: {0:?}")] DeserializeSettings(#[from] SpannedError), diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 4ec650e935632..b5ef275103ac3 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -293,6 +293,13 @@ impl AssetProcessor { AssetPath::from_path(&path).with_source(source.id()) ); } + AssetReaderError::HttpError(status) => { + error!( + "Path '{}' was removed, but the destination reader could not determine if it \ + was a folder or a file due to receiving an unexpected HTTP Status {status}", + AssetPath::from_path(&path).with_source(source.id()) + ); + } } } } @@ -344,6 +351,13 @@ impl AssetProcessor { AssetReaderError::NotFound(_err) => { // The processed folder does not exist. No need to update anything } + AssetReaderError::HttpError(status) => { + self.log_unrecoverable().await; + error!( + "Unrecoverable Error: Failed to read the processed assets at {path:?} in order to remove assets that no longer exist \ + in the source directory. Restart the asset processor to fully reprocess assets. HTTP Status Code {status}" + ); + } AssetReaderError::Io(err) => { self.log_unrecoverable().await; error!( @@ -752,7 +766,7 @@ impl AssetProcessor { .await .map_err(|e| ProcessError::AssetReaderError { path: asset_path.clone(), - err: AssetReaderError::Io(e), + err: AssetReaderError::Io(e.into()), })?; // PERF: in theory these hashes could be streamed if we want to avoid allocating the whole asset. diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index 41bbdf27bf3ec..2165c57b9cb13 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -1,8 +1,8 @@ use crate::{ meta::{AssetHash, MetaTransform}, - Asset, AssetHandleProvider, AssetPath, DependencyLoadState, ErasedLoadedAsset, Handle, - InternalAssetEvent, LoadState, RecursiveDependencyLoadState, StrongHandle, UntypedAssetId, - UntypedHandle, + Asset, AssetHandleProvider, AssetLoadError, AssetPath, DependencyLoadState, ErasedLoadedAsset, + Handle, InternalAssetEvent, LoadState, RecursiveDependencyLoadState, StrongHandle, + UntypedAssetId, UntypedHandle, }; use bevy_ecs::world::World; use bevy_log::warn; @@ -74,6 +74,8 @@ pub(crate) struct AssetInfos { pub(crate) living_labeled_assets: HashMap, HashSet>, pub(crate) handle_providers: HashMap, pub(crate) dependency_loaded_event_sender: HashMap, + pub(crate) dependency_failed_event_sender: + HashMap, AssetLoadError)>, } impl std::fmt::Debug for AssetInfos { @@ -197,7 +199,8 @@ impl AssetInfos { let mut should_load = false; if loading_mode == HandleLoadingMode::Force || (loading_mode == HandleLoadingMode::Request - && info.load_state == LoadState::NotLoaded) + && (info.load_state == LoadState::NotLoaded + || info.load_state == LoadState::Failed)) { info.load_state = LoadState::Loading; info.dep_load_state = DependencyLoadState::Loading; @@ -268,8 +271,12 @@ impl AssetInfos { self.infos.get_mut(&id) } - pub(crate) fn get_path_handle(&self, path: AssetPath) -> Option { - let id = *self.path_to_id.get(&path)?; + pub(crate) fn get_path_id(&self, path: &AssetPath) -> Option { + self.path_to_id.get(path).copied() + } + + pub(crate) fn get_path_handle(&self, path: &AssetPath) -> Option { + let id = *self.path_to_id.get(path)?; self.get_id_handle(id) } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index ffa192f0cd833..51cb399d10182 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -12,8 +12,9 @@ use crate::{ MetaTransform, Settings, }, path::AssetPath, - Asset, AssetEvent, AssetHandleProvider, AssetId, AssetMetaCheck, Assets, DeserializeMetaError, - ErasedLoadedAsset, Handle, LoadedUntypedAsset, UntypedAssetId, UntypedHandle, + Asset, AssetEvent, AssetHandleProvider, AssetId, AssetLoadFailedEvent, AssetMetaCheck, Assets, + DeserializeMetaError, ErasedLoadedAsset, Handle, LoadedUntypedAsset, UntypedAssetId, + UntypedAssetLoadFailedEvent, UntypedHandle, }; use bevy_ecs::prelude::*; use bevy_log::{error, info, warn}; @@ -119,7 +120,7 @@ impl AssetServer { } } - /// Retrieves the [`AssetReader`] for the given `source`. + /// Retrieves the [`AssetSource`] for the given `source`. pub fn get_source<'a>( &'a self, source: impl Into>, @@ -173,11 +174,30 @@ impl AssetServer { .resource_mut::>>() .send(AssetEvent::LoadedWithDependencies { id: id.typed() }); } - self.data - .infos - .write() + fn failed_sender( + world: &mut World, + id: UntypedAssetId, + path: AssetPath<'static>, + error: AssetLoadError, + ) { + world + .resource_mut::>>() + .send(AssetLoadFailedEvent { + id: id.typed(), + path, + error, + }); + } + + let mut infos = self.data.infos.write(); + + infos .dependency_loaded_event_sender .insert(TypeId::of::(), sender::); + + infos + .dependency_failed_event_sender + .insert(TypeId::of::(), failed_sender::); } pub(crate) fn register_handle_provider(&self, handle_provider: AssetHandleProvider) { @@ -366,6 +386,7 @@ impl AssetServer { let server = self.clone(); IoTaskPool::get() .spawn(async move { + let path_clone = path.clone(); match server.load_untyped_async(path).await { Ok(handle) => server.send_asset_event(InternalAssetEvent::Loaded { id, @@ -377,7 +398,11 @@ impl AssetServer { }), Err(err) => { error!("{err}"); - server.send_asset_event(InternalAssetEvent::Failed { id }); + server.send_asset_event(InternalAssetEvent::Failed { + id, + path: path_clone, + error: err, + }); } } }) @@ -406,7 +431,11 @@ impl AssetServer { // if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if // we cannot find the meta and loader if let Some(handle) = &input_handle { - self.send_asset_event(InternalAssetEvent::Failed { id: handle.id() }); + self.send_asset_event(InternalAssetEvent::Failed { + id: handle.id(), + path: path.clone_owned(), + error: e.clone(), + }); } e })?; @@ -511,6 +540,8 @@ impl AssetServer { Err(err) => { self.send_asset_event(InternalAssetEvent::Failed { id: base_handle.id(), + error: err.clone(), + path: path.into_owned(), }); Err(err) } @@ -534,7 +565,6 @@ impl AssetServer { IoTaskPool::get() .spawn(async move { if server.data.infos.read().should_reload(&path) { - info!("Reloading {path} because it has changed"); if let Err(err) = server.load_internal(None, path, true, None).await { error!("{}", err); } @@ -690,7 +720,7 @@ impl AssetServer { }), Err(err) => { error!("Failed to load folder. {err}"); - server.send_asset_event(InternalAssetEvent::Failed { id }); + server.send_asset_event(InternalAssetEvent::Failed { id, error: err, path }); }, } }) @@ -775,12 +805,20 @@ impl AssetServer { self.data.infos.read().contains_key(id.into()) } + /// Returns an active untyped asset id for the given path, if the asset at the given path has already started loading, + /// or is still "alive". + pub fn get_path_id<'a>(&self, path: impl Into>) -> Option { + let infos = self.data.infos.read(); + let path = path.into(); + infos.get_path_id(&path) + } + /// Returns an active untyped handle for the given path, if the asset at the given path has already started loading, /// or is still "alive". pub fn get_handle_untyped<'a>(&self, path: impl Into>) -> Option { let infos = self.data.infos.read(); let path = path.into(); - infos.get_path_handle(path) + infos.get_path_handle(&path) } /// Returns the path for the given `id`, if it has one. @@ -874,7 +912,7 @@ impl AssetServer { ron::de::from_bytes(&meta_bytes).map_err(|e| { AssetLoadError::DeserializeMeta { path: asset_path.clone_owned(), - error: Box::new(DeserializeMetaError::DeserializeMinimal(e)), + error: DeserializeMetaError::DeserializeMinimal(e).into(), } })?; let loader_name = match minimal.asset { @@ -894,7 +932,7 @@ impl AssetServer { let meta = loader.deserialize_meta(&meta_bytes).map_err(|e| { AssetLoadError::DeserializeMeta { path: asset_path.clone_owned(), - error: Box::new(e), + error: e.into(), } })?; @@ -931,7 +969,7 @@ impl AssetServer { AssetLoadError::AssetLoaderError { path: asset_path.clone_owned(), loader_name: loader.type_name(), - error: e, + error: e.into(), } }) } @@ -941,6 +979,7 @@ impl AssetServer { pub fn handle_internal_asset_events(world: &mut World) { world.resource_scope(|world, server: Mut| { let mut infos = server.data.infos.write(); + let mut untyped_failures = vec![]; for event in server.data.asset_event_receiver.try_iter() { match event { InternalAssetEvent::Loaded { id, loaded_asset } => { @@ -958,10 +997,30 @@ pub fn handle_internal_asset_events(world: &mut World) { .expect("Asset event sender should exist"); sender(world, id); } - InternalAssetEvent::Failed { id } => infos.process_asset_fail(id), + InternalAssetEvent::Failed { id, path, error } => { + infos.process_asset_fail(id); + + // Send untyped failure event + untyped_failures.push(UntypedAssetLoadFailedEvent { + id, + path: path.clone(), + error: error.clone(), + }); + + // Send typed failure event + let sender = infos + .dependency_failed_event_sender + .get(&id.type_id()) + .expect("Asset failed event sender should exist"); + sender(world, id, path, error); + } } } + if !untyped_failures.is_empty() { + world.send_event_batch(untyped_failures); + } + fn queue_ancestors( asset_path: &AssetPath, infos: &AssetInfos, @@ -981,7 +1040,7 @@ pub fn handle_internal_asset_events(world: &mut World) { current_folder = parent.to_path_buf(); let parent_asset_path = AssetPath::from(current_folder.clone()).with_source(source.clone()); - if let Some(folder_handle) = infos.get_path_handle(parent_asset_path.clone()) { + if let Some(folder_handle) = infos.get_path_handle(&parent_asset_path) { info!("Reloading folder {parent_asset_path} because the content has changed"); server.load_folder_internal(folder_handle.id(), parent_asset_path); } @@ -1032,6 +1091,7 @@ pub fn handle_internal_asset_events(world: &mut World) { } for path in paths_to_reload { + info!("Reloading {path} because it has changed"); server.reload(path); } }); @@ -1066,6 +1126,8 @@ pub(crate) enum InternalAssetEvent { }, Failed { id: UntypedAssetId, + path: AssetPath<'static>, + error: AssetLoadError, }, } @@ -1109,7 +1171,7 @@ pub enum RecursiveDependencyLoadState { } /// An error that occurs during an [`Asset`] load. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum AssetLoadError { #[error("Requested handle of type {requested:?} for asset '{path}' does not match actual asset type '{actual_asset_name}', which used loader '{loader_name}'")] RequestedHandleTypeMismatch { @@ -1143,7 +1205,7 @@ pub enum AssetLoadError { AssetLoaderError { path: AssetPath<'static>, loader_name: &'static str, - error: Box, + error: Arc, }, #[error("The file at '{}' does not contain the labeled asset '{}'; it contains the following {} assets: {}", base_path, @@ -1158,14 +1220,14 @@ pub enum AssetLoadError { } /// An error that occurs when an [`AssetLoader`] is not registered for a given extension. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("no `AssetLoader` found{}", format_missing_asset_ext(.extensions))] pub struct MissingAssetLoaderForExtensionError { extensions: Vec, } /// An error that occurs when an [`AssetLoader`] is not registered for a given [`std::any::type_name`]. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("no `AssetLoader` found with the name '{type_name}'")] pub struct MissingAssetLoaderForTypeNameError { type_name: String,