diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index e101e11209a2b..5c06783b50bc3 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -19,7 +19,7 @@ use bevy_ecs::{ system::{Res, ResMut}, }; use bevy_platform::collections::HashMap; -use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use derive_more::derive::From; use petgraph::{ graph::{DiGraph, NodeIndex}, @@ -238,7 +238,7 @@ pub enum AnimationNodeType { /// /// The canonical extension for [`AnimationGraph`]s is `.animgraph.ron`. Plain /// `.animgraph` is supported as well. -#[derive(Default)] +#[derive(Default, TypePath)] pub struct AnimationGraphAssetLoader; /// Errors that can occur when serializing animation graphs to RON. diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 03da015711faa..f5be6691045e3 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -778,7 +778,7 @@ mod tests { pub sub_texts: Vec, } - #[derive(Default)] + #[derive(Default, TypePath)] pub struct CoolTextLoader; #[derive(Error, Debug)] @@ -1932,6 +1932,7 @@ mod tests { .init_asset::() .register_asset_loader(CoolTextLoader); + #[derive(TypePath)] struct NestedLoadOfSubassetLoader; impl AssetLoader for NestedLoadOfSubassetLoader { @@ -2152,6 +2153,7 @@ mod tests { // Note: we can't just use the GatedReader, since currently we hold the handle until after // we've selected the reader. The GatedReader blocks this process, so we need to wait until // we gate in the loader instead. + #[derive(TypePath)] struct GatedLoader { in_loader_sender: async_channel::Sender<()>, gate_receiver: async_channel::Receiver<()>, @@ -2469,6 +2471,7 @@ mod tests { #[derive(Serialize, Deserialize, Default)] struct U8LoaderSettings(u8); + #[derive(TypePath)] struct U8Loader; impl AssetLoader for U8Loader { @@ -2566,6 +2569,7 @@ mod tests { .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) .init_asset::(); + #[derive(TypePath)] struct TwoSubassetLoader; impl AssetLoader for TwoSubassetLoader { @@ -2618,6 +2622,7 @@ mod tests { .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) .init_asset::(); + #[derive(TypePath)] struct TrivialLoader; impl AssetLoader for TrivialLoader { diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 90ee558f05dd6..9c1a23efb5387 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -14,6 +14,7 @@ use alloc::{ use atomicow::CowArc; use bevy_ecs::{error::BevyError, world::World}; use bevy_platform::collections::{HashMap, HashSet}; +use bevy_reflect::TypePath; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::any::{Any, TypeId}; use downcast_rs::{impl_downcast, Downcast}; @@ -28,7 +29,7 @@ use thiserror::Error; /// This trait is generally used in concert with [`AssetReader`](crate::io::AssetReader) to load assets from a byte source. /// /// For a complementary version of this trait that can save assets, see [`AssetSaver`](crate::saver::AssetSaver). -pub trait AssetLoader: Send + Sync + 'static { +pub trait AssetLoader: TypePath + Send + Sync + 'static { /// The top level [`Asset`] loaded by this [`AssetLoader`]. type Asset: Asset; /// The settings type used by this [`AssetLoader`]. @@ -66,8 +67,8 @@ pub trait ErasedAssetLoader: Send + Sync + 'static { fn deserialize_meta(&self, meta: &[u8]) -> Result, DeserializeMetaError>; /// Returns the default meta value for the [`AssetLoader`] (erased as [`Box`]). fn default_meta(&self) -> Box; - /// Returns the type name of the [`AssetLoader`]. - fn type_name(&self) -> &'static str; + /// Returns the type path of the [`AssetLoader`]. + fn type_path(&self) -> &'static str; /// Returns the [`TypeId`] of the [`AssetLoader`]. fn type_id(&self) -> TypeId; /// Returns the type name of the top-level [`Asset`] loaded by the [`AssetLoader`]. @@ -111,13 +112,13 @@ where fn default_meta(&self) -> Box { Box::new(AssetMeta::::new(crate::meta::AssetAction::Load { - loader: self.type_name().to_string(), + loader: self.type_path().to_string(), settings: L::Settings::default(), })) } - fn type_name(&self) -> &'static str { - core::any::type_name::() + fn type_path(&self) -> &'static str { + L::type_path() } fn type_id(&self) -> TypeId { diff --git a/crates/bevy_asset/src/loader_builders.rs b/crates/bevy_asset/src/loader_builders.rs index 994eb33590bee..f842d26a50f81 100644 --- a/crates/bevy_asset/src/loader_builders.rs +++ b/crates/bevy_asset/src/loader_builders.rs @@ -481,7 +481,7 @@ impl NestedLoader<'_, '_, StaticTyped, Immediate<'_, '_>> { path, requested: TypeId::of::(), actual_asset_name: loader.asset_type_name(), - loader_name: loader.type_name(), + loader_name: loader.type_path(), }, }) }) diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index dcc74bd2d74f0..3dca3ee453c88 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -40,6 +40,8 @@ mod log; mod process; +#[cfg(feature = "trace")] +use bevy_reflect::TypePath; pub use log::*; pub use process::*; @@ -56,10 +58,12 @@ use crate::{ AssetLoadError, AssetMetaCheck, AssetPath, AssetServer, AssetServerMode, DeserializeMetaError, MissingAssetLoaderForExtensionError, UnapprovedPathMode, WriteDefaultMetaError, }; -use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, sync::Arc, vec, vec::Vec}; +use alloc::{ + borrow::ToOwned, boxed::Box, collections::VecDeque, string::String, sync::Arc, vec, vec::Vec, +}; use bevy_ecs::prelude::*; use bevy_platform::{ - collections::{HashMap, HashSet}, + collections::{hash_map::Entry, HashMap, HashSet}, sync::{PoisonError, RwLock}, }; use bevy_tasks::IoTaskPool; @@ -110,9 +114,8 @@ pub struct AssetProcessorData { /// avoids needing to use [`block_on`](bevy_tasks::block_on) to set the factory). log_factory: Mutex>>, log: async_lock::RwLock>>, - processors: RwLock>>, - /// Default processors for file extensions - default_processors: RwLock, &'static str>>, + /// The processors that will be used to process assets. + processors: RwLock, state: async_lock::RwLock, sources: AssetSources, initialized_sender: async_broadcast::Sender<()>, @@ -121,6 +124,31 @@ pub struct AssetProcessorData { finished_receiver: async_broadcast::Receiver<()>, } +#[derive(Default)] +struct Processors { + /// Maps the type path of the processor to its instance. + type_path_to_processor: HashMap<&'static str, Arc>, + /// Maps the short type path of the processor to its instance. + short_type_path_to_processor: HashMap<&'static str, ShortTypeProcessorEntry>, + /// Maps the file extension of an asset to the type path of the processor we should use to + /// process it by default. + file_extension_to_default_processor: HashMap, &'static str>, +} + +enum ShortTypeProcessorEntry { + /// There is a unique processor with the given short type path. + Unique { + /// The full type path of the processor. + type_path: &'static str, + /// The processor itself. + processor: Arc, + }, + /// There are (at least) two processors with the same short type path (storing the full type + /// paths of all conflicting processors). Users must fully specify the type path in order to + /// disambiguate. + Ambiguous(Vec<&'static str>), +} + impl AssetProcessor { /// Creates a new [`AssetProcessor`] instance. pub fn new(source: &mut AssetSourceBuilders) -> Self { @@ -610,50 +638,92 @@ impl AssetProcessor { /// Register a new asset processor. pub fn register_processor(&self, processor: P) { - let mut process_plans = self + let mut processors = self .data .processors .write() .unwrap_or_else(PoisonError::into_inner); #[cfg(feature = "trace")] let processor = InstrumentedAssetProcessor(processor); - process_plans.insert(core::any::type_name::

(), Arc::new(processor)); + let processor = Arc::new(processor); + processors + .type_path_to_processor + .insert(P::type_path(), processor.clone()); + match processors + .short_type_path_to_processor + .entry(P::short_type_path()) + { + Entry::Vacant(entry) => { + entry.insert(ShortTypeProcessorEntry::Unique { + type_path: P::type_path(), + processor, + }); + } + Entry::Occupied(mut entry) => match entry.get_mut() { + ShortTypeProcessorEntry::Unique { type_path, .. } => { + let type_path = *type_path; + *entry.get_mut() = + ShortTypeProcessorEntry::Ambiguous(vec![type_path, P::type_path()]); + } + ShortTypeProcessorEntry::Ambiguous(type_paths) => { + type_paths.push(P::type_path()); + } + }, + } } /// Set the default processor for the given `extension`. Make sure `P` is registered with [`AssetProcessor::register_processor`]. pub fn set_default_processor(&self, extension: &str) { - let mut default_processors = self + let mut processors = self .data - .default_processors + .processors .write() .unwrap_or_else(PoisonError::into_inner); - default_processors.insert(extension.into(), core::any::type_name::

()); + processors + .file_extension_to_default_processor + .insert(extension.into(), P::type_path()); } /// Returns the default processor for the given `extension`, if it exists. pub fn get_default_processor(&self, extension: &str) -> Option> { - let default_processors = self + let processors = self .data - .default_processors - .read() - .unwrap_or_else(PoisonError::into_inner); - let key = default_processors.get(extension)?; - self.data .processors .read() - .unwrap_or_else(PoisonError::into_inner) - .get(key) - .cloned() + .unwrap_or_else(PoisonError::into_inner); + let key = processors + .file_extension_to_default_processor + .get(extension)?; + processors.type_path_to_processor.get(key).cloned() } /// Returns the processor with the given `processor_type_name`, if it exists. - pub fn get_processor(&self, processor_type_name: &str) -> Option> { + pub fn get_processor( + &self, + processor_type_name: &str, + ) -> Result, GetProcessorError> { let processors = self .data .processors .read() .unwrap_or_else(PoisonError::into_inner); - processors.get(processor_type_name).cloned() + if let Some(short_type_processor) = processors + .short_type_path_to_processor + .get(processor_type_name) + { + return match short_type_processor { + ShortTypeProcessorEntry::Unique { processor, .. } => Ok(processor.clone()), + ShortTypeProcessorEntry::Ambiguous(examples) => Err(GetProcessorError::Ambiguous { + processor_short_name: processor_type_name.to_owned(), + ambiguous_processor_names: examples.clone(), + }), + }; + } + processors + .type_path_to_processor + .get(processor_type_name) + .cloned() + .ok_or_else(|| GetProcessorError::Missing(processor_type_name.to_owned())) } /// Populates the initial view of each asset by scanning the unprocessed and processed asset folders. @@ -877,9 +947,7 @@ impl AssetProcessor { (meta, None) } AssetActionMinimal::Process { processor } => { - let processor = self - .get_processor(&processor) - .ok_or_else(|| ProcessError::MissingProcessor(processor))?; + let processor = self.get_processor(&processor)?; let meta = processor.deserialize_meta(&meta_bytes)?; (meta, Some(processor)) } @@ -1138,7 +1206,6 @@ impl AssetProcessorData { log: Default::default(), processors: Default::default(), asset_infos: Default::default(), - default_processors: Default::default(), } } @@ -1220,6 +1287,7 @@ impl AssetProcessorData { } #[cfg(feature = "trace")] +#[derive(TypePath)] struct InstrumentedAssetProcessor(T); #[cfg(feature = "trace")] @@ -1243,7 +1311,7 @@ impl Process for InstrumentedAssetProcessor { }; let span = info_span!( "asset processing", - processor = core::any::type_name::(), + processor = T::type_path(), asset = context.path().to_string(), ); self.0.process(context, meta, writer).instrument(span) @@ -1564,6 +1632,33 @@ pub enum SetTransactionLogFactoryError { AlreadyInUse, } +/// An error when retrieving an asset processor. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum GetProcessorError { + #[error("The processor '{0}' does not exist")] + Missing(String), + #[error("The processor '{processor_short_name}' is ambiguous between several processors: {ambiguous_processor_names:?}")] + Ambiguous { + processor_short_name: String, + ambiguous_processor_names: Vec<&'static str>, + }, +} + +impl From for ProcessError { + fn from(err: GetProcessorError) -> Self { + match err { + GetProcessorError::Missing(name) => Self::MissingProcessor(name), + GetProcessorError::Ambiguous { + processor_short_name, + ambiguous_processor_names, + } => Self::AmbiguousProcessor { + processor_short_name, + ambiguous_processor_names, + }, + } + } +} + // The asset processor currently requires multi_threaded. #[cfg(feature = "multi_threaded")] #[cfg(test)] diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index b37265d0fb660..905904566c84c 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -14,7 +14,9 @@ use alloc::{ borrow::ToOwned, boxed::Box, string::{String, ToString}, + vec::Vec, }; +use bevy_reflect::TypePath; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::marker::PhantomData; use serde::{Deserialize, Serialize}; @@ -25,7 +27,7 @@ use thiserror::Error; /// /// This is a "low level", maximally flexible interface. Most use cases are better served by the [`LoadTransformAndSave`] implementation /// of [`Process`]. -pub trait Process: Send + Sync + Sized + 'static { +pub trait Process: TypePath + Send + Sync + Sized + 'static { /// The configuration / settings used to process the asset. This will be stored in the [`AssetMeta`] and is user-configurable per-asset. type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>; /// The [`AssetLoader`] that will be used to load the final processed asset. @@ -59,6 +61,7 @@ pub trait Process: Send + Sync + Sized + 'static { /// This uses [`LoadTransformAndSaveSettings`] to configure the processor. /// /// [`Asset`]: crate::Asset +#[derive(TypePath)] pub struct LoadTransformAndSave< L: AssetLoader, T: AssetTransformer, @@ -120,6 +123,11 @@ pub enum ProcessError { #[error("The processor '{0}' does not exist")] #[from(ignore)] MissingProcessor(String), + #[error("The processor '{processor_short_name}' is ambiguous between several processors: {ambiguous_processor_names:?}")] + AmbiguousProcessor { + processor_short_name: String, + ambiguous_processor_names: Vec<&'static str>, + }, #[error("Encountered an AssetReader error for '{path}': {err}")] #[from(ignore)] AssetReaderError { @@ -180,7 +188,7 @@ where return Err(ProcessError::WrongMetaType); }; let loader_meta = AssetMeta::::new(AssetAction::Load { - loader: core::any::type_name::().to_string(), + loader: Loader::type_path().to_string(), settings: settings.loader_settings, }); let pre_transformed_asset = TransformedAsset::::from_loaded( @@ -237,7 +245,7 @@ impl ErasedProcessor for P { let loader_settings =

