Use Asset Path Extension for AssetLoader Disambiguation (#11644)

# Objective

- Fixes #11638
- See
[here](https://github.com/bevyengine/bevy/issues/11638#issuecomment-1920508465)
for details on the cause of this issue.

## Solution

- Modified `AssetLoaders` to capture possibility of multiple
`AssetLoader` registrations operating on the same `Asset` type, but
different extensions.
- Added an algorithm which will attempt to resolve via `AssetLoader`
name, then `Asset` type, then by extension. If at any point multiple
loaders fit a particular criteria, the next criteria is used as a tie
breaker.
This commit is contained in:
Zachary Harrold 2024-02-13 02:44:55 +11:00 committed by GitHub
parent 87add5660f
commit 7b5a4ec4ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 885 additions and 168 deletions

View file

@ -38,6 +38,7 @@ pub trait AssetLoader: Send + Sync + 'static {
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>>;
/// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot.
/// Note that users of this [`AssetLoader`] may choose to load files with a non-matching extension.
fn extensions(&self) -> &[&str] {
&[]
}

View file

@ -0,0 +1,807 @@
use crate::{
loader::{AssetLoader, ErasedAssetLoader},
path::AssetPath,
};
use async_broadcast::RecvError;
use bevy_log::{error, info, warn};
use bevy_tasks::IoTaskPool;
use bevy_utils::{HashMap, TypeIdMap};
use std::{any::TypeId, sync::Arc};
use thiserror::Error;
#[derive(Default)]
pub(crate) struct AssetLoaders {
loaders: Vec<MaybeAssetLoader>,
type_id_to_loaders: TypeIdMap<Vec<usize>>,
extension_to_loaders: HashMap<String, Vec<usize>>,
type_name_to_loader: HashMap<&'static str, usize>,
preregistered_loaders: HashMap<&'static str, usize>,
}
impl AssetLoaders {
/// Get the [`AssetLoader`] stored at the specific index
fn get_by_index(&self, index: usize) -> Option<MaybeAssetLoader> {
self.loaders.get(index).cloned()
}
/// Registers a new [`AssetLoader`]. [`AssetLoader`]s must be registered before they can be used.
pub(crate) fn push<L: AssetLoader>(&mut self, loader: L) {
let type_name = std::any::type_name::<L>();
let loader_asset_type = TypeId::of::<L::Asset>();
let loader_asset_type_name = std::any::type_name::<L::Asset>();
let loader = Arc::new(loader);
let (loader_index, is_new) =
if let Some(index) = self.preregistered_loaders.remove(type_name) {
(index, false)
} else {
(self.loaders.len(), true)
};
if is_new {
for extension in loader.extensions() {
let list = self
.extension_to_loaders
.entry(extension.to_string())
.or_default();
if !list.is_empty() {
warn!("duplicate registration for extension `{extension}`.");
}
list.push(loader_index);
}
self.type_name_to_loader.insert(type_name, loader_index);
let list = self
.type_id_to_loaders
.entry(loader_asset_type)
.or_default();
if !list.is_empty() {
info!("duplicate registration for type `{loader_asset_type_name}`.");
}
list.push(loader_index);
self.loaders.push(MaybeAssetLoader::Ready(loader));
} else {
let maybe_loader = std::mem::replace(
self.loaders.get_mut(loader_index).unwrap(),
MaybeAssetLoader::Ready(loader.clone()),
);
match maybe_loader {
MaybeAssetLoader::Ready(_) => unreachable!(),
MaybeAssetLoader::Pending { sender, .. } => {
IoTaskPool::get()
.spawn(async move {
let _ = sender.broadcast(loader).await;
})
.detach();
}
}
}
}
/// Pre-register an [`AssetLoader`] that will later be added.
///
/// Assets loaded with matching extensions will be blocked until the
/// real loader is added.
pub(crate) fn reserve<L: AssetLoader>(&mut self, extensions: &[&str]) {
let loader_asset_type = TypeId::of::<L::Asset>();
let loader_asset_type_name = std::any::type_name::<L::Asset>();
let type_name = std::any::type_name::<L>();
let loader_index = self.loaders.len();
self.preregistered_loaders.insert(type_name, loader_index);
self.type_name_to_loader.insert(type_name, loader_index);
for extension in extensions {
let list = self
.extension_to_loaders
.entry(extension.to_string())
.or_default();
if !list.is_empty() {
warn!("duplicate preregistration for extension `{extension}`.");
}
list.push(loader_index);
}
let list = self
.type_id_to_loaders
.entry(loader_asset_type)
.or_default();
if !list.is_empty() {
info!("duplicate preregistration for type `{loader_asset_type_name}`.");
}
list.push(loader_index);
let (mut sender, receiver) = async_broadcast::broadcast(1);
sender.set_overflow(true);
self.loaders
.push(MaybeAssetLoader::Pending { sender, receiver });
}
/// Get the [`AssetLoader`] by name
pub(crate) fn get_by_name(&self, name: &str) -> Option<MaybeAssetLoader> {
let index = self.type_name_to_loader.get(name).copied()?;
self.get_by_index(index)
}
/// Find an [`AssetLoader`] based on provided search criteria
pub(crate) fn find(
&self,
type_name: Option<&str>,
asset_type_id: Option<TypeId>,
extension: Option<&str>,
asset_path: Option<&AssetPath<'_>>,
) -> Option<MaybeAssetLoader> {
// If provided the type name of the loader, return that immediately
if let Some(type_name) = type_name {
return self.get_by_name(type_name);
}
// The presence of a label will affect loader choice
let label = asset_path.as_ref().and_then(|path| path.label());
// Try by asset type
let candidates = if let Some(type_id) = asset_type_id {
if label.is_none() {
Some(self.type_id_to_loaders.get(&type_id)?)
} else {
None
}
} else {
None
};
if let Some(candidates) = candidates {
if candidates.is_empty() {
return None;
} else if candidates.len() == 1 {
let index = candidates.first().copied().unwrap();
return self.get_by_index(index);
}
}
// Asset type is insufficient, use extension information
let try_extension = |extension| {
if let Some(indices) = self.extension_to_loaders.get(extension) {
if let Some(candidates) = candidates {
if candidates.is_empty() {
indices.last()
} else {
indices
.iter()
.rev()
.find(|index| candidates.contains(index))
}
} else {
indices.last()
}
} else {
None
}
};
// Try the provided extension
if let Some(extension) = extension {
if let Some(&index) = try_extension(extension) {
return self.get_by_index(index);
}
}
// Try extracting the extension from the path
if let Some(full_extension) = asset_path.and_then(|path| path.get_full_extension()) {
if let Some(&index) = try_extension(full_extension.as_str()) {
return self.get_by_index(index);
}
// Try secondary extensions from the path
for extension in AssetPath::iter_secondary_extensions(&full_extension) {
if let Some(&index) = try_extension(extension) {
return self.get_by_index(index);
}
}
}
// Fallback if no resolution step was conclusive
match candidates?
.last()
.copied()
.and_then(|index| self.get_by_index(index))
{
Some(loader) => {
warn!(
"Multiple AssetLoaders found for Asset: {:?}; Path: {:?}; Extension: {:?}",
asset_type_id, asset_path, extension
);
Some(loader)
}
None => {
warn!(
"No AssetLoader found for Asset: {:?}; Path: {:?}; Extension: {:?}",
asset_type_id, asset_path, extension
);
None
}
}
}
/// Get the [`AssetLoader`] for a given asset type
pub(crate) fn get_by_type(&self, type_id: TypeId) -> Option<MaybeAssetLoader> {
let index = self.type_id_to_loaders.get(&type_id)?.last().copied()?;
self.get_by_index(index)
}
/// Get the [`AssetLoader`] for a given extension
pub(crate) fn get_by_extension(&self, extension: &str) -> Option<MaybeAssetLoader> {
let index = self.extension_to_loaders.get(extension)?.last().copied()?;
self.get_by_index(index)
}
/// Get the [`AssetLoader`] for a given path
pub(crate) fn get_by_path(&self, path: &AssetPath<'_>) -> Option<MaybeAssetLoader> {
let extension = path.get_full_extension()?;
let result = std::iter::once(extension.as_str())
.chain(AssetPath::iter_secondary_extensions(&extension))
.filter_map(|extension| self.extension_to_loaders.get(extension)?.last().copied())
.find_map(|index| self.get_by_index(index))?;
Some(result)
}
}
#[derive(Error, Debug, Clone)]
pub(crate) enum GetLoaderError {
#[error(transparent)]
CouldNotResolve(#[from] RecvError),
}
#[derive(Clone)]
pub(crate) enum MaybeAssetLoader {
Ready(Arc<dyn ErasedAssetLoader>),
Pending {
sender: async_broadcast::Sender<Arc<dyn ErasedAssetLoader>>,
receiver: async_broadcast::Receiver<Arc<dyn ErasedAssetLoader>>,
},
}
impl MaybeAssetLoader {
pub(crate) async fn get(self) -> Result<Arc<dyn ErasedAssetLoader>, GetLoaderError> {
match self {
MaybeAssetLoader::Ready(loader) => Ok(loader),
MaybeAssetLoader::Pending { mut receiver, .. } => Ok(receiver.recv().await?),
}
}
}
#[cfg(test)]
mod tests {
use std::{
marker::PhantomData,
path::Path,
sync::mpsc::{channel, Receiver, Sender},
};
use bevy_reflect::TypePath;
use bevy_tasks::block_on;
use crate::{self as bevy_asset, Asset};
use super::*;
#[derive(Asset, TypePath, Debug)]
struct A(usize);
#[derive(Asset, TypePath, Debug)]
struct B(usize);
#[derive(Asset, TypePath, Debug)]
struct C(usize);
struct Loader<A: Asset, const N: usize, const E: usize> {
sender: Sender<()>,
_phantom: PhantomData<A>,
}
impl<T: Asset, const N: usize, const E: usize> Loader<T, N, E> {
fn new() -> (Self, Receiver<()>) {
let (tx, rx) = channel();
let loader = Self {
sender: tx,
_phantom: PhantomData,
};
(loader, rx)
}
}
impl<T: Asset, const N: usize, const E: usize> AssetLoader for Loader<T, N, E> {
type Asset = T;
type Settings = ();
type Error = String;
fn load<'a>(
&'a self,
_: &'a mut crate::io::Reader,
_: &'a Self::Settings,
_: &'a mut crate::LoadContext,
) -> bevy_utils::BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
self.sender.send(()).unwrap();
Box::pin(async move {
Err(format!(
"Loaded {}:{}",
std::any::type_name::<Self::Asset>(),
N
))
})
}
fn extensions(&self) -> &[&str] {
self.sender.send(()).unwrap();
match E {
1 => &["a"],
2 => &["b"],
3 => &["c"],
4 => &["d"],
_ => &[],
}
}
}
/// Basic framework for creating, storing, loading, and checking an [`AssetLoader`] inside an [`AssetLoaders`]
#[test]
fn basic() {
let mut loaders = AssetLoaders::default();
let (loader, rx) = Loader::<A, 1, 0>::new();
assert!(rx.try_recv().is_err());
loaders.push(loader);
assert!(rx.try_recv().is_ok());
assert!(rx.try_recv().is_err());
let loader = block_on(
loaders
.get_by_name(std::any::type_name::<Loader<A, 1, 0>>())
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx.try_recv().is_ok());
assert!(rx.try_recv().is_err());
}
/// Ensure that if multiple loaders have different types but no extensions, they can be found
#[test]
fn type_resolution() {
let mut loaders = AssetLoaders::default();
let (loader_a1, rx_a1) = Loader::<A, 1, 0>::new();
let (loader_b1, rx_b1) = Loader::<B, 1, 0>::new();
let (loader_c1, rx_c1) = Loader::<C, 1, 0>::new();
loaders.push(loader_a1);
loaders.push(loader_b1);
loaders.push(loader_c1);
assert!(rx_a1.try_recv().is_ok());
assert!(rx_b1.try_recv().is_ok());
assert!(rx_c1.try_recv().is_ok());
let loader = block_on(loaders.get_by_type(TypeId::of::<A>()).unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_ok());
assert!(rx_b1.try_recv().is_err());
assert!(rx_c1.try_recv().is_err());
let loader = block_on(loaders.get_by_type(TypeId::of::<B>()).unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_err());
assert!(rx_b1.try_recv().is_ok());
assert!(rx_c1.try_recv().is_err());
let loader = block_on(loaders.get_by_type(TypeId::of::<C>()).unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_err());
assert!(rx_b1.try_recv().is_err());
assert!(rx_c1.try_recv().is_ok());
}
/// Ensure that the last loader added is selected
#[test]
fn type_resolution_shadow() {
let mut loaders = AssetLoaders::default();
let (loader_a1, rx_a1) = Loader::<A, 1, 0>::new();
let (loader_a2, rx_a2) = Loader::<A, 2, 0>::new();
let (loader_a3, rx_a3) = Loader::<A, 3, 0>::new();
loaders.push(loader_a1);
loaders.push(loader_a2);
loaders.push(loader_a3);
assert!(rx_a1.try_recv().is_ok());
assert!(rx_a2.try_recv().is_ok());
assert!(rx_a3.try_recv().is_ok());
let loader = block_on(loaders.get_by_type(TypeId::of::<A>()).unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_err());
assert!(rx_a2.try_recv().is_err());
assert!(rx_a3.try_recv().is_ok());
}
/// Ensure that if multiple loaders have like types but differing extensions, they can be found
#[test]
fn extension_resolution() {
let mut loaders = AssetLoaders::default();
let (loader_a1, rx_a1) = Loader::<A, 1, 1>::new();
let (loader_b1, rx_b1) = Loader::<A, 1, 2>::new();
let (loader_c1, rx_c1) = Loader::<A, 1, 3>::new();
loaders.push(loader_a1);
loaders.push(loader_b1);
loaders.push(loader_c1);
assert!(rx_a1.try_recv().is_ok());
assert!(rx_b1.try_recv().is_ok());
assert!(rx_c1.try_recv().is_ok());
let loader = block_on(loaders.get_by_extension("a").unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_ok());
assert!(rx_b1.try_recv().is_err());
assert!(rx_c1.try_recv().is_err());
let loader = block_on(loaders.get_by_extension("b").unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_err());
assert!(rx_b1.try_recv().is_ok());
assert!(rx_c1.try_recv().is_err());
let loader = block_on(loaders.get_by_extension("c").unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_err());
assert!(rx_b1.try_recv().is_err());
assert!(rx_c1.try_recv().is_ok());
}
/// Ensure that if multiple loaders have like types but differing extensions, they can be found
#[test]
fn path_resolution() {
let mut loaders = AssetLoaders::default();
let (loader_a1, rx_a1) = Loader::<A, 1, 1>::new();
let (loader_b1, rx_b1) = Loader::<A, 1, 2>::new();
let (loader_c1, rx_c1) = Loader::<A, 1, 3>::new();
loaders.push(loader_a1);
loaders.push(loader_b1);
loaders.push(loader_c1);
assert!(rx_a1.try_recv().is_ok());
assert!(rx_b1.try_recv().is_ok());
assert!(rx_c1.try_recv().is_ok());
let path = AssetPath::from_path(Path::new("asset.a"));
let loader = block_on(loaders.get_by_path(&path).unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_ok());
assert!(rx_b1.try_recv().is_err());
assert!(rx_c1.try_recv().is_err());
let path = AssetPath::from_path(Path::new("asset.b"));
let loader = block_on(loaders.get_by_path(&path).unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_err());
assert!(rx_b1.try_recv().is_ok());
assert!(rx_c1.try_recv().is_err());
let path = AssetPath::from_path(Path::new("asset.c"));
let loader = block_on(loaders.get_by_path(&path).unwrap().get()).unwrap();
loader.extensions();
assert!(rx_a1.try_recv().is_err());
assert!(rx_b1.try_recv().is_err());
assert!(rx_c1.try_recv().is_ok());
}
/// Full resolution algorithm
#[test]
fn total_resolution() {
let mut loaders = AssetLoaders::default();
let (loader_a1_a, rx_a1_a) = Loader::<A, 1, 1>::new();
let (loader_b1_b, rx_b1_b) = Loader::<B, 1, 2>::new();
let (loader_c1_a, rx_c1_a) = Loader::<C, 1, 1>::new();
let (loader_c1_b, rx_c1_b) = Loader::<C, 1, 2>::new();
let (loader_c1_c, rx_c1_c) = Loader::<C, 1, 3>::new();
loaders.push(loader_a1_a);
loaders.push(loader_b1_b);
loaders.push(loader_c1_a);
loaders.push(loader_c1_b);
loaders.push(loader_c1_c);
assert!(rx_a1_a.try_recv().is_ok());
assert!(rx_b1_b.try_recv().is_ok());
assert!(rx_c1_a.try_recv().is_ok());
assert!(rx_c1_b.try_recv().is_ok());
assert!(rx_c1_c.try_recv().is_ok());
// Type and Extension agree
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<A>()),
None,
Some(&AssetPath::from_path(Path::new("asset.a"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_ok());
assert!(rx_b1_b.try_recv().is_err());
assert!(rx_c1_a.try_recv().is_err());
assert!(rx_c1_b.try_recv().is_err());
assert!(rx_c1_c.try_recv().is_err());
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<B>()),
None,
Some(&AssetPath::from_path(Path::new("asset.b"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_err());
assert!(rx_b1_b.try_recv().is_ok());
assert!(rx_c1_a.try_recv().is_err());
assert!(rx_c1_b.try_recv().is_err());
assert!(rx_c1_c.try_recv().is_err());
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<C>()),
None,
Some(&AssetPath::from_path(Path::new("asset.c"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_err());
assert!(rx_b1_b.try_recv().is_err());
assert!(rx_c1_a.try_recv().is_err());
assert!(rx_c1_b.try_recv().is_err());
assert!(rx_c1_c.try_recv().is_ok());
// Type should override Extension
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<C>()),
None,
Some(&AssetPath::from_path(Path::new("asset.a"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_err());
assert!(rx_b1_b.try_recv().is_err());
assert!(rx_c1_a.try_recv().is_ok());
assert!(rx_c1_b.try_recv().is_err());
assert!(rx_c1_c.try_recv().is_err());
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<C>()),
None,
Some(&AssetPath::from_path(Path::new("asset.b"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_err());
assert!(rx_b1_b.try_recv().is_err());
assert!(rx_c1_a.try_recv().is_err());
assert!(rx_c1_b.try_recv().is_ok());
assert!(rx_c1_c.try_recv().is_err());
// Type should override bad / missing extension
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<A>()),
None,
Some(&AssetPath::from_path(Path::new("asset.x"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_ok());
assert!(rx_b1_b.try_recv().is_err());
assert!(rx_c1_a.try_recv().is_err());
assert!(rx_c1_b.try_recv().is_err());
assert!(rx_c1_c.try_recv().is_err());
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<A>()),
None,
Some(&AssetPath::from_path(Path::new("asset"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_ok());
assert!(rx_b1_b.try_recv().is_err());
assert!(rx_c1_a.try_recv().is_err());
assert!(rx_c1_b.try_recv().is_err());
assert!(rx_c1_c.try_recv().is_err());
}
/// Ensure that if there is a complete ambiguity in [`AssetLoader`] to use, prefer most recently registered by asset type.
#[test]
fn ambiguity_resolution() {
let mut loaders = AssetLoaders::default();
let (loader_a1_a, rx_a1_a) = Loader::<A, 1, 1>::new();
let (loader_a2_a, rx_a2_a) = Loader::<A, 2, 1>::new();
let (loader_a3_a, rx_a3_a) = Loader::<A, 3, 1>::new();
loaders.push(loader_a1_a);
loaders.push(loader_a2_a);
loaders.push(loader_a3_a);
assert!(rx_a1_a.try_recv().is_ok());
assert!(rx_a2_a.try_recv().is_ok());
assert!(rx_a3_a.try_recv().is_ok());
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<A>()),
None,
Some(&AssetPath::from_path(Path::new("asset.a"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_err());
assert!(rx_a2_a.try_recv().is_err());
assert!(rx_a3_a.try_recv().is_ok());
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<A>()),
None,
Some(&AssetPath::from_path(Path::new("asset.x"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_err());
assert!(rx_a2_a.try_recv().is_err());
assert!(rx_a3_a.try_recv().is_ok());
let loader = block_on(
loaders
.find(
None,
Some(TypeId::of::<A>()),
None,
Some(&AssetPath::from_path(Path::new("asset"))),
)
.unwrap()
.get(),
)
.unwrap();
loader.extensions();
assert!(rx_a1_a.try_recv().is_err());
assert!(rx_a2_a.try_recv().is_err());
assert!(rx_a3_a.try_recv().is_ok());
}
}

View file

@ -1,4 +1,5 @@
mod info;
mod loaders;
use crate::{
folder::LoadedFolder,
@ -17,12 +18,13 @@ use crate::{
UntypedAssetLoadFailedEvent, UntypedHandle,
};
use bevy_ecs::prelude::*;
use bevy_log::{error, info, warn};
use bevy_log::{error, info};
use bevy_tasks::IoTaskPool;
use bevy_utils::{CowArc, HashMap, HashSet, TypeIdMap};
use bevy_utils::{CowArc, HashSet};
use crossbeam_channel::{Receiver, Sender};
use futures_lite::StreamExt;
use info::*;
use loaders::*;
use parking_lot::RwLock;
use std::path::PathBuf;
use std::{any::TypeId, path::Path, sync::Arc};
@ -135,42 +137,7 @@ impl AssetServer {
/// Registers a new [`AssetLoader`]. [`AssetLoader`]s must be registered before they can be used.
pub fn register_loader<L: AssetLoader>(&self, loader: L) {
let mut loaders = self.data.loaders.write();
let type_name = std::any::type_name::<L>();
let loader = Arc::new(loader);
let (loader_index, is_new) =
if let Some(index) = loaders.preregistered_loaders.remove(type_name) {
(index, false)
} else {
(TypeId::of::<L::Asset>(), true)
};
for extension in loader.extensions() {
loaders
.extension_to_type_id
.insert(extension.to_string(), loader_index);
}
if is_new {
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(
loaders.type_id_to_loader.get_mut(&loader_index).unwrap(),
MaybeAssetLoader::Ready(loader.clone()),
);
match maybe_loader {
MaybeAssetLoader::Ready(_) => unreachable!(),
MaybeAssetLoader::Pending { sender, .. } => {
IoTaskPool::get()
.spawn(async move {
let _ = sender.broadcast(loader).await;
})
.detach();
}
}
}
self.data.loaders.write().push(loader);
}
/// Registers a new [`Asset`] type. [`Asset`] types must be registered before assets of that type can be loaded.
@ -219,20 +186,13 @@ impl AssetServer {
&self,
extension: &str,
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForExtensionError> {
let loader = {
let loaders = self.data.loaders.read();
let index = *loaders.extension_to_type_id.get(extension).ok_or_else(|| {
MissingAssetLoaderForExtensionError {
extensions: vec![extension.to_string()],
}
})?;
loaders.type_id_to_loader[&index].clone()
let error = || MissingAssetLoaderForExtensionError {
extensions: vec![extension.to_string()],
};
match loader {
MaybeAssetLoader::Ready(loader) => Ok(loader),
MaybeAssetLoader::Pending { mut receiver, .. } => Ok(receiver.recv().await.unwrap()),
}
let loader = { self.data.loaders.read().get_by_extension(extension) };
loader.ok_or_else(error)?.get().await.map_err(|_| error())
}
/// Returns the registered [`AssetLoader`] associated with the given [`std::any::type_name`], if it exists.
@ -240,20 +200,13 @@ impl AssetServer {
&self,
type_name: &str,
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForTypeNameError> {
let loader = {
let loaders = self.data.loaders.read();
let index = *loaders.type_name_to_type_id.get(type_name).ok_or_else(|| {
MissingAssetLoaderForTypeNameError {
type_name: type_name.to_string(),
}
})?;
loaders.type_id_to_loader[&index].clone()
let error = || MissingAssetLoaderForTypeNameError {
type_name: type_name.to_string(),
};
match loader {
MaybeAssetLoader::Ready(loader) => Ok(loader),
MaybeAssetLoader::Pending { mut receiver, .. } => Ok(receiver.recv().await.unwrap()),
}
let loader = { self.data.loaders.read().get_by_name(type_name) };
loader.ok_or_else(error)?.get().await.map_err(|_| error())
}
/// Retrieves the default [`AssetLoader`] for the given path, if one can be found.
@ -262,23 +215,25 @@ impl AssetServer {
path: impl Into<AssetPath<'a>>,
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForExtensionError> {
let path = path.into();
let full_extension =
path.get_full_extension()
.ok_or(MissingAssetLoaderForExtensionError {
let error = || {
let Some(full_extension) = path.get_full_extension() else {
return MissingAssetLoaderForExtensionError {
extensions: Vec::new(),
})?;
if let Ok(loader) = self.get_asset_loader_with_extension(&full_extension).await {
return Ok(loader);
}
for extension in AssetPath::iter_secondary_extensions(&full_extension) {
if let Ok(loader) = self.get_asset_loader_with_extension(extension).await {
return Ok(loader);
}
}
let mut extensions = vec![full_extension.clone()];
extensions
.extend(AssetPath::iter_secondary_extensions(&full_extension).map(|e| e.to_string()));
Err(MissingAssetLoaderForExtensionError { extensions })
};
};
let mut extensions = vec![full_extension.clone()];
extensions.extend(
AssetPath::iter_secondary_extensions(&full_extension).map(|e| e.to_string()),
);
MissingAssetLoaderForExtensionError { extensions }
};
let loader = { self.data.loaders.read().get_by_path(&path) };
loader.ok_or_else(error)?.get().await.map_err(|_| error())
}
/// Retrieves the default [`AssetLoader`] for the given [`Asset`] [`TypeId`], if one can be found.
@ -286,19 +241,11 @@ impl AssetServer {
&self,
type_id: TypeId,
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForTypeIdError> {
let loader = {
let loaders = self.data.loaders.read();
loaders
.type_id_to_loader
.get(&type_id)
.ok_or(MissingAssetLoaderForTypeIdError { type_id })?
.clone()
};
let error = || MissingAssetLoaderForTypeIdError { type_id };
match loader {
MaybeAssetLoader::Ready(loader) => Ok(loader),
MaybeAssetLoader::Pending { mut receiver, .. } => Ok(receiver.recv().await.unwrap()),
}
let loader = { self.data.loaders.read().get_by_type(type_id) };
loader.ok_or_else(error)?.get().await.map_err(|_| error())
}
/// Retrieves the default [`AssetLoader`] for the given [`Asset`] type, if one can be found.
@ -937,27 +884,7 @@ impl AssetServer {
/// Assets loaded with matching extensions will be blocked until the
/// real loader is added.
pub fn preregister_loader<L: AssetLoader>(&self, extensions: &[&str]) {
let mut loaders = self.data.loaders.write();
let loader_index = TypeId::of::<L::Asset>();
let type_name = std::any::type_name::<L>();
loaders
.preregistered_loaders
.insert(type_name, loader_index);
loaders.type_name_to_type_id.insert(type_name, loader_index);
for extension in extensions {
if loaders
.extension_to_type_id
.insert(extension.to_string(), loader_index)
.is_some()
{
warn!("duplicate preregistration for `{extension}`, any assets loaded with the previous loader will never complete.");
}
}
let (mut sender, receiver) = async_broadcast::broadcast(1);
sender.set_overflow(true);
loaders
.type_id_to_loader
.insert(loader_index, MaybeAssetLoader::Pending { sender, receiver });
self.data.loaders.write().reserve::<L>(extensions);
}
/// Retrieve a handle for the given path. This will create a handle (and [`AssetInfo`]) if it does not exist
@ -1039,7 +966,22 @@ impl AssetServer {
Ok((meta, loader, reader))
}
Err(AssetReaderError::NotFound(_)) => {
let loader = self.resolve_loader(asset_path, asset_type_id).await?;
// TODO: Handle error transformation
let loader = {
self.data
.loaders
.read()
.find(None, asset_type_id, None, Some(asset_path))
};
let error = || AssetLoadError::MissingAssetLoader {
loader_name: None,
asset_type_id,
extension: None,
asset_path: Some(asset_path.to_string()),
};
let loader = loader.ok_or_else(error)?.get().await.map_err(|_| error())?;
let meta = loader.default_meta();
Ok((meta, loader, reader))
@ -1047,50 +989,27 @@ impl AssetServer {
Err(err) => Err(err.into()),
}
} else {
let loader = self.resolve_loader(asset_path, asset_type_id).await?;
let loader = {
self.data
.loaders
.read()
.find(None, asset_type_id, None, Some(asset_path))
};
let error = || AssetLoadError::MissingAssetLoader {
loader_name: None,
asset_type_id,
extension: None,
asset_path: Some(asset_path.to_string()),
};
let loader = loader.ok_or_else(error)?.get().await.map_err(|_| error())?;
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<TypeId>,
) -> Result<Arc<dyn ErasedAssetLoader>, 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<'_>,
@ -1236,23 +1155,6 @@ pub fn handle_internal_asset_events(world: &mut World) {
});
}
#[derive(Default)]
pub(crate) struct AssetLoaders {
type_id_to_loader: TypeIdMap<MaybeAssetLoader>,
extension_to_type_id: HashMap<String, TypeId>,
type_name_to_type_id: HashMap<&'static str, TypeId>,
preregistered_loaders: HashMap<&'static str, TypeId>,
}
#[derive(Clone)]
enum MaybeAssetLoader {
Ready(Arc<dyn ErasedAssetLoader>),
Pending {
sender: async_broadcast::Sender<Arc<dyn ErasedAssetLoader>>,
receiver: async_broadcast::Receiver<Arc<dyn ErasedAssetLoader>>,
},
}
/// Internal events for asset load results
#[allow(clippy::large_enum_variant)]
pub(crate) enum InternalAssetEvent {
@ -1319,6 +1221,13 @@ pub enum AssetLoadError {
actual_asset_name: &'static str,
loader_name: &'static str,
},
#[error("Could not find an asset loader matching: Loader Name: {loader_name:?}; Asset Type: {loader_name:?}; Extension: {extension:?}; Path: {asset_path:?};")]
MissingAssetLoader {
loader_name: Option<String>,
asset_type_id: Option<TypeId>,
extension: Option<String>,
asset_path: Option<String>,
},
#[error(transparent)]
MissingAssetLoaderForExtension(#[from] MissingAssetLoaderForExtensionError),
#[error(transparent)]