-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Use Asset Path Extension for AssetLoader
Disambiguation
#11644
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
Use Asset Path Extension for AssetLoader
Disambiguation
#11644
Conversation
Added algorithm which will attempt to resolve via `AssetLoader` name, then `Asset` type, then by extension. If at any point multiple loaders fit a particular criteria, the next criteria is used as a tie breaker.
I tried an extended example of the previous review I did, it does not work: Example// my-asset
RonAsset (
name: "Cube 1",
translation: (0.1, 0.9, 2.4)
) // my-asset-2
RonOtherAsset (
name: "Cube 2",
translation: (0.8, 0.3, 0.9)
) // my-asset.ronasset
RonAsset (
name: "Cube 3",
translation: (0.8, 0.3, 0.9)
) // my-asset.also-ronasset
RonAsset (
name: "Cube 4",
translation: (0.8, 3.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 RonOtherAsset {
pub name: String,
pub translation: Vec3,
}
#[derive(Default)]
pub struct RonLoaderNoExt;
#[derive(Default)]
pub struct RonLoaderWithExt;
#[derive(Default)]
pub struct RonLoaderWithOtherExt;
#[derive(Default)]
pub struct RonLoaderNoExtOtherAsset;
#[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 RonLoaderNoExt {
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 RonLoaderNoExtOtherAsset {
type Asset = RonOtherAsset;
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::<RonOtherAsset>(&bytes)?;
Ok(custom_asset)
})
}
}
// same asset but with ext
impl AssetLoader for RonLoaderWithExt {
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)
})
}
fn extensions(&self) -> &[&str] {
&["ronasset"]
}
}
// same asset but with other ext
impl AssetLoader for RonLoaderWithOtherExt {
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)
})
}
fn extensions(&self) -> &[&str] {
&["also-ronasset"]
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_asset::<RonAsset>()
.init_asset_loader::<RonLoaderNoExt>()
.init_asset_loader::<RonLoaderWithExt>()
.init_asset_loader::<RonLoaderWithOtherExt>()
.init_asset::<RonOtherAsset>()
.init_asset_loader::<RonLoaderNoExtOtherAsset>()
.add_systems(Startup, setup)
.add_systems(Update, (set_cube_translations, 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::<RonOtherAsset>("data/my-asset-2"),
));
// cube 3
commands.spawn((
PbrBundle {
mesh: meshes.add(shape::Cube { size: 1.0 }),
material: materials.add(Color::rgb_u8(124, 244, 10)),
..default()
},
asset_server.load::<RonAsset>("data/my-asset.ronasset"),
));
// cube 4
commands.spawn((
PbrBundle {
mesh: meshes.add(shape::Cube { size: 1.0 }),
material: materials.add(Color::rgb_u8(10, 244, 255)),
..default()
},
asset_server.load::<RonAsset>("data/my-asset.also-ronasset"),
));
// 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_translations(
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<RonOtherAsset>)>,
ron_assets: Res<Assets<RonOtherAsset>>,
) {
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;
}
}
} The scenario:
Run by Output:
|
I've marked this as draft for the moment since it needs some unit tests and a QA pass. Unfortunately it's not as simple as I had hoped to resolve, but I will continue working on it and try and get something as soon as possible. |
The issue here is that there are multiple |
I believe this is now ready for review. I've refactored the More than half of this PR consists of unit tests for the resolution behaviour. To work around using To the best of my testing, hot reloading, aliased loaders, and meta file overrides all work as expected, but please let me know if there's anything that I have missed! Some key behaviours for the
|
I tried the example I posted again, and I'm a bit unsure if I'm hitting a problem or intended behaviour. Each time I update
The registered loaders are:
And another type:
The three first loaders map to the same type, but they have no overlap in extensions required (unless all loaders implicitly can load without extension?). The last loader shouldn't create any issues I think. So should there in this case be any disambiguity? |
Yes this is how I've intended this functionality to work. In essence, a lack of extension means "we don't know what the extension is", so any This is analogous to having 3 Perhaps your assumption was instead that if a loader specified no extensions, then it would be the one used for extensionless assets? Not saying that's wrong or a bad assumption, just calling out that it's different to my assumptions and I want to clarify. |
I think my confusion arose because listing which extensions are supported by a loader is very explicit: &["ron", "foo", "json"] but extensionless support is implicit, and somehow that feels surprising to me. But now that I know I understand what happens in the example, and as long as docs clarify this then I believe it should be ok. |
That's my bad for not making my own assumptions clear, thank you for pointing it out! My interpretation on the extension list is it is the preferred extensions of a loader. Since meta files allowed you to choose a loader by name (ignoring the extension), you could already load an asset with a different extension. Further, since you could have overlapping extension lists, your loader wasn't guaranteed to receive all asset paths matching the list, so it was already quite a loose definition. I'm adding a short note to the
Which should clear up confusion on the implementation side of As for users (ones calling |
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.
Tried the example I posted which still works (and I understand why there are disambiguities now). Nice with tests.
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.
Solid work: this is a nice refactor / fix and well-tested.
Objective
Solution
AssetLoaders
to capture possibility of multipleAssetLoader
registrations operating on the sameAsset
type, but different extensions.AssetLoader
name, thenAsset
type, then by extension. If at any point multiple loaders fit a particular criteria, the next criteria is used as a tie breaker.