mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +00:00
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:
parent
16d28ccb91
commit
afa7b5cba5
6 changed files with 336 additions and 57 deletions
3
assets/data/asset_no_extension
Normal file
3
assets/data/asset_no_extension
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
CustomAsset (
|
||||||
|
value: 13
|
||||||
|
)
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(map) = path_to_id.get_mut(path) {
|
||||||
|
map.remove(&type_id);
|
||||||
|
|
||||||
|
if map.is_empty() {
|
||||||
path_to_id.remove(path);
|
path_to_id.remove(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue