diff --git a/assets/data/asset_no_extension b/assets/data/asset_no_extension new file mode 100644 index 0000000000..8132d3c0e0 --- /dev/null +++ b/assets/data/asset_no_extension @@ -0,0 +1,3 @@ +CustomAsset ( + value: 13 +) diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 7c9669c67a..9b8f83b0ee 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -267,10 +267,7 @@ impl AsyncRead for VecReader { /// Appends `.meta` to the given path. pub(crate) fn get_meta_path(path: &Path) -> PathBuf { let mut meta_path = path.to_path_buf(); - let mut extension = path - .extension() - .unwrap_or_else(|| panic!("missing extension for asset path {path:?}")) - .to_os_string(); + let mut extension = path.extension().unwrap_or_default().to_os_string(); extension.push(".meta"); meta_path.set_extension(extension); meta_path diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index b2ccc4350e..b209c6f1f7 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -37,8 +37,10 @@ pub trait AssetLoader: Send + Sync + 'static { load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result>; - /// Returns a list of extensions supported by this asset loader, without the preceding dot. - fn extensions(&self) -> &[&str]; + /// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot. + fn extensions(&self) -> &[&str] { + &[] + } } /// Provides type-erased access to an [`AssetLoader`]. @@ -396,7 +398,7 @@ impl<'a> LoadContext<'a> { /// See [`AssetPath`] for more on labeled assets. pub fn has_labeled_asset<'b>(&self, label: impl Into>) -> bool { let path = self.asset_path.clone().with_label(label.into()); - self.asset_server.get_handle_untyped(&path).is_some() + !self.asset_server.get_handles_untyped(&path).is_empty() } /// "Finishes" this context by populating the final [`Asset`] value (and the erased [`AssetMeta`] value, if it exists). @@ -546,7 +548,7 @@ impl<'a> LoadContext<'a> { let loaded_asset = { let (meta, loader, mut reader) = self .asset_server - .get_meta_loader_and_reader(&path) + .get_meta_loader_and_reader(&path, None) .await .map_err(to_error)?; self.asset_server diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index 2165c57b9c..e08dbed7c3 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -61,7 +61,7 @@ impl AssetInfo { #[derive(Default)] pub(crate) struct AssetInfos { - path_to_id: HashMap, UntypedAssetId>, + path_to_id: HashMap, HashMap>, infos: HashMap, /// If set to `true`, this informs [`AssetInfos`] to track data relevant to watching for changes (such as `load_dependants`) /// This should only be set at startup. @@ -191,7 +191,20 @@ impl AssetInfos { loading_mode: HandleLoadingMode, meta_transform: Option, ) -> Result<(UntypedHandle, bool), GetOrCreateHandleInternalError> { - match self.path_to_id.entry(path.clone()) { + let handles = self.path_to_id.entry(path.clone()).or_default(); + + let type_id = type_id + .or_else(|| { + // If a TypeId is not provided, we may be able to infer it if only a single entry exists + if handles.len() == 1 { + Some(*handles.keys().next().unwrap()) + } else { + None + } + }) + .ok_or(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified)?; + + match handles.entry(type_id) { Entry::Occupied(entry) => { let id = *entry.get(); // if there is a path_to_id entry, info always exists @@ -222,9 +235,6 @@ impl AssetInfos { // We must create a new strong handle for the existing id and ensure that the drop of the old // strong handle doesn't remove the asset from the Assets collection info.handle_drops_to_skip += 1; - let type_id = type_id.ok_or( - GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified, - )?; let provider = self .handle_providers .get(&type_id) @@ -241,8 +251,6 @@ impl AssetInfos { HandleLoadingMode::NotLoading => false, HandleLoadingMode::Request | HandleLoadingMode::Force => true, }; - let type_id = type_id - .ok_or(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified)?; let handle = Self::create_handle_internal( &mut self.infos, &self.handle_providers, @@ -271,13 +279,52 @@ impl AssetInfos { self.infos.get_mut(&id) } - pub(crate) fn get_path_id(&self, path: &AssetPath) -> Option { - self.path_to_id.get(path).copied() + pub(crate) fn get_path_and_type_id_handle( + &self, + path: &AssetPath, + type_id: TypeId, + ) -> Option { + let id = self.path_to_id.get(path)?.get(&type_id)?; + self.get_id_handle(*id) } - pub(crate) fn get_path_handle(&self, path: &AssetPath) -> Option { - let id = *self.path_to_id.get(path)?; - self.get_id_handle(id) + pub(crate) fn get_path_ids<'a>( + &'a self, + path: &'a AssetPath<'a>, + ) -> impl Iterator + 'a { + /// Concrete type to allow returning an `impl Iterator` even if `self.path_to_id.get(&path)` is `None` + enum HandlesByPathIterator { + None, + Some(T), + } + + impl Iterator for HandlesByPathIterator + where + T: Iterator, + { + type Item = UntypedAssetId; + + fn next(&mut self) -> Option { + match self { + HandlesByPathIterator::None => None, + HandlesByPathIterator::Some(iter) => iter.next(), + } + } + } + + if let Some(type_id_to_id) = self.path_to_id.get(path) { + HandlesByPathIterator::Some(type_id_to_id.values().copied()) + } else { + HandlesByPathIterator::None + } + } + + pub(crate) fn get_path_handles<'a>( + &'a self, + path: &'a AssetPath<'a>, + ) -> impl Iterator + 'a { + self.get_path_ids(path) + .filter_map(|id| self.get_id_handle(id)) } pub(crate) fn get_id_handle(&self, id: UntypedAssetId) -> Option { @@ -289,12 +336,13 @@ impl AssetInfos { /// Returns `true` if the asset this path points to is still alive pub(crate) fn is_path_alive<'a>(&self, path: impl Into>) -> bool { let path = path.into(); - if let Some(id) = self.path_to_id.get(&path) { - if let Some(info) = self.infos.get(id) { - return info.weak_handle.strong_count() > 0; - } - } - false + + let result = self + .get_path_ids(&path) + .filter_map(|id| self.infos.get(&id)) + .any(|info| info.weak_handle.strong_count() > 0); + + result } /// Returns `true` if the asset at this path should be reloaded @@ -592,7 +640,7 @@ impl AssetInfos { fn process_handle_drop_internal( infos: &mut HashMap, - path_to_id: &mut HashMap, UntypedAssetId>, + path_to_id: &mut HashMap, HashMap>, loader_dependants: &mut HashMap, HashSet>>, living_labeled_assets: &mut HashMap, HashSet>, watching_for_changes: bool, @@ -609,6 +657,8 @@ impl AssetInfos { return false; } + let type_id = entry.key().type_id(); + let info = entry.remove(); let Some(path) = &info.path else { return true; @@ -622,7 +672,15 @@ impl AssetInfos { living_labeled_assets, ); } - path_to_id.remove(path); + + if let Some(map) = path_to_id.get_mut(path) { + map.remove(&type_id); + + if map.is_empty() { + path_to_id.remove(path); + } + }; + true } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index f84f7d1e70..9863c87130 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -142,20 +142,22 @@ impl AssetServer { if let Some(index) = loaders.preregistered_loaders.remove(type_name) { (index, false) } else { - (loaders.values.len(), true) + (TypeId::of::(), true) }; for extension in loader.extensions() { loaders - .extension_to_index + .extension_to_type_id .insert(extension.to_string(), loader_index); } if is_new { - loaders.type_name_to_index.insert(type_name, loader_index); - loaders.values.push(MaybeAssetLoader::Ready(loader)); + loaders.type_name_to_type_id.insert(type_name, loader_index); + loaders + .type_id_to_loader + .insert(loader_index, MaybeAssetLoader::Ready(loader)); } else { let maybe_loader = std::mem::replace( - &mut loaders.values[loader_index], + loaders.type_id_to_loader.get_mut(&loader_index).unwrap(), MaybeAssetLoader::Ready(loader.clone()), ); match maybe_loader { @@ -219,12 +221,12 @@ impl AssetServer { ) -> Result, MissingAssetLoaderForExtensionError> { let loader = { let loaders = self.data.loaders.read(); - let index = *loaders.extension_to_index.get(extension).ok_or_else(|| { + let index = *loaders.extension_to_type_id.get(extension).ok_or_else(|| { MissingAssetLoaderForExtensionError { extensions: vec![extension.to_string()], } })?; - loaders.values[index].clone() + loaders.type_id_to_loader[&index].clone() }; match loader { @@ -240,13 +242,13 @@ impl AssetServer { ) -> Result, MissingAssetLoaderForTypeNameError> { let loader = { let loaders = self.data.loaders.read(); - let index = *loaders.type_name_to_index.get(type_name).ok_or_else(|| { + let index = *loaders.type_name_to_type_id.get(type_name).ok_or_else(|| { MissingAssetLoaderForTypeNameError { type_name: type_name.to_string(), } })?; - loaders.values[index].clone() + loaders.type_id_to_loader[&index].clone() }; match loader { MaybeAssetLoader::Ready(loader) => Ok(loader), @@ -279,6 +281,34 @@ impl AssetServer { Err(MissingAssetLoaderForExtensionError { extensions }) } + /// Retrieves the default [`AssetLoader`] for the given [`Asset`] [`TypeId`], if one can be found. + pub async fn get_asset_loader_with_asset_type_id<'a>( + &self, + type_id: TypeId, + ) -> Result, MissingAssetLoaderForTypeIdError> { + let loader = { + let loaders = self.data.loaders.read(); + loaders + .type_id_to_loader + .get(&type_id) + .ok_or(MissingAssetLoaderForTypeIdError { type_id })? + .clone() + }; + + match loader { + MaybeAssetLoader::Ready(loader) => Ok(loader), + MaybeAssetLoader::Pending { mut receiver, .. } => Ok(receiver.recv().await.unwrap()), + } + } + + /// Retrieves the default [`AssetLoader`] for the given [`Asset`] type, if one can be found. + pub async fn get_asset_loader_with_asset_type<'a, A: Asset>( + &self, + ) -> Result, MissingAssetLoaderForTypeIdError> { + self.get_asset_loader_with_asset_type_id(TypeId::of::()) + .await + } + /// Begins loading an [`Asset`] of type `A` stored at `path`. This will not block on the asset load. Instead, /// it returns a "strong" [`Handle`]. When the [`Asset`] is loaded (and enters [`LoadState::Loaded`]), it will be added to the /// associated [`Assets`] resource. @@ -427,10 +457,12 @@ impl AssetServer { force: bool, meta_transform: Option, ) -> Result { + let asset_type_id = input_handle.as_ref().map(|handle| handle.type_id()); + let path = path.into_owned(); let path_clone = path.clone(); let (mut meta, loader, mut reader) = self - .get_meta_loader_and_reader(&path_clone) + .get_meta_loader_and_reader(&path_clone, asset_type_id) .await .map_err(|e| { // if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if @@ -477,6 +509,11 @@ impl AssetServer { let handle = if let Some((handle, should_load)) = handle_result { if path.label().is_none() && handle.type_id() != loader.asset_type_id() { + error!( + "Expected {:?}, got {:?}", + handle.type_id(), + loader.asset_type_id() + ); return Err(AssetLoadError::RequestedHandleTypeMismatch { path: path.into_owned(), requested: handle.type_id(), @@ -569,7 +606,24 @@ impl AssetServer { let path = path.into().into_owned(); IoTaskPool::get() .spawn(async move { - if server.data.infos.read().should_reload(&path) { + let mut reloaded = false; + + let requests = server + .data + .infos + .read() + .get_path_handles(&path) + .map(|handle| server.load_internal(Some(handle), path.clone(), true, None)) + .collect::>(); + + for result in requests { + match result.await { + Ok(_) => reloaded = true, + Err(err) => error!("{}", err), + } + } + + if !reloaded && server.data.infos.read().should_reload(&path) { if let Err(err) = server.load_internal(None, path, true, None).await { error!("{}", err); } @@ -792,7 +846,7 @@ impl AssetServer { /// Returns an active handle for the given path, if the asset at the given path has already started loading, /// or is still "alive". pub fn get_handle<'a, A: Asset>(&self, path: impl Into>) -> Option> { - self.get_handle_untyped(path) + self.get_path_and_type_id_handle(&path.into(), TypeId::of::()) .map(|h| h.typed_debug_checked()) } @@ -812,18 +866,58 @@ impl AssetServer { /// 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". + /// Returns the first ID in the event of multiple assets being registered against a single path. + /// + /// # See also + /// [`get_path_ids`][Self::get_path_ids] for all handles. 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) + let mut ids = infos.get_path_ids(&path); + ids.next() + } + + /// Returns all active untyped asset IDs for the given path, if the assets at the given path have already started loading, + /// or are still "alive". + /// Multiple IDs will be returned in the event that a single path is used by multiple [`AssetLoader`]'s. + pub fn get_path_ids<'a>(&self, path: impl Into>) -> Vec { + let infos = self.data.infos.read(); + let path = path.into(); + infos.get_path_ids(&path).collect() } /// Returns an active untyped handle for the given path, if the asset at the given path has already started loading, /// or is still "alive". + /// Returns the first handle in the event of multiple assets being registered against a single path. + /// + /// # See also + /// [`get_handles_untyped`][Self::get_handles_untyped] for all handles. 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) + let mut handles = infos.get_path_handles(&path); + handles.next() + } + + /// Returns all active untyped handles for the given path, if the assets at the given path have already started loading, + /// or are still "alive". + /// Multiple handles will be returned in the event that a single path is used by multiple [`AssetLoader`]'s. + pub fn get_handles_untyped<'a>(&self, path: impl Into>) -> Vec { + let infos = self.data.infos.read(); + let path = path.into(); + infos.get_path_handles(&path).collect() + } + + /// Returns an active untyped handle for the given path and [`TypeId`], if the asset at the given path has already started loading, + /// or is still "alive". + pub fn get_path_and_type_id_handle( + &self, + path: &AssetPath, + type_id: TypeId, + ) -> Option { + let infos = self.data.infos.read(); + let path = path.into(); + infos.get_path_and_type_id_handle(&path, type_id) } /// Returns the path for the given `id`, if it has one. @@ -844,15 +938,15 @@ impl AssetServer { /// real loader is added. pub fn preregister_loader(&self, extensions: &[&str]) { let mut loaders = self.data.loaders.write(); - let loader_index = loaders.values.len(); + let loader_index = TypeId::of::(); let type_name = std::any::type_name::(); loaders .preregistered_loaders .insert(type_name, loader_index); - loaders.type_name_to_index.insert(type_name, loader_index); + loaders.type_name_to_type_id.insert(type_name, loader_index); for extension in extensions { if loaders - .extension_to_index + .extension_to_type_id .insert(extension.to_string(), loader_index) .is_some() { @@ -862,8 +956,8 @@ impl AssetServer { let (mut sender, receiver) = async_broadcast::broadcast(1); sender.set_overflow(true); loaders - .values - .push(MaybeAssetLoader::Pending { sender, receiver }); + .type_id_to_loader + .insert(loader_index, MaybeAssetLoader::Pending { sender, receiver }); } /// Retrieve a handle for the given path. This will create a handle (and [`AssetInfo`]) if it does not exist @@ -885,6 +979,7 @@ impl AssetServer { pub(crate) async fn get_meta_loader_and_reader<'a>( &'a self, asset_path: &'a AssetPath<'_>, + asset_type_id: Option, ) -> Result< ( Box, @@ -944,19 +1039,58 @@ impl AssetServer { Ok((meta, loader, reader)) } Err(AssetReaderError::NotFound(_)) => { - let loader = self.get_path_asset_loader(asset_path).await?; + let loader = self.resolve_loader(asset_path, asset_type_id).await?; + let meta = loader.default_meta(); Ok((meta, loader, reader)) } Err(err) => Err(err.into()), } } else { - let loader = self.get_path_asset_loader(asset_path).await?; + let loader = self.resolve_loader(asset_path, asset_type_id).await?; + let meta = loader.default_meta(); Ok((meta, loader, reader)) } } + /// Selects an [`AssetLoader`] for the provided path and (optional) [`Asset`] [`TypeId`]. + /// Prefers [`TypeId`], and falls back to reading the file extension in the provided [`AssetPath`] otherwise. + async fn resolve_loader<'a>( + &'a self, + asset_path: &'a AssetPath<'_>, + asset_type_id: Option, + ) -> Result, MissingAssetLoaderForExtensionError> { + let loader = 'type_resolution: { + let Some(type_id) = asset_type_id else { + // If not provided an asset_type_id, type inference is broken + break 'type_resolution None; + }; + + let None = asset_path.label() else { + // Labelled sub-assets could be any type, not just the one registered for the loader + break 'type_resolution None; + }; + + let Ok(loader) = self.get_asset_loader_with_asset_type_id(type_id).await else { + bevy_log::warn!( + "Could not load asset via type_id: no asset loader registered for {:?}", + type_id + ); + break 'type_resolution None; + }; + + Some(loader) + }; + + let loader = match loader { + Some(loader) => loader, + None => self.get_path_asset_loader(asset_path).await?, + }; + + Ok(loader) + } + pub(crate) async fn load_with_meta_loader_and_reader( &self, asset_path: &AssetPath<'_>, @@ -1045,9 +1179,9 @@ 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) { + for folder_handle in infos.get_path_handles(&parent_asset_path) { info!("Reloading folder {parent_asset_path} because the content has changed"); - server.load_folder_internal(folder_handle.id(), parent_asset_path); + server.load_folder_internal(folder_handle.id(), parent_asset_path.clone()); } } }; @@ -1104,10 +1238,10 @@ pub fn handle_internal_asset_events(world: &mut World) { #[derive(Default)] pub(crate) struct AssetLoaders { - values: Vec, - extension_to_index: HashMap, - type_name_to_index: HashMap<&'static str, usize>, - preregistered_loaders: HashMap<&'static str, usize>, + type_id_to_loader: HashMap, + extension_to_type_id: HashMap, + type_name_to_type_id: HashMap<&'static str, TypeId>, + preregistered_loaders: HashMap<&'static str, TypeId>, } #[derive(Clone)] @@ -1190,6 +1324,8 @@ pub enum AssetLoadError { #[error(transparent)] MissingAssetLoaderForTypeName(#[from] MissingAssetLoaderForTypeNameError), #[error(transparent)] + MissingAssetLoaderForTypeIdError(#[from] MissingAssetLoaderForTypeIdError), + #[error(transparent)] AssetReaderError(#[from] AssetReaderError), #[error(transparent)] MissingAssetSourceError(#[from] MissingAssetSourceError), @@ -1238,6 +1374,13 @@ pub struct MissingAssetLoaderForTypeNameError { type_name: String, } +/// An error that occurs when an [`AssetLoader`] is not registered for a given [`Asset`] [`TypeId`]. +#[derive(Error, Debug, Clone)] +#[error("no `AssetLoader` found with the ID '{type_id:?}'")] +pub struct MissingAssetLoaderForTypeIdError { + pub type_id: TypeId, +} + fn format_missing_asset_ext(exts: &[String]) -> String { if !exts.is_empty() { format!( diff --git a/examples/asset/custom_asset.rs b/examples/asset/custom_asset.rs index 8dd111ea4a..022bbaa309 100644 --- a/examples/asset/custom_asset.rs +++ b/examples/asset/custom_asset.rs @@ -53,12 +53,52 @@ impl AssetLoader for CustomAssetLoader { } } +#[derive(Asset, TypePath, Debug)] +pub struct Blob { + pub bytes: Vec, +} + +#[derive(Default)] +pub struct BlobAssetLoader; + +/// Possible errors that can be produced by [`CustomAssetLoader`] +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum BlobAssetLoaderError { + /// An [IO](std::io) Error + #[error("Could not load file: {0}")] + Io(#[from] std::io::Error), +} + +impl AssetLoader for BlobAssetLoader { + type Asset = Blob; + type Settings = (); + type Error = BlobAssetLoaderError; + + fn load<'a>( + &'a self, + reader: &'a mut Reader, + _settings: &'a (), + _load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + info!("Loading Blob..."); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + + Ok(Blob { bytes }) + }) + } +} + fn main() { App::new() .add_plugins(DefaultPlugins) .init_resource::() .init_asset::() + .init_asset::() .init_asset_loader::() + .init_asset_loader::() .add_systems(Startup, setup) .add_systems(Update, print_on_load) .run(); @@ -67,19 +107,55 @@ fn main() { #[derive(Resource, Default)] struct State { handle: Handle, + other_handle: Handle, + blob: Handle, printed: bool, } fn setup(mut state: ResMut, asset_server: Res) { + // Recommended way to load an asset state.handle = asset_server.load("data/asset.custom"); + + // File extensions are optional, but are recommended for project management and last-resort inference + state.other_handle = asset_server.load("data/asset_no_extension"); + + // Will use BlobAssetLoader instead of CustomAssetLoader thanks to type inference + state.blob = asset_server.load("data/asset.custom"); } -fn print_on_load(mut state: ResMut, custom_assets: Res>) { +fn print_on_load( + mut state: ResMut, + custom_assets: Res>, + blob_assets: Res>, +) { let custom_asset = custom_assets.get(&state.handle); - if state.printed || custom_asset.is_none() { + let other_custom_asset = custom_assets.get(&state.other_handle); + let blob = blob_assets.get(&state.blob); + + // Can't print results if the assets aren't ready + if state.printed { + return; + } + + if custom_asset.is_none() { + info!("Custom Asset Not Ready"); + return; + } + + if other_custom_asset.is_none() { + info!("Other Custom Asset Not Ready"); + return; + } + + if blob.is_none() { + info!("Blob Not Ready"); return; } info!("Custom asset loaded: {:?}", custom_asset.unwrap()); + info!("Custom asset loaded: {:?}", other_custom_asset.unwrap()); + info!("Blob Size: {:?} Bytes", blob.unwrap().bytes.len()); + + // Once printed, we won't print again state.printed = true; }