Skip to content

Commit afa7b5c

Browse files
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 <[email protected]>
1 parent 16d28cc commit afa7b5c

File tree

6 files changed

+336
-57
lines changed

6 files changed

+336
-57
lines changed

assets/data/asset_no_extension

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CustomAsset (
2+
value: 13
3+
)

crates/bevy_asset/src/io/mod.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,7 @@ impl AsyncRead for VecReader {
267267
/// Appends `.meta` to the given path.
268268
pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
269269
let mut meta_path = path.to_path_buf();
270-
let mut extension = path
271-
.extension()
272-
.unwrap_or_else(|| panic!("missing extension for asset path {path:?}"))
273-
.to_os_string();
270+
let mut extension = path.extension().unwrap_or_default().to_os_string();
274271
extension.push(".meta");
275272
meta_path.set_extension(extension);
276273
meta_path

crates/bevy_asset/src/loader.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ pub trait AssetLoader: Send + Sync + 'static {
3737
load_context: &'a mut LoadContext,
3838
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>>;
3939

40-
/// Returns a list of extensions supported by this asset loader, without the preceding dot.
41-
fn extensions(&self) -> &[&str];
40+
/// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot.
41+
fn extensions(&self) -> &[&str] {
42+
&[]
43+
}
4244
}
4345

4446
/// Provides type-erased access to an [`AssetLoader`].
@@ -396,7 +398,7 @@ impl<'a> LoadContext<'a> {
396398
/// See [`AssetPath`] for more on labeled assets.
397399
pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
398400
let path = self.asset_path.clone().with_label(label.into());
399-
self.asset_server.get_handle_untyped(&path).is_some()
401+
!self.asset_server.get_handles_untyped(&path).is_empty()
400402
}
401403

