-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added Support for Extension-less Assets #10153
Added Support for Extension-less Assets #10153
Conversation
* Refactored `AssetLoaders` into its own module to decouple it from `AssetServer`. * Added using `TypeId` from `Handle` as a hint for selecting an `AssetLoader`. This is tried before relying on file extensions. * Updated `custom_asset` example to demonstrate extension-less assets.
I like this refactor! Decoupling that is a good step, and it's small enough that I don't feel terrible doing it in the same PR.
Oh this is extremely useful: I've run into this before and had to do bizarre
Thanks for calling this out: it's a solid choice. |
If a provided `TypeId` is not found in the `AssetLoaders`, fall back to file extensions.
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "disambiguate between assets of the same file extension via type annotations" bit of this PR is wildly useful: can you add / extend an example for it?
Also added support for multiple assets using the same path as long as they are distinct asset types.
I've updated the #[derive(Asset, TypePath, Debug)]
pub struct Blob {
pub bytes: Vec<u8>,
} This allows loading the same path multiple times, provided they are for distinct asset types: #[derive(Resource, Default)]
struct State {
handle: Handle<CustomAsset>,
other_handle: Handle<CustomAsset>,
blob: Handle<Blob>,
printed: bool,
}
fn setup(mut state: ResMut<State>, asset_server: Res<AssetServer>) {
// Recommended way to load an asset
state.handle = asset_server.load("data/asset.custom");
// File extensions are optional, but are recommended
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");
} In the example, I also show that you can create a loader with no defined extensions: impl AssetLoader for BlobAssetLoader {
type Asset = Blob;
type Settings = ();
type Error = BlobAssetLoaderError;
fn load<'a>(/* snip */) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
/* snip */
}
fn extensions(&self) -> &[&str] {
// This loader won't be used for any files by default
&[]
}
} This blob loader I think is a good example of where you might want that behaviour; you have an asset which you want to load, but it's not the default way you would interact with that particular file. It would be possible to now provide a default implementation for |
@alice-i-cecile bump for review. |
So @bushrat011899, extending on my proposed use case a bit.
Is that feasible with this PR? That said, this example seems great to me, and I'm happy to approve this PR as is. |
@alice-i-cecile Yes that should be entirely possible:
// Handle<JsonAsset> implied by .json file extension if
// `JsonAssetLoader` is configured to be default handler of .json
let my_data_json_handle = asset_server.load("data/my_data.json");
let my_data_json = json_assets.get(&my_data_json_handle)?;
let serde_json::Value::String(magic_token) = my_data_json.get("magic_token")? else {
return None;
};
// Can re-use the path value for DRY reasons
let path = my_data_json_handle.path().unwrap();
// All match arms need to have the same return type, using UntypedHandle to avoid an enum
let my_data_handle = match magic_token {
"key_bindings" => asset_server.load::<KeyBindingsAsset>(&path).untyped(),
"settings" => asset_server.load::<SettingsAsset>(&path).untyped(),
"scoreboard" => asset_server.load::<ScoreboardAsset>(&path).untyped(),
_ => my_data_json_handle.untyped(),
};
Some(my_data_handle) I imagine you'd want an |
Awesome, I'll spin out a follow-up issue for that. |
Sorry about the radio silence everyone, I had an unusually lovely holiday break over the new year, but I'm back now and wanting to sort out some of these outstanding PRs from last year. I doubt this will be approved before the 0.13 merge window closes, but I've upstreamed this branch to the latest version of I've also narrowed the scope of this PR to try and help spur its adoption:
To focus on the core functionality of this PR:
Thanks again to everyone who reviewed this PR over the last couple months while I was away! ❤️ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Welcome back! I'm happy with this, and pleased to see that there's now no breaking changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, this is really high quality stuff! I didn't see any issues, but I am not submitting an approval because I still feel a little hazy about the asset server internals.
Not sure if okay as-is or not, worth mentioning: I tried having a However, if I try loading a So I suppose the question is why require listing the supported extension(s) in the loader trait impl at all, when type inference can help out? |
I made an example which breaks and I think it shouldn't. EDIT: The example tries using two custom extension-less loaders. Example// my-asset
RonAsset (
name: "Cube 1",
translation: (0.1, 0.9, 2.4)
) // my-asset-2
RonAsset2 (
name: "Cube 2",
translation: (0.8, 0.3, 0.9)
) use bevy::{
asset::{io::Reader, ron, AssetLoader, AsyncReadExt, LoadContext},
prelude::*,
reflect::TypePath,
utils::{thiserror, thiserror::Error, BoxedFuture},
};
use serde::Deserialize;
#[derive(Asset, TypePath, Debug, Deserialize, Component)]
pub struct RonAsset {
pub name: String,
pub translation: Vec3,
}
#[derive(Asset, TypePath, Debug, Deserialize, Component)]
pub struct RonAsset2 {
pub name: String,
pub translation: Vec3,
}
#[derive(Default)]
pub struct RonLoader;
#[derive(Default)]
pub struct RonLoader2;
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum RonLoaderError {
/// An [IO](std::io) Error
#[error("Could not load asset: {0}")]
Io(#[from] std::io::Error),
/// A [RON](ron) Error
#[error("Could not parse RON: {0}")]
RonSpannedError(#[from] ron::error::SpannedError),
}
impl AssetLoader for RonLoader {
type Asset = RonAsset;
type Settings = ();
type Error = RonLoaderError;
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 {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let custom_asset = ron::de::from_bytes::<RonAsset>(&bytes)?;
Ok(custom_asset)
})
}
}
impl AssetLoader for RonLoader2 {
type Asset = RonAsset2;
type Settings = ();
type Error = RonLoaderError;
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 {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let custom_asset = ron::de::from_bytes::<RonAsset2>(&bytes)?;
Ok(custom_asset)
})
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_asset::<RonAsset>()
.init_asset_loader::<RonLoader>()
.init_asset::<RonAsset2>()
.init_asset_loader::<RonLoader2>()
.add_systems(Startup, setup)
.add_systems(Update, (set_cube_translation, set_cube_translation2))
.run();
}
#[derive(Debug, Component)]
struct Cube;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
// circular base
commands.spawn(PbrBundle {
mesh: meshes.add(shape::Circle::new(4.0)),
material: materials.add(Color::WHITE),
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
..default()
});
// cube 1
commands.spawn((
PbrBundle {
mesh: meshes.add(shape::Cube { size: 1.0 }),
material: materials.add(Color::rgb_u8(124, 144, 255)),
..default()
},
asset_server.load::<RonAsset>("data/my-asset"),
));
// cube 2
commands.spawn((
PbrBundle {
mesh: meshes.add(shape::Cube { size: 1.0 }),
material: materials.add(Color::rgb_u8(124, 244, 255)),
..default()
},
asset_server.load::<RonAsset2>("data/my-asset-2"),
));
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 250_000.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
fn set_cube_translation(
mut transform_via_asset: Query<(&mut Transform, &Handle<RonAsset>)>,
ron_assets: Res<Assets<RonAsset>>,
) {
for (mut transform, asset_handle) in &mut transform_via_asset {
let Some(asset) = ron_assets.get(asset_handle) else {
continue;
};
if transform.translation != asset.translation {
info!("Updating {}", asset.name);
transform.translation = asset.translation;
}
}
}
fn set_cube_translation2(
mut transform_via_asset: Query<(&mut Transform, &Handle<RonAsset2>)>,
ron_assets: Res<Assets<RonAsset2>>,
) {
for (mut transform, asset_handle) in &mut transform_via_asset {
let Some(asset) = ron_assets.get(asset_handle) else {
continue;
};
if transform.translation != asset.translation {
info!("Updating {}", asset.name);
transform.translation = asset.translation;
}
}
} Run via Initially all seems ok:
Updating one of the files via hot-reloading gives an error:
Updating the other file does not give an error, but the system updating the translation doesn't run like it should either
|
Ah that's why I'm having trouble replicating this issue myself, I wasn't testing with the |
When a reload is requested for a given path, now `load_internal` will be called with the existing `UntypedHandle` for that asset (if still valid), for each handle. If that process is unsuccessful, but the asset still should be reloaded (as decided by `should_reload`), then a `load_internal` is called without a handle as well.
Ok @torsteingrindvik this should be resolved now. Again, thanks for testing this, I had completely forgotten to test against the Running your example now works as expected, where editing the extensionless assets is immediately reflected in the running application with no runtime errors. |
Great! Approved. Thanks yourself for the nice PR. |
# Objective - Addresses **Support processing and loading files without extensions** from bevyengine#9714 - Addresses **More runtime loading configuration** from bevyengine#9714 - Fixes bevyengine#367 - Fixes bevyengine#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>
Objective
Solution
AssetServer::load::<A>
andAssetServer::load_with_settings::<A>
can now use theAsset
type parameterA
to select a registeredAssetLoader
without inspecting the providedAssetPath
. This change cascades ontoLoadContext::load
andLoadContext::load_with_settings
. This allows the loading of assets which have incorrect or ambiguous file extensions.Since a single
AssetPath
no longer maps 1:1 with anAsset
, I've also modified how assets are loaded to permit multiple asset types to be loaded from a single path. This allows for two differentAssetLoaders
(which return different types of assets) to both load a single path (if requested).As these changes are reflected in the
LoadContext
as well as theAssetServer
, customAssetLoaders
can also take advantage of this behaviour to create more complex assets.Change Log
custom_asset
example to demonstrate extension-less assets.AssetServer::get_handles_untyped
and AddedAssetServer::get_path_ids
Notes
As a part of that refactor, I chose to store
AssetLoader
s (withinAssetLoaders
) using aHashMap<TypeId, ...>
instead of aVec<...>
. My reasoning for this was I needed to add a relationship betweenAsset
TypeId
s and theAssetLoader
, so instead of having aVec
and aHashMap
, I combined the two, removing theusize
index from the adjacent maps.