Added Support for Extension-less Assets (#10153)

# Objective

- Addresses **Support processing and loading files without extensions**
from #9714
- Addresses **More runtime loading configuration** from #9714
- Fixes #367
- Fixes #10703

## Solution

`AssetServer::load::<A>` and `AssetServer::load_with_settings::<A>` can
now use the `Asset` type parameter `A` to select a registered
`AssetLoader` without inspecting the provided `AssetPath`. This change
cascades onto `LoadContext::load` and `LoadContext::load_with_settings`.
This allows the loading of assets which have incorrect or ambiguous file
extensions.

```rust
// Allow the type to be inferred by context
let handle = asset_server.load("data/asset_no_extension");

// Hint the type through the handle
let handle: Handle<CustomAsset> = asset_server.load("data/asset_no_extension");

// Explicit through turbofish
let handle = asset_server.load::<CustomAsset>("data/asset_no_extension");
```

Since a single `AssetPath` no longer maps 1:1 with an `Asset`, I've also
modified how assets are loaded to permit multiple asset types to be
loaded from a single path. This allows for two different `AssetLoaders`
(which return different types of assets) to both load a single path (if
requested).

```rust
// Uses GltfLoader
let model = asset_server.load::<Gltf>("cube.gltf");

// Hypothetical Blob loader for data transmission (for example)
let blob = asset_server.load::<Blob>("cube.gltf");
```

As these changes are reflected in the `LoadContext` as well as the
`AssetServer`, custom `AssetLoaders` can also take advantage of this
behaviour to create more complex assets.

---

## Change Log

- Updated `custom_asset` example to demonstrate extension-less assets.
- Added `AssetServer::get_handles_untyped` and Added
`AssetServer::get_path_ids`

## Notes

As a part of that refactor, I chose to store `AssetLoader`s (within
`AssetLoaders`) using a `HashMap<TypeId, ...>` instead of a `Vec<...>`.
My reasoning for this was I needed to add a relationship between `Asset`
`TypeId`s and the `AssetLoader`, so instead of having a `Vec` and a
`HashMap`, I combined the two, removing the `usize` index from the
adjacent maps.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Zachary Harrold 2024-02-01 01:58:08 +11:00 committed by GitHub
parent 16d28ccb91
commit afa7b5cba5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 336 additions and 57 deletions

View file

@ -0,0 +1,3 @@
CustomAsset (
value: 13
)

View file

@ -267,10 +267,7 @@ impl AsyncRead for VecReader {
/// Appends `.meta` to the given path. /// Appends `.meta` to the given path.
pub(crate) fn get_meta_path(path: &Path) -> PathBuf { pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
let mut meta_path = path.to_path_buf(); let mut meta_path = path.to_path_buf();
let mut extension = path let mut extension = path.extension().unwrap_or_default().to_os_string();
.extension()
.unwrap_or_else(|| panic!("missing extension for asset path {path:?}"))
.to_os_string();
extension.push(".meta"); extension.push(".meta");
meta_path.set_extension(extension); meta_path.set_extension(extension);
meta_path meta_path

View file

@ -37,8 +37,10 @@ pub trait AssetLoader: Send + Sync + 'static {
load_context: &'a mut LoadContext, load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>>; ) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>>;
/// Returns a list of extensions supported by this asset loader, without the preceding dot. /// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot.
fn extensions(&self) -> &[&str]; fn extensions(&self) -> &[&str] {
&[]
}
} }
/// Provides type-erased access to an [`AssetLoader`]. /// Provides type-erased access to an [`AssetLoader`].
@ -396,7 +398,7 @@ impl<'a> LoadContext<'a> {
/// See [`AssetPath`] for more on labeled assets. /// See [`AssetPath`] for more on labeled assets.
pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool { pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
let path = self.asset_path.clone().with_label(label.into()); 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). /// "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 loaded_asset = {
let (meta, loader, mut reader) = self let (meta, loader, mut reader) = self
.asset_server .asset_server
.get_meta_loader_and_reader(&path) .get_meta_loader_and_reader(&path, None)
.await .await
.map_err(to_error)?; .map_err(to_error)?;
self.asset_server self.asset_server

View file

@ -61,7 +61,7 @@ impl AssetInfo {
#[derive(Default)] #[derive(Default)]
pub(crate) struct AssetInfos { pub(crate) struct AssetInfos {
path_to_id: HashMap<AssetPath<'static>, UntypedAssetId>, path_to_id: HashMap<AssetPath<'static>, HashMap<TypeId, UntypedAssetId>>,
infos: HashMap<UntypedAssetId, AssetInfo>, infos: HashMap<UntypedAssetId, AssetInfo>,
/// If set to `true`, this informs [`AssetInfos`] to track data relevant to watching for changes (such as `load_dependants`) /// 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. /// This should only be set at startup.
@ -191,7 +191,20 @@ impl AssetInfos {
loading_mode: HandleLoadingMode, loading_mode: HandleLoadingMode,
meta_transform: Option<MetaTransform>, meta_transform: Option<MetaTransform>,
) -> Result<(UntypedHandle, bool), GetOrCreateHandleInternalError> { ) -> 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) => { Entry::Occupied(entry) => {
let id = *entry.get(); let id = *entry.get();
// if there is a path_to_id entry, info always exists // 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 // 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 // strong handle doesn't remove the asset from the Assets collection
info.handle_drops_to_skip += 1; info.handle_drops_to_skip += 1;
let type_id = type_id.ok_or(
GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified,
)?;
let provider = self let provider = self
.handle_providers .handle_providers
.get(&type_id) .get(&type_id)
@ -241,8 +251,6 @@ impl AssetInfos {
HandleLoadingMode::NotLoading => false, HandleLoadingMode::NotLoading => false,
HandleLoadingMode::Request | HandleLoadingMode::Force => true, HandleLoadingMode::Request | HandleLoadingMode::Force => true,
}; };
let type_id = type_id
.ok_or(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified)?;
let handle = Self::create_handle_internal( let handle = Self::create_handle_internal(
&mut self.infos, &mut self.infos,
&self.handle_providers, &self.handle_providers,
@ -271,13 +279,52 @@ impl AssetInfos {
self.infos.get_mut(&id) self.infos.get_mut(&id)
} }
pub(crate) fn get_path_id(&self, path: &AssetPath) -> Option<UntypedAssetId> { pub(crate) fn get_path_and_type_id_handle(
self.path_to_id.get(path).copied() &self,
path: &AssetPath,
type_id: TypeId,
) -> Option<UntypedHandle> {
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<UntypedHandle> { pub(crate) fn get_path_ids<'a>(
let id = *self.path_to_id.get(path)?; &'a self,
self.get_id_handle(id) path: &'a AssetPath<'a>,
) -> impl Iterator<Item = UntypedAssetId> + 'a {
/// Concrete type to allow returning an `impl Iterator` even if `self.path_to_id.get(&path)` is `None`
enum HandlesByPathIterator<T> {
None,
Some(T),
}
impl<T> Iterator for HandlesByPathIterator<T>
where
T: Iterator<Item = UntypedAssetId>,
{
type Item = UntypedAssetId;
fn next(&mut self) -> Option<Self::Item> {
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<Item = UntypedHandle> + 'a {
self.get_path_ids(path)
.filter_map(|id| self.get_id_handle(id))
} }
pub(crate) fn get_id_handle(&self, id: UntypedAssetId) -> Option<UntypedHandle> { pub(crate) fn get_id_handle(&self, id: UntypedAssetId) -> Option<UntypedHandle> {
@ -289,12 +336,13 @@ impl AssetInfos {
/// Returns `true` if the asset this path points to is still alive /// Returns `true` if the asset this path points to is still alive
pub(crate) fn is_path_alive<'a>(&self, path: impl Into<AssetPath<'a>>) -> bool { pub(crate) fn is_path_alive<'a>(&self, path: impl Into<AssetPath<'a>>) -> bool {
let path = path.into(); let path = path.into();
if let Some(id) = self.path_to_id.get(&path) {
if let Some(info) = self.infos.get(id) { let result = self
return info.weak_handle.strong_count() > 0; .get_path_ids(&path)
} .filter_map(|id| self.infos.get(&id))
} .any(|info| info.weak_handle.strong_count() > 0);
false
result
} }
/// Returns `true` if the asset at this path should be reloaded /// Returns `true` if the asset at this path should be reloaded
@ -592,7 +640,7 @@ impl AssetInfos {
fn process_handle_drop_internal( fn process_handle_drop_internal(
infos: &mut HashMap<UntypedAssetId, AssetInfo>, infos: &mut HashMap<UntypedAssetId, AssetInfo>,
path_to_id: &mut HashMap<AssetPath<'static>, UntypedAssetId>, path_to_id: &mut HashMap<AssetPath<'static>, HashMap<TypeId, UntypedAssetId>>,
loader_dependants: &mut HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>, loader_dependants: &mut HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>,
living_labeled_assets: &mut HashMap<AssetPath<'static>, HashSet<String>>, living_labeled_assets: &mut HashMap<AssetPath<'static>, HashSet<String>>,
watching_for_changes: bool, watching_for_changes: bool,
@ -609,6 +657,8 @@ impl AssetInfos {
return false; return false;
} }
let type_id = entry.key().type_id();
let info = entry.remove(); let info = entry.remove();
let Some(path) = &info.path else { let Some(path) = &info.path else {
return true; return true;
@ -622,7 +672,15 @@ impl AssetInfos {
living_labeled_assets, 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 true
} }

View file

@ -142,20 +142,22 @@ impl AssetServer {
if let Some(index) = loaders.preregistered_loaders.remove(type_name) { if let Some(index) = loaders.preregistered_loaders.remove(type_name) {
(index, false) (index, false)
} else { } else {
(loaders.values.len(), true) (TypeId::of::<L::Asset>(), true)
}; };
for extension in loader.extensions() { for extension in loader.extensions() {
loaders loaders
.extension_to_index .extension_to_type_id
.insert(extension.to_string(), loader_index); .insert(extension.to_string(), loader_index);
} }
if is_new { if is_new {
loaders.type_name_to_index.insert(type_name, loader_index); loaders.type_name_to_type_id.insert(type_name, loader_index);
loaders.values.push(MaybeAssetLoader::Ready(loader)); loaders
.type_id_to_loader
.insert(loader_index, MaybeAssetLoader::Ready(loader));
} else { } else {
let maybe_loader = std::mem::replace( 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()), MaybeAssetLoader::Ready(loader.clone()),
); );
match maybe_loader { match maybe_loader {
@ -219,12 +221,12 @@ impl AssetServer {
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForExtensionError> { ) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForExtensionError> {
let loader = { let loader = {
let loaders = self.data.loaders.read(); 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 { MissingAssetLoaderForExtensionError {
extensions: vec![extension.to_string()], extensions: vec![extension.to_string()],
} }
})?; })?;
loaders.values[index].clone() loaders.type_id_to_loader[&index].clone()
}; };
match loader { match loader {
@ -240,13 +242,13 @@ impl AssetServer {
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForTypeNameError> { ) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForTypeNameError> {
let loader = { let loader = {
let loaders = self.data.loaders.read(); 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 { MissingAssetLoaderForTypeNameError {
type_name: type_name.to_string(), type_name: type_name.to_string(),
} }
})?; })?;
loaders.values[index].clone() loaders.type_id_to_loader[&index].clone()
}; };
match loader { match loader {
MaybeAssetLoader::Ready(loader) => Ok(loader), MaybeAssetLoader::Ready(loader) => Ok(loader),
@ -279,6 +281,34 @@ impl AssetServer {
Err(MissingAssetLoaderForExtensionError { extensions }) 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<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()
};
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<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForTypeIdError> {
self.get_asset_loader_with_asset_type_id(TypeId::of::<A>())
.await
}
/// Begins loading an [`Asset`] of type `A` stored at `path`. This will not block on the asset load. Instead, /// 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 /// it returns a "strong" [`Handle`]. When the [`Asset`] is loaded (and enters [`LoadState::Loaded`]), it will be added to the
/// associated [`Assets`] resource. /// associated [`Assets`] resource.
@ -427,10 +457,12 @@ impl AssetServer {
force: bool, force: bool,
meta_transform: Option<MetaTransform>, meta_transform: Option<MetaTransform>,
) -> Result<UntypedHandle, AssetLoadError> { ) -> Result<UntypedHandle, AssetLoadError> {
let asset_type_id = input_handle.as_ref().map(|handle| handle.type_id());
let path = path.into_owned(); let path = path.into_owned();
let path_clone = path.clone(); let path_clone = path.clone();
let (mut meta, loader, mut reader) = self 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 .await
.map_err(|e| { .map_err(|e| {
// if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if // 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 { let handle = if let Some((handle, should_load)) = handle_result {
if path.label().is_none() && handle.type_id() != loader.asset_type_id() { 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 { return Err(AssetLoadError::RequestedHandleTypeMismatch {
path: path.into_owned(), path: path.into_owned(),
requested: handle.type_id(), requested: handle.type_id(),
@ -569,7 +606,24 @@ impl AssetServer {
let path = path.into().into_owned(); let path = path.into().into_owned();
IoTaskPool::get() IoTaskPool::get()
.spawn(async move { .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::<Vec<_>>();
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 { if let Err(err) = server.load_internal(None, path, true, None).await {
error!("{}", err); 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, /// Returns an active handle for the given path, if the asset at the given path has already started loading,
/// or is still "alive". /// or is still "alive".
pub fn get_handle<'a, A: Asset>(&self, path: impl Into<AssetPath<'a>>) -> Option<Handle<A>> { pub fn get_handle<'a, A: Asset>(&self, path: impl Into<AssetPath<'a>>) -> Option<Handle<A>> {
self.get_handle_untyped(path) self.get_path_and_type_id_handle(&path.into(), TypeId::of::<A>())
.map(|h| h.typed_debug_checked()) .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, /// 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". /// 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<AssetPath<'a>>) -> Option<UntypedAssetId> { pub fn get_path_id<'a>(&self, path: impl Into<AssetPath<'a>>) -> Option<UntypedAssetId> {
let infos = self.data.infos.read(); let infos = self.data.infos.read();
let path = path.into(); 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<AssetPath<'a>>) -> Vec<UntypedAssetId> {
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, /// Returns an active untyped handle for the given path, if the asset at the given path has already started loading,
/// or is still "alive". /// 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<AssetPath<'a>>) -> Option<UntypedHandle> { pub fn get_handle_untyped<'a>(&self, path: impl Into<AssetPath<'a>>) -> Option<UntypedHandle> {
let infos = self.data.infos.read(); let infos = self.data.infos.read();
let path = path.into(); 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<AssetPath<'a>>) -> Vec<UntypedHandle> {
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<UntypedHandle> {
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. /// Returns the path for the given `id`, if it has one.
@ -844,15 +938,15 @@ impl AssetServer {
/// real loader is added. /// real loader is added.
pub fn preregister_loader<L: AssetLoader>(&self, extensions: &[&str]) { pub fn preregister_loader<L: AssetLoader>(&self, extensions: &[&str]) {
let mut loaders = self.data.loaders.write(); let mut loaders = self.data.loaders.write();
let loader_index = loaders.values.len(); let loader_index = TypeId::of::<L::Asset>();
let type_name = std::any::type_name::<L>(); let type_name = std::any::type_name::<L>();
loaders loaders
.preregistered_loaders .preregistered_loaders
.insert(type_name, loader_index); .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 { for extension in extensions {
if loaders if loaders
.extension_to_index .extension_to_type_id
.insert(extension.to_string(), loader_index) .insert(extension.to_string(), loader_index)
.is_some() .is_some()
{ {
@ -862,8 +956,8 @@ impl AssetServer {
let (mut sender, receiver) = async_broadcast::broadcast(1); let (mut sender, receiver) = async_broadcast::broadcast(1);
sender.set_overflow(true); sender.set_overflow(true);
loaders loaders
.values .type_id_to_loader
.push(MaybeAssetLoader::Pending { sender, receiver }); .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 /// 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>( pub(crate) async fn get_meta_loader_and_reader<'a>(
&'a self, &'a self,
asset_path: &'a AssetPath<'_>, asset_path: &'a AssetPath<'_>,
asset_type_id: Option<TypeId>,
) -> Result< ) -> Result<
( (
Box<dyn AssetMetaDyn>, Box<dyn AssetMetaDyn>,
@ -944,19 +1039,58 @@ impl AssetServer {
Ok((meta, loader, reader)) Ok((meta, loader, reader))
} }
Err(AssetReaderError::NotFound(_)) => { 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(); let meta = loader.default_meta();
Ok((meta, loader, reader)) Ok((meta, loader, reader))
} }
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
} }
} else { } 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(); let meta = loader.default_meta();
Ok((meta, loader, reader)) 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( pub(crate) async fn load_with_meta_loader_and_reader(
&self, &self,
asset_path: &AssetPath<'_>, asset_path: &AssetPath<'_>,
@ -1045,9 +1179,9 @@ pub fn handle_internal_asset_events(world: &mut World) {
current_folder = parent.to_path_buf(); current_folder = parent.to_path_buf();
let parent_asset_path = let parent_asset_path =
AssetPath::from(current_folder.clone()).with_source(source.clone()); 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"); 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)] #[derive(Default)]
pub(crate) struct AssetLoaders { pub(crate) struct AssetLoaders {
values: Vec<MaybeAssetLoader>, type_id_to_loader: HashMap<TypeId, MaybeAssetLoader>,
extension_to_index: HashMap<String, usize>, extension_to_type_id: HashMap<String, TypeId>,
type_name_to_index: HashMap<&'static str, usize>, type_name_to_type_id: HashMap<&'static str, TypeId>,
preregistered_loaders: HashMap<&'static str, usize>, preregistered_loaders: HashMap<&'static str, TypeId>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -1190,6 +1324,8 @@ pub enum AssetLoadError {
#[error(transparent)] #[error(transparent)]
MissingAssetLoaderForTypeName(#[from] MissingAssetLoaderForTypeNameError), MissingAssetLoaderForTypeName(#[from] MissingAssetLoaderForTypeNameError),
#[error(transparent)] #[error(transparent)]
MissingAssetLoaderForTypeIdError(#[from] MissingAssetLoaderForTypeIdError),
#[error(transparent)]
AssetReaderError(#[from] AssetReaderError), AssetReaderError(#[from] AssetReaderError),
#[error(transparent)] #[error(transparent)]
MissingAssetSourceError(#[from] MissingAssetSourceError), MissingAssetSourceError(#[from] MissingAssetSourceError),
@ -1238,6 +1374,13 @@ pub struct MissingAssetLoaderForTypeNameError {
type_name: String, 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 { fn format_missing_asset_ext(exts: &[String]) -> String {
if !exts.is_empty() { if !exts.is_empty() {
format!( format!(

View file

@ -53,12 +53,52 @@ impl AssetLoader for CustomAssetLoader {
} }
} }
#[derive(Asset, TypePath, Debug)]
pub struct Blob {
pub bytes: Vec<u8>,
}
#[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<Self::Asset, Self::Error>> {
Box::pin(async move {
info!("Loading Blob...");
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
Ok(Blob { bytes })
})
}
}
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.init_resource::<State>() .init_resource::<State>()
.init_asset::<CustomAsset>() .init_asset::<CustomAsset>()
.init_asset::<Blob>()
.init_asset_loader::<CustomAssetLoader>() .init_asset_loader::<CustomAssetLoader>()
.init_asset_loader::<BlobAssetLoader>()
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, print_on_load) .add_systems(Update, print_on_load)
.run(); .run();
@ -67,19 +107,55 @@ fn main() {
#[derive(Resource, Default)] #[derive(Resource, Default)]
struct State { struct State {
handle: Handle<CustomAsset>, handle: Handle<CustomAsset>,
other_handle: Handle<CustomAsset>,
blob: Handle<Blob>,
printed: bool, printed: bool,
} }
fn setup(mut state: ResMut<State>, asset_server: Res<AssetServer>) { fn setup(mut state: ResMut<State>, asset_server: Res<AssetServer>) {
// Recommended way to load an asset
state.handle = asset_server.load("data/asset.custom"); 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<State>, custom_assets: Res<Assets<CustomAsset>>) { fn print_on_load(
mut state: ResMut<State>,
custom_assets: Res<Assets<CustomAsset>>,
blob_assets: Res<Assets<Blob>>,
) {
let custom_asset = custom_assets.get(&state.handle); 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; return;
} }
info!("Custom asset loaded: {:?}", custom_asset.unwrap()); 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; state.printed = true;
} }