402404
/// "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> {
546548
let loaded_asset = {
547549
let (meta, loader, mut reader) = self
548550
.asset_server
549-
.get_meta_loader_and_reader(&path)
551+
.get_meta_loader_and_reader(&path, None)
550552
.await
551553
.map_err(to_error)?;
552554
self.asset_server

crates/bevy_asset/src/server/info.rs

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ impl AssetInfo {
6161

6262
#[derive(Default)]
6363
pub(crate) struct AssetInfos {
64-
path_to_id: HashMap<AssetPath<'static>, UntypedAssetId>,
64+
path_to_id: HashMap<AssetPath<'static>, HashMap<TypeId, UntypedAssetId>>,
6565
infos: HashMap<UntypedAssetId, AssetInfo>,
6666
/// If set to `true`, this informs [`AssetInfos`] to track data relevant to watching for changes (such as `load_dependants`)
6767
/// This should only be set at startup.
@@ -191,7 +191,20 @@ impl AssetInfos {
191191
loading_mode: HandleLoadingMode,
192192
meta_transform: Option<MetaTransform>,
193193
) -> Result<(UntypedHandle, bool), GetOrCreateHandleInternalError> {
194-
match self.path_to_id.entry(path.clone()) {
194+
let handles = self.path_to_id.entry(path.clone()).or_default();
195+
196+
let type_id = type_id
197+
.or_else(|| {
198+
// If a TypeId is not provided, we may be able to infer it if only a single entry exists
199+
if handles.len() == 1 {
200+
Some(*handles.keys().next().unwrap())
201+
} else {
202+
None
203+
}
204+
})
205+
.ok_or(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified)?;
206+
207+
match handles.entry(type_id) {
195208
Entry::Occupied(entry) => {
196209
let id = *entry.get();
197210
// if there is a path_to_id entry, info always exists
@@ -222,9 +235,6 @@ impl AssetInfos {
222235
// We must create a new strong handle for the existing id and ensure that the drop of the old
223236
// strong handle doesn't remove the asset from the Assets collection
224237
info.handle_drops_to_skip += 1;
225-
let type_id = type_id.ok_or(
226-
GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified,
227-
)?;
228238
let provider = self
229239
.handle_providers
230240
.get(&type_id)
@@ -241,8 +251,6 @@ impl AssetInfos {
241251
HandleLoadingMode::NotLoading => false,
242252
HandleLoadingMode::Request | HandleLoadingMode::Force => true,
243253
};
244-
let type_id = type_id
245-
.ok_or(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified)?;
246254
let handle = Self::create_handle_internal(
247255
&mut self.infos,
248256
&self.handle_providers,
@@ -271,13 +279,52 @@ impl AssetInfos {
271279
self.infos.get_mut(&id)
272280
}
273281

274-
pub(crate) fn get_path_id(&self, path: &AssetPath) -> Option<UntypedAssetId> {
275-
self.path_to_id.get(path).copied()
282+
pub(crate) fn get_path_and_type_id_handle(
283+
&self,
284+
path: &AssetPath,
285+
type_id: TypeId,
286+
) -> Option<UntypedHandle> {
287+
let id = self.path_to_id.get(path)?.get(&type_id)?;
288+
self.get_id_handle(*id)
289+
}
290+
291+
pub(crate) fn get_path_ids<'a>(
292+
&'a self,
293+
path: &'a AssetPath<'a>,
294+
) -> impl Iterator<Item = UntypedAssetId> + 'a {
295+
/// Concrete type to allow returning an `impl Iterator` even if `self.path_to_id.get(&path)` is `None`
296+
enum HandlesByPathIterator<T> {
297+
None,
298+
Some(T),
299+
}
300+
301+
impl<T> Iterator for HandlesByPathIterator<T>
302+
where
303+
T: Iterator<Item = UntypedAssetId>,
304+
{
305+
type Item = UntypedAssetId;
306+
307+
fn next(&mut self) -> Option<Self::Item> {
308+
match self {
309+
HandlesByPathIterator::None => None,
310+
HandlesByPathIterator::Some(iter) => iter.next(),
311+
}
312+
}
313+
}
314+
315+
if let Some(type_id_to_id) = self.path_to_id.get(path) {
316+
HandlesByPathIterator::Some(type_id_to_id.values().copied())
317+
} else {
318+
HandlesByPathIterator::None
319+
}
276320
}
277321

278-
pub(crate) fn get_path_handle(&self, path: &AssetPath) -> Option<UntypedHandle> {
279-
let id = *self.path_to_id.get(path)?;
280-
self.get_id_handle(id)
322+
pub(crate) fn get_path_handles<'a>(
323+
&'a self,
324+
path: &'a AssetPath<'a>,
325+
) -> impl Iterator<Item = UntypedHandle> + 'a {
326+
self.get_path_ids(path)
327+
.filter_map(|id| self.get_id_handle(id))
281328
}
282329

283330
pub(crate) fn get_id_handle(&self, id: UntypedAssetId) -> Option<UntypedHandle> {
@@ -289,12 +336,13 @@ impl AssetInfos {
289336
/// Returns `true` if the asset this path points to is still alive
290337
pub(crate) fn is_path_alive<'a>(&self, path: impl Into<AssetPath<'a>>) -> bool {
291338
let path = path.into();
292-
if let Some(id) = self.path_to_id.get(&path) {
293-
if let Some(info) = self.infos.get(id) {
294-
return info.weak_handle.strong_count() > 0;
295-
}
296-
}
297-
false
339+
340+
let result = self
341+
.get_path_ids(&path)
342+
.filter_map(|id| self.infos.get(&id))
343+
.any(|info| info.weak_handle.strong_count() > 0);
344+
345+
result
298346
}
299347

300348
/// Returns `true` if the asset at this path should be reloaded
@@ -592,7 +640,7 @@ impl AssetInfos {
592640

593641
fn process_handle_drop_internal(
594642
infos: &mut HashMap<UntypedAssetId, AssetInfo>,
595-
path_to_id: &mut HashMap<AssetPath<'static>, UntypedAssetId>,
643+
path_to_id: &mut HashMap<AssetPath<'static>, HashMap<TypeId, UntypedAssetId>>,
596644
loader_dependants: &mut HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>,
597645
living_labeled_assets: &mut HashMap<AssetPath<'static>, HashSet<String>>,
598646
watching_for_changes: bool,
@@ -609,6 +657,8 @@ impl AssetInfos {
609657
return false;
610658
}
611659

660+
let type_id = entry.key().type_id();
661+
612662
let info = entry.remove();
613663
let Some(path) = &info.path else {
614664
return true;
@@ -622,7 +672,15 @@ impl AssetInfos {
622672
living_labeled_assets,
623673
);
624674
}
625-
path_to_id.remove(path);
675+
676+
if let Some(map) = path_to_id.get_mut(path) {
677+
map.remove(&type_id);
678+
679+
if map.is_empty() {
680+
path_to_id.remove(path);
681+
}
682+
};
683+
626684
true
627685
}
628686

0 commit comments

Comments
 (0)