::process(self, context, *meta, writer).await?; let output_meta: Box = Box::new(AssetMeta::::new(AssetAction::Load { - loader: core::any::type_name::().to_string(), + loader: P::OutputLoader::type_path().to_string(), settings: loader_settings, })); Ok(output_meta) @@ -251,7 +259,7 @@ impl ErasedProcessor for P { fn default_meta(&self) -> Box { Box::new(AssetMeta::<(), P>::new(AssetAction::Process { - processor: core::any::type_name::

().to_string(), + processor: P::type_path().to_string(), settings: P::Settings::default(), })) } @@ -307,7 +315,7 @@ impl<'a> ProcessContext<'a> { meta: AssetMeta, ) -> Result { let server = &self.processor.server; - let loader_name = core::any::type_name::(); + let loader_name = L::type_path(); let loader = server.get_asset_loader_with_type_name(loader_name).await?; let mut reader = SliceReader::new(self.asset_bytes); let loaded_asset = server diff --git a/crates/bevy_asset/src/processor/tests.rs b/crates/bevy_asset/src/processor/tests.rs index 25208f759fe4c..a0c3f42f8ba71 100644 --- a/crates/bevy_asset/src/processor/tests.rs +++ b/crates/bevy_asset/src/processor/tests.rs @@ -2,6 +2,7 @@ use alloc::{ boxed::Box, collections::BTreeMap, string::{String, ToString}, + sync::Arc, vec, vec::Vec, }; @@ -18,11 +19,12 @@ use bevy_tasks::BoxedFuture; use crate::{ io::{ memory::{Dir, MemoryAssetReader, MemoryAssetWriter}, - AssetSource, AssetSourceId, Reader, + AssetSource, AssetSourceBuilders, AssetSourceId, Reader, }, + meta::AssetMeta, processor::{ - AssetProcessor, LoadTransformAndSave, LogEntry, ProcessorTransactionLog, - ProcessorTransactionLogFactory, + AssetProcessor, GetProcessorError, LoadTransformAndSave, LogEntry, Process, ProcessContext, + ProcessError, ProcessorTransactionLog, ProcessorTransactionLogFactory, }, saver::AssetSaver, tests::{CoolText, CoolTextLoader, CoolTextRon, SubText}, @@ -30,6 +32,132 @@ use crate::{ Asset, AssetApp, AssetLoader, AssetMode, AssetPath, AssetPlugin, LoadContext, }; +#[derive(TypePath)] +struct MyProcessor(PhantomData T>); + +impl Process for MyProcessor { + type OutputLoader = (); + type Settings = (); + + async fn process( + &self, + _context: &mut ProcessContext<'_>, + _meta: AssetMeta<(), Self>, + _writer: &mut crate::io::Writer, + ) -> Result<(), ProcessError> { + Ok(()) + } +} + +#[derive(TypePath)] +struct Marker; + +fn create_empty_asset_processor() -> AssetProcessor { + let mut sources = AssetSourceBuilders::default(); + // Create an empty asset source so that AssetProcessor is happy. + let dir = Dir::default(); + let memory_reader = MemoryAssetReader { root: dir.clone() }; + sources.insert( + AssetSourceId::Default, + AssetSource::build().with_reader(move || Box::new(memory_reader.clone())), + ); + + AssetProcessor::new(&mut sources) +} + +#[test] +fn get_asset_processor_by_name() { + let asset_processor = create_empty_asset_processor(); + asset_processor.register_processor(MyProcessor::(PhantomData)); + + let long_processor = asset_processor + .get_processor( + "bevy_asset::processor::tests::MyProcessor", + ) + .expect("Processor was previously registered"); + let short_processor = asset_processor + .get_processor("MyProcessor") + .expect("Processor was previously registered"); + + // We can use either the long or short processor name and we will get the same processor + // out. + assert!(Arc::ptr_eq(&long_processor, &short_processor)); +} + +#[test] +fn missing_processor_returns_error() { + let asset_processor = create_empty_asset_processor(); + + let Err(long_processor_err) = asset_processor.get_processor( + "bevy_asset::processor::tests::MyProcessor", + ) else { + panic!("Processor was returned even though we never registered any."); + }; + let GetProcessorError::Missing(long_processor_err) = &long_processor_err else { + panic!("get_processor returned incorrect error: {long_processor_err}"); + }; + assert_eq!( + long_processor_err, + "bevy_asset::processor::tests::MyProcessor" + ); + + // Short paths should also return an error. + + let Err(long_processor_err) = asset_processor.get_processor("MyProcessor") else { + panic!("Processor was returned even though we never registered any."); + }; + let GetProcessorError::Missing(long_processor_err) = &long_processor_err else { + panic!("get_processor returned incorrect error: {long_processor_err}"); + }; + assert_eq!(long_processor_err, "MyProcessor"); +} + +// Create another marker type whose short name will overlap `Marker`. +mod sneaky { + use bevy_reflect::TypePath; + + #[derive(TypePath)] + pub struct Marker; +} + +#[test] +fn ambiguous_short_path_returns_error() { + let asset_processor = create_empty_asset_processor(); + asset_processor.register_processor(MyProcessor::(PhantomData)); + asset_processor.register_processor(MyProcessor::(PhantomData)); + + let Err(long_processor_err) = asset_processor.get_processor("MyProcessor") else { + panic!("Processor was returned even though the short path is ambiguous."); + }; + let GetProcessorError::Ambiguous { + processor_short_name, + ambiguous_processor_names, + } = &long_processor_err + else { + panic!("get_processor returned incorrect error: {long_processor_err}"); + }; + assert_eq!(processor_short_name, "MyProcessor"); + let expected_ambiguous_names = [ + "bevy_asset::processor::tests::MyProcessor", + "bevy_asset::processor::tests::MyProcessor", + ]; + assert_eq!(ambiguous_processor_names, &expected_ambiguous_names); + + let processor_1 = asset_processor + .get_processor( + "bevy_asset::processor::tests::MyProcessor", + ) + .expect("Processor was previously registered"); + let processor_2 = asset_processor + .get_processor( + "bevy_asset::processor::tests::MyProcessor", + ) + .expect("Processor was previously registered"); + + // If we fully specify the paths, we get the two different processors. + assert!(!Arc::ptr_eq(&processor_1, &processor_2)); +} + struct AppWithProcessor { app: App, source_dir: Dir, @@ -120,6 +248,7 @@ fn create_app_with_asset_processor() -> AppWithProcessor { } } +#[derive(TypePath)] struct CoolTextSaver; impl AssetSaver for CoolTextSaver { @@ -159,9 +288,10 @@ impl AssetSaver for CoolTextSaver { // Note: while we allow any Fn, since closures are unnameable types, creating a processor with a // closure cannot be used (since we need to include the name of the transformer in the meta // file). +#[derive(TypePath)] struct RootAssetTransformer, A: Asset>(M, PhantomData); -trait MutateAsset: Send + Sync + 'static { +trait MutateAsset: TypePath + Send + Sync + 'static { fn mutate(&self, asset: &mut A); } @@ -227,6 +357,19 @@ fn no_meta_or_default_processor_copies_asset() { assert_eq!(processed_asset, source_asset); } +// The asset processor currently requires multi_threaded. +#[cfg(feature = "multi_threaded")] +#[derive(TypePath)] +struct AddText; + +// The asset processor currently requires multi_threaded. +#[cfg(feature = "multi_threaded")] +impl MutateAsset for AddText { + fn mutate(&self, text: &mut CoolText) { + text.text.push_str("_def"); + } +} + #[test] fn asset_processor_transforms_asset_default_processor() { let AppWithProcessor { @@ -235,14 +378,6 @@ fn asset_processor_transforms_asset_default_processor() { processed_dir, } = create_app_with_asset_processor(); - struct AddText; - - impl MutateAsset for AddText { - fn mutate(&self, text: &mut CoolText) { - text.text.push_str("_def"); - } - } - type CoolTextProcessor = LoadTransformAndSave< CoolTextLoader, RootAssetTransformer, @@ -293,13 +428,65 @@ fn asset_processor_transforms_asset_with_meta() { processed_dir, } = create_app_with_asset_processor(); - struct AddText; + type CoolTextProcessor = LoadTransformAndSave< + CoolTextLoader, + RootAssetTransformer, + CoolTextSaver, + >; + app.register_asset_loader(CoolTextLoader) + .register_asset_processor(CoolTextProcessor::new( + RootAssetTransformer::new(AddText), + CoolTextSaver, + )); - impl MutateAsset for AddText { - fn mutate(&self, text: &mut CoolText) { - text.text.push_str("_def"); - } - } + let path = Path::new("abc.cool.ron"); + source_dir.insert_asset_text( + path, + r#"( + text: "abc", + dependencies: [], + embedded_dependencies: [], + sub_texts: [], +)"#, + ); + source_dir.insert_meta_text(path, r#"( + meta_format_version: "1.0", + asset: Process( + processor: "bevy_asset::processor::process::LoadTransformAndSave, bevy_asset::processor::tests::CoolTextSaver>", + settings: ( + loader_settings: (), + transformer_settings: (), + saver_settings: (), + ), + ), +)"#); + + // Start the app, which also starts the asset processor. + app.update(); + + // Wait for all processing to finish. + bevy_tasks::block_on( + app.world() + .resource::() + .data() + .wait_until_finished(), + ); + + let processed_asset = processed_dir.get_asset(path).unwrap(); + let processed_asset = str::from_utf8(processed_asset.value()).unwrap(); + assert_eq!( + processed_asset, + r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"# + ); +} + +#[test] +fn asset_processor_transforms_asset_with_short_path_meta() { + let AppWithProcessor { + mut app, + source_dir, + processed_dir, + } = create_app_with_asset_processor(); type CoolTextProcessor = LoadTransformAndSave< CoolTextLoader, @@ -325,7 +512,7 @@ fn asset_processor_transforms_asset_with_meta() { source_dir.insert_meta_text(path, r#"( meta_format_version: "1.0", asset: Process( - processor: "bevy_asset::processor::process::LoadTransformAndSave, bevy_asset::processor::tests::CoolTextSaver>", + processor: "LoadTransformAndSave, CoolTextSaver>", settings: ( loader_settings: (), transformer_settings: (), @@ -358,6 +545,7 @@ struct FakeGltf { gltf_nodes: BTreeMap, } +#[derive(TypePath)] struct FakeGltfLoader; impl AssetLoader for FakeGltfLoader { @@ -394,6 +582,7 @@ struct FakeBsn { // scene that holds all the data including parents. // TODO: It would be nice if the inlining was actually done as an `AssetTransformer`, but // `Process` currently has no way to load nested assets. +#[derive(TypePath)] struct FakeBsnLoader; impl AssetLoader for FakeBsnLoader { diff --git a/crates/bevy_asset/src/saver.rs b/crates/bevy_asset/src/saver.rs index 0f3e3987f264a..86d170db02497 100644 --- a/crates/bevy_asset/src/saver.rs +++ b/crates/bevy_asset/src/saver.rs @@ -5,6 +5,7 @@ use crate::{ use alloc::boxed::Box; use atomicow::CowArc; use bevy_platform::collections::HashMap; +use bevy_reflect::TypePath; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::{borrow::Borrow, hash::Hash, ops::Deref}; use serde::{Deserialize, Serialize}; @@ -15,7 +16,7 @@ use serde::{Deserialize, Serialize}; /// This trait is generally used in concert with [`AssetWriter`](crate::io::AssetWriter) to write assets as bytes. /// /// For a complementary version of this trait that can load assets, see [`AssetLoader`]. -pub trait AssetSaver: Send + Sync + 'static { +pub trait AssetSaver: TypePath + Send + Sync + 'static { /// The top level [`Asset`] saved by this [`AssetSaver`]. type Asset: Asset; /// The settings type used by this [`AssetSaver`]. diff --git a/crates/bevy_asset/src/server/loaders.rs b/crates/bevy_asset/src/server/loaders.rs index 9c13c861bd986..e414502a0c89d 100644 --- a/crates/bevy_asset/src/server/loaders.rs +++ b/crates/bevy_asset/src/server/loaders.rs @@ -5,6 +5,8 @@ use crate::{ use alloc::{boxed::Box, sync::Arc, vec::Vec}; use async_broadcast::RecvError; use bevy_platform::collections::HashMap; +#[cfg(feature = "trace")] +use bevy_reflect::TypePath; use bevy_tasks::IoTaskPool; use bevy_utils::TypeIdMap; use core::any::TypeId; @@ -23,8 +25,8 @@ pub(crate) struct AssetLoaders { loaders: Vec, type_id_to_loaders: TypeIdMap>, extension_to_loaders: HashMap, Vec>, - type_name_to_loader: HashMap<&'static str, usize>, - preregistered_loaders: HashMap<&'static str, usize>, + type_path_to_loader: HashMap<&'static str, usize>, + type_path_to_preregistered_loader: HashMap<&'static str, usize>, } impl AssetLoaders { @@ -35,7 +37,8 @@ impl AssetLoaders { /// Registers a new [`AssetLoader`]. [`AssetLoader`]s must be registered before they can be used. pub(crate) fn push(&mut self, loader: L) { - let type_name = core::any::type_name::(); + let type_path = L::type_path(); + // TODO: Allow using the short path of loaders. let loader_asset_type = TypeId::of::(); let loader_asset_type_name = core::any::type_name::(); @@ -44,7 +47,7 @@ impl AssetLoaders { let loader = Arc::new(loader); let (loader_index, is_new) = - if let Some(index) = self.preregistered_loaders.remove(type_name) { + if let Some(index) = self.type_path_to_preregistered_loader.remove(type_path) { (index, false) } else { (self.loaders.len(), true) @@ -75,7 +78,7 @@ impl AssetLoaders { Loader must be specified in a .meta file in order to load assets of this type with these extensions."); } - self.type_name_to_loader.insert(type_name, loader_index); + self.type_path_to_loader.insert(type_path, loader_index); self.type_id_to_loaders .entry(loader_asset_type) @@ -108,12 +111,14 @@ impl AssetLoaders { pub(crate) fn reserve(&mut self, extensions: &[&str]) { let loader_asset_type = TypeId::of::(); let loader_asset_type_name = core::any::type_name::(); - let type_name = core::any::type_name::(); + let type_path = L::type_path(); + // TODO: Allow using the short path of loaders. let loader_index = self.loaders.len(); - self.preregistered_loaders.insert(type_name, loader_index); - self.type_name_to_loader.insert(type_name, loader_index); + self.type_path_to_preregistered_loader + .insert(type_path, loader_index); + self.type_path_to_loader.insert(type_path, loader_index); let existing_loaders_for_type_id = self.type_id_to_loaders.get(&loader_asset_type); let mut duplicate_extensions = Vec::new(); @@ -152,7 +157,7 @@ impl AssetLoaders { /// Get the [`AssetLoader`] by name pub(crate) fn get_by_name(&self, name: &str) -> Option { - let index = self.type_name_to_loader.get(name).copied()?; + let index = self.type_path_to_loader.get(name).copied()?; self.get_by_index(index) } @@ -309,6 +314,7 @@ impl MaybeAssetLoader { } #[cfg(feature = "trace")] +#[derive(TypePath)] struct InstrumentedAssetLoader(T); #[cfg(feature = "trace")] @@ -325,7 +331,7 @@ impl AssetLoader for InstrumentedAssetLoader { ) -> impl ConditionalSendFuture> { let span = info_span!( "asset loading", - loader = core::any::type_name::(), + loader = T::type_path(), asset = load_context.asset_path().to_string(), ); self.0.load(reader, settings, load_context).instrument(span) @@ -361,6 +367,7 @@ mod tests { #[derive(Asset, TypePath, Debug)] struct C; + #[derive(TypePath)] struct Loader { sender: Sender<()>, _phantom: PhantomData, @@ -430,7 +437,7 @@ mod tests { let loader = block_on( loaders - .get_by_name(core::any::type_name::>()) + .get_by_name( as TypePath>::type_path()) .unwrap() .get(), ) diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 49ed00233fe6d..6236c7e850158 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -252,7 +252,7 @@ impl AssetServer { loader.get().await.map_err(|_| error()) } - /// Returns the registered [`AssetLoader`] associated with the given [`core::any::type_name`], if it exists. + /// Returns the registered [`AssetLoader`] associated with the given type name, if it exists. pub async fn get_asset_loader_with_type_name( &self, type_name: &str, @@ -790,7 +790,7 @@ impl AssetServer { path: path.into_owned(), requested: asset_type_id, actual_asset_name: loader.asset_type_name(), - loader_name: loader.type_name(), + loader_name: loader.type_path(), }); } } @@ -1535,12 +1535,12 @@ impl AssetServer { .await .map_err(|_| AssetLoadError::AssetLoaderPanic { path: asset_path.clone_owned(), - loader_name: loader.type_name(), + loader_name: loader.type_path(), })? .map_err(|e| { AssetLoadError::AssetLoaderError(AssetLoaderError { path: asset_path.clone_owned(), - loader_name: loader.type_name(), + loader_name: loader.type_path(), error: e.into(), }) }) diff --git a/crates/bevy_asset/src/transformer.rs b/crates/bevy_asset/src/transformer.rs index f80f44511ab4c..575efbfbaa8eb 100644 --- a/crates/bevy_asset/src/transformer.rs +++ b/crates/bevy_asset/src/transformer.rs @@ -2,6 +2,7 @@ use crate::{meta::Settings, Asset, ErasedLoadedAsset, Handle, LabeledAsset, Unty use alloc::boxed::Box; use atomicow::CowArc; use bevy_platform::collections::HashMap; +use bevy_reflect::TypePath; use bevy_tasks::ConditionalSendFuture; use core::{ borrow::Borrow, @@ -15,7 +16,7 @@ use serde::{Deserialize, Serialize}; /// Transforms an [`Asset`] of a given [`AssetTransformer::AssetInput`] type to an [`Asset`] of [`AssetTransformer::AssetOutput`] type. /// /// This trait is commonly used in association with [`LoadTransformAndSave`](crate::processor::LoadTransformAndSave) to accomplish common asset pipeline workflows. -pub trait AssetTransformer: Send + Sync + 'static { +pub trait AssetTransformer: TypePath + Send + Sync + 'static { /// The [`Asset`] type which this [`AssetTransformer`] takes as and input. type AssetInput: Asset; /// The [`Asset`] type which this [`AssetTransformer`] outputs. @@ -249,6 +250,7 @@ impl<'a, A: Asset> TransformedSubAsset<'a, A> { } /// An identity [`AssetTransformer`] which infallibly returns the input [`Asset`] on transformation.] +#[derive(TypePath)] pub struct IdentityAssetTransformer { _phantom: PhantomData A>, } diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 9cf3ca988b0b1..be54c89ea78ee 100644 --- a/crates/bevy_audio/src/audio_source.rs +++ b/crates/bevy_audio/src/audio_source.rs @@ -34,7 +34,7 @@ impl AsRef<[u8]> for AudioSource { /// `.mp3` with `bevy/mp3` /// `.flac` with `bevy/flac` /// `.wav` with `bevy/wav` -#[derive(Default)] +#[derive(Default, TypePath)] pub struct AudioLoader; impl AssetLoader for AudioLoader { diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 4c651b0dc9ac7..5c9db689f38d2 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -2,6 +2,7 @@ mod extensions; mod gltf_ext; use alloc::sync::Arc; +use bevy_reflect::TypePath; use std::{ io::Error, path::{Path, PathBuf}, @@ -136,6 +137,7 @@ pub enum GltfError { } /// Loads glTF files with all of their data as their corresponding bevy representations. +#[derive(TypePath)] pub struct GltfLoader { /// List of compressed image formats handled by the loader. pub supported_compressed_formats: CompressedImageFormats, diff --git a/crates/bevy_image/src/compressed_image_saver.rs b/crates/bevy_image/src/compressed_image_saver.rs index bcae2f238060d..3675dfc39baa3 100644 --- a/crates/bevy_image/src/compressed_image_saver.rs +++ b/crates/bevy_image/src/compressed_image_saver.rs @@ -1,13 +1,15 @@ use crate::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; use bevy_asset::saver::{AssetSaver, SavedAsset}; +use bevy_reflect::TypePath; use futures_lite::AsyncWriteExt; use thiserror::Error; +#[derive(TypePath)] pub struct CompressedImageSaver; #[non_exhaustive] -#[derive(Debug, Error)] +#[derive(Debug, Error, TypePath)] pub enum CompressedImageSaverError { #[error(transparent)] Io(#[from] std::io::Error), diff --git a/crates/bevy_image/src/exr_texture_loader.rs b/crates/bevy_image/src/exr_texture_loader.rs index 9cbf315bb4f88..e40c9735dfb49 100644 --- a/crates/bevy_image/src/exr_texture_loader.rs +++ b/crates/bevy_image/src/exr_texture_loader.rs @@ -1,12 +1,13 @@ use crate::{Image, TextureAccessError, TextureFormatPixelInfo}; use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; +use bevy_reflect::TypePath; use image::ImageDecoder; use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; /// Loads EXR textures as Texture assets -#[derive(Clone, Default)] +#[derive(Clone, Default, TypePath)] #[cfg(feature = "exr")] pub struct ExrTextureLoader; @@ -18,7 +19,7 @@ pub struct ExrTextureLoaderSettings { /// Possible errors that can be produced by [`ExrTextureLoader`] #[non_exhaustive] -#[derive(Debug, Error)] +#[derive(Debug, Error, TypePath)] #[cfg(feature = "exr")] pub enum ExrTextureLoaderError { #[error(transparent)] diff --git a/crates/bevy_image/src/hdr_texture_loader.rs b/crates/bevy_image/src/hdr_texture_loader.rs index 83e9df3b3d807..6210c91dccf87 100644 --- a/crates/bevy_image/src/hdr_texture_loader.rs +++ b/crates/bevy_image/src/hdr_texture_loader.rs @@ -1,13 +1,14 @@ use crate::{Image, TextureAccessError, TextureFormatPixelInfo}; use bevy_asset::RenderAssetUsages; use bevy_asset::{io::Reader, AssetLoader, LoadContext}; +use bevy_reflect::TypePath; use image::DynamicImage; use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; /// Loads HDR textures as Texture assets -#[derive(Clone, Default)] +#[derive(Clone, Default, TypePath)] pub struct HdrTextureLoader; #[derive(Serialize, Deserialize, Default, Debug)] diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 91d03cb9d1116..2fa51e62a5c40 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -1,12 +1,13 @@ use crate::image::{Image, ImageFormat, ImageType, TextureError}; use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; +use bevy_reflect::TypePath; use thiserror::Error; use super::{CompressedImageFormats, ImageSampler}; use serde::{Deserialize, Serialize}; /// Loader for images that can be read by the `image` crate. -#[derive(Clone)] +#[derive(Clone, TypePath)] pub struct ImageLoader { supported_compressed_formats: CompressedImageFormats, } diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index 49a993ffa4388..b965165db9727 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -145,6 +145,7 @@ pub struct MeshletBoundingSphere { } /// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets. +#[derive(TypePath)] pub struct MeshletMeshSaver; impl AssetSaver for MeshletMeshSaver { @@ -193,6 +194,7 @@ impl AssetSaver for MeshletMeshSaver { } /// An [`AssetLoader`] for `.meshlet_mesh` [`MeshletMesh`] assets. +#[derive(TypePath)] pub struct MeshletMeshLoader; impl AssetLoader for MeshletMeshLoader { diff --git a/crates/bevy_scene/src/scene_loader.rs b/crates/bevy_scene/src/scene_loader.rs index 16b6015023a85..3c448e6f83403 100644 --- a/crates/bevy_scene/src/scene_loader.rs +++ b/crates/bevy_scene/src/scene_loader.rs @@ -2,7 +2,7 @@ use bevy_ecs::{ reflect::AppTypeRegistry, world::{FromWorld, World}, }; -use bevy_reflect::TypeRegistryArc; +use bevy_reflect::{TypePath, TypeRegistryArc}; use thiserror::Error; #[cfg(feature = "serialize")] @@ -15,7 +15,7 @@ use { /// Asset loader for a Bevy dynamic scene (`.scn` / `.scn.ron`). /// /// The loader handles assets serialized with [`DynamicScene::serialize`]. -#[derive(Debug)] +#[derive(Debug, TypePath)] pub struct SceneLoader { #[cfg_attr( not(feature = "serialize"), diff --git a/crates/bevy_shader/src/shader.rs b/crates/bevy_shader/src/shader.rs index 932de7b98ceff..4de9b8e913a40 100644 --- a/crates/bevy_shader/src/shader.rs +++ b/crates/bevy_shader/src/shader.rs @@ -338,7 +338,7 @@ impl From<&Source> for naga_oil::compose::ShaderType { } } -#[derive(Default)] +#[derive(Default, TypePath)] pub struct ShaderLoader; #[non_exhaustive] diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index 9e0f2185a234e..22bacf208af13 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -1,8 +1,9 @@ use crate::Font; use bevy_asset::{io::Reader, AssetLoader, LoadContext}; +use bevy_reflect::TypePath; use thiserror::Error; -#[derive(Default)] +#[derive(Default, TypePath)] /// An [`AssetLoader`] for [`Font`]s, for use by the [`AssetServer`](bevy_asset::AssetServer) pub struct FontLoader; diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index e514924ca9d0a..0663a398a072a 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -17,7 +17,7 @@ struct GzAsset { uncompressed: ErasedLoadedAsset, } -#[derive(Default)] +#[derive(Default, TypePath)] struct GzAssetLoader; /// Possible errors that can be produced by [`GzAssetLoader`] diff --git a/examples/asset/custom_asset.rs b/examples/asset/custom_asset.rs index 8d4ac958ecdd1..60982e53fa313 100644 --- a/examples/asset/custom_asset.rs +++ b/examples/asset/custom_asset.rs @@ -17,7 +17,7 @@ struct CustomAsset { value: i32, } -#[derive(Default)] +#[derive(Default, TypePath)] struct CustomAssetLoader; /// Possible errors that can be produced by [`CustomAssetLoader`] @@ -58,7 +58,7 @@ struct Blob { bytes: Vec, } -#[derive(Default)] +#[derive(Default, TypePath)] struct BlobAssetLoader; /// Possible errors that can be produced by [`BlobAssetLoader`] diff --git a/examples/asset/processing/asset_processing.rs b/examples/asset/processing/asset_processing.rs index d5da644c27190..651dbe2148a76 100644 --- a/examples/asset/processing/asset_processing.rs +++ b/examples/asset/processing/asset_processing.rs @@ -69,7 +69,7 @@ impl Plugin for TextPlugin { #[derive(Asset, TypePath, Debug)] struct Text(String); -#[derive(Default)] +#[derive(Default, TypePath)] struct TextLoader; #[derive(Clone, Default, Serialize, Deserialize)] @@ -120,7 +120,7 @@ struct CoolText { dependencies: Vec>, } -#[derive(Default)] +#[derive(Default, TypePath)] struct CoolTextLoader; #[derive(Debug, Error)] @@ -182,7 +182,7 @@ impl AssetLoader for CoolTextLoader { } } -#[derive(Default)] +#[derive(Default, TypePath)] struct CoolTextTransformer; #[derive(Default, Serialize, Deserialize)] @@ -206,6 +206,7 @@ impl AssetTransformer for CoolTextTransformer { } } +#[derive(TypePath)] struct CoolTextSaver; impl AssetSaver for CoolTextSaver { diff --git a/examples/asset/processing/assets/a.cool.ron.meta b/examples/asset/processing/assets/a.cool.ron.meta index d87c629bac33d..bfe78b3128f5d 100644 --- a/examples/asset/processing/assets/a.cool.ron.meta +++ b/examples/asset/processing/assets/a.cool.ron.meta @@ -1,7 +1,7 @@ ( meta_format_version: "1.0", asset: Process( - processor: "bevy_asset::processor::process::LoadTransformAndSave", + processor: "LoadTransformAndSave", settings: ( loader_settings: (), transformer_settings: ( diff --git a/release-content/migration-guides/type_path_for_asset_traits.md b/release-content/migration-guides/type_path_for_asset_traits.md new file mode 100644 index 0000000000000..eda0fd901735f --- /dev/null +++ b/release-content/migration-guides/type_path_for_asset_traits.md @@ -0,0 +1,25 @@ +--- +title: Traits `AssetLoader`, `AssetTransformer`, `AssetSaver`, and `Process` all now require `TypePath` +pull_requests: [21339] +--- + +The `AssetLoader`, `AssetTransformer`, `AssetSaver`, and `Process` traits now include a super trait +of `TypePath`. This means if you previously had a loader like: + +```rust +struct MyFunkyLoader { + add_funk: u32, +} +``` + +You will need to add the following derive: + +```rust +#[derive(TypePath)] +struct MyFunkyLoader { + add_funk: u32, +} +``` + +`TypePath` comes from `bevy_reflect`, so libraries may also need to add a dependency on +`bevy_reflect`. diff --git a/release-content/release-notes/short_type_path_asset_processors.md b/release-content/release-notes/short_type_path_asset_processors.md new file mode 100644 index 0000000000000..ab74ae7febb98 --- /dev/null +++ b/release-content/release-notes/short_type_path_asset_processors.md @@ -0,0 +1,44 @@ +--- +title: Short-type-path asset processors +authors: ["@andriyDev"] +pull_requests: [21339] +--- + +Asset processors allow manipulating assets at "publish-time" to convert them into a more optimal +form when loading the data at runtime. This can either be done using a default processor, which +processes all assets with a particular file extension, or by specifying the processor in the asset's +meta file. + +In previous versions of Bevy, the processor had to be **fully** specified in the asset's meta file. +For example: + +```ron +( + meta_format_version: "1.0", + asset: Process( + processor: "bevy_asset::processor::process::LoadTransformAndSave", + settings: ( + loader_settings: (), + transformer_settings: (), + saver_settings: (), + ), + ), +) +``` + +As you can see, processor types can be very verbose! In order to make these meta files easier to +manipulate, we now also support using the "short type path" of the asset. This would look like: + +```ron +( + meta_format_version: "1.0", + asset: Process( + processor: "LoadTransformAndSave", + settings: ( + loader_settings: (), + transformer_settings: (), + saver_settings: (), + ), + ), +) +```