Add more load_direct implementations (#13415)

# Objective
- Introduce variants of `LoadContext::load_direct` which allow picking
asset type & configuring settings.
- Fixes #12963.

## Solution
- Implements `ErasedLoadedAsset::downcast` and adds some accessors to
`LoadedAsset<A>`.
- Changes `load_direct`/`load_direct_with_reader` to be typed, and
introduces `load_direct_untyped`/`load_direct_untyped_with_reader`.
- Introduces `load_direct_with_settings` and
`load_direct_with_reader_and_settings`.

## Testing
- I've run cargo test and played with the examples which use
`load_direct`.
- I also extended the `asset_processing` example to use the new typed
version of `load_direct` and use `load_direct_with_settings`.

---

## Changelog
- Introduced new `load_direct` methods in `LoadContext` to allow
specifying type & settings

## Migration Guide
- `LoadContext::load_direct` has been renamed to
`LoadContext::load_direct_untyped`. You may find the new `load_direct`
is more appropriate for your use case (and the migration may only be
moving one type parameter).
- `LoadContext::load_direct_with_reader` has been renamed to
`LoadContext::load_direct_untyped_with_reader`.

---

This might not be an obvious win as a solution because it introduces
quite a few new `load_direct` alternatives - but it does follow the
existing pattern pretty well. I'm very open to alternatives.
😅
This commit is contained in:
Ricky Taylor 2024-05-21 19:32:00 +01:00 committed by GitHub
parent 2857eb6b9d
commit 26df1c1179
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 297 additions and 89 deletions

View file

@ -510,12 +510,13 @@ mod tests {
let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
let mut embedded = String::new();
for dep in ron.embedded_dependencies {
let loaded = load_context.load_direct(&dep).await.map_err(|_| {
Self::Error::CannotLoadDependency {
let loaded = load_context
.load_direct::<CoolText>(&dep)
.await
.map_err(|_| Self::Error::CannotLoadDependency {
dependency: dep.into(),
}
})?;
let cool = loaded.get::<CoolText>().unwrap();
})?;
let cool = loaded.get();
embedded.push_str(&cool.text);
}
Ok(CoolText {

View file

@ -1,8 +1,8 @@
use crate::{
io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader},
meta::{
loader_settings_meta_transform, AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal,
Settings,
loader_settings_meta_transform, meta_transform_settings, AssetHash, AssetMeta,
AssetMetaDyn, ProcessedInfoMinimal, Settings,
},
path::AssetPath,
Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, LoadedUntypedAsset,
@ -165,6 +165,29 @@ impl<A: Asset> LoadedAsset<A> {
meta,
}
}
/// Cast (and take ownership) of the [`Asset`] value of the given type.
pub fn take(self) -> A {
self.value
}
/// Retrieves a reference to the internal [`Asset`] type.
pub fn get(&self) -> &A {
&self.value
}
/// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
pub fn get_labeled(
&self,
label: impl Into<CowArc<'static, str>>,
) -> Option<&ErasedLoadedAsset> {
self.labeled_assets.get(&label.into()).map(|a| &a.asset)
}
/// Iterate over all labels for "labeled assets" in the loaded asset
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
self.labeled_assets.keys().map(|s| &**s)
}
}
impl<A: Asset> From<A> for LoadedAsset<A> {
@ -228,6 +251,25 @@ impl ErasedLoadedAsset {
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
self.labeled_assets.keys().map(|s| &**s)
}
/// Cast this loaded asset as the given type. If the type does not match,
/// the original type-erased asset is returned.
#[allow(clippy::result_large_err)]
pub fn downcast<A: Asset>(mut self) -> Result<LoadedAsset<A>, ErasedLoadedAsset> {
match self.value.downcast::<A>() {
Ok(value) => Ok(LoadedAsset {
value: *value,
dependencies: self.dependencies,
loader_dependencies: self.loader_dependencies,
labeled_assets: self.labeled_assets,
meta: self.meta,
}),
Err(value) => {
self.value = value;
Err(self)
}
}
}
}
/// A type erased container for an [`Asset`] value that is capable of inserting the [`Asset`] into a [`World`]'s [`Assets`] collection.
@ -464,7 +506,7 @@ impl<'a> LoadContext<'a> {
/// Retrieves a handle for the asset at the given path and adds that path as a dependency of the asset.
/// If the current context is a normal [`AssetServer::load`], an actual asset load will be kicked off immediately, which ensures the load happens
/// as soon as possible.
/// "Normal loads" kicked from within a normal Bevy App will generally configure the context to kick off loads immediately.
/// "Normal loads" kicked from within a normal Bevy App will generally configure the context to kick off loads immediately.
/// If the current context is configured to not load dependencies automatically (ex: [`AssetProcessor`](crate::processor::AssetProcessor)),
/// a load will not be kicked off automatically. It is then the calling context's responsibility to begin a load if necessary.
pub fn load<'b, A: Asset>(&mut self, path: impl Into<AssetPath<'b>>) -> Handle<A> {
@ -495,7 +537,7 @@ impl<'a> LoadContext<'a> {
/// Loads the [`Asset`] of type `A` at the given `path` with the given [`AssetLoader::Settings`] settings `S`. This is a "deferred"
/// load. If the settings type `S` does not match the settings expected by `A`'s asset loader, an error will be printed to the log
/// and the asset load will fail.
/// and the asset load will fail.
pub fn load_with_settings<'b, A: Asset, S: Settings + Default>(
&mut self,
path: impl Into<AssetPath<'b>>,
@ -525,6 +567,95 @@ impl<'a> LoadContext<'a> {
handle
}
async fn load_direct_untyped_internal(
&mut self,
path: AssetPath<'static>,
meta: Box<dyn AssetMetaDyn>,
loader: &dyn ErasedAssetLoader,
reader: &mut Reader<'_>,
) -> Result<ErasedLoadedAsset, LoadDirectError> {
let loaded_asset = self
.asset_server
.load_with_meta_loader_and_reader(
&path,
meta,
loader,
reader,
false,
self.populate_hashes,
)
.await
.map_err(|error| LoadDirectError {
dependency: path.clone(),
error,
})?;
let info = loaded_asset
.meta
.as_ref()
.and_then(|m| m.processed_info().as_ref());
let hash = info.map(|i| i.full_hash).unwrap_or(Default::default());
self.loader_dependencies.insert(path, hash);
Ok(loaded_asset)
}
async fn load_direct_internal<A: Asset>(
&mut self,
path: AssetPath<'static>,
meta: Box<dyn AssetMetaDyn>,
loader: &dyn ErasedAssetLoader,
reader: &mut Reader<'_>,
) -> Result<LoadedAsset<A>, LoadDirectError> {
self.load_direct_untyped_internal(path.clone(), meta, loader, &mut *reader)
.await
.and_then(move |untyped_asset| {
untyped_asset.downcast::<A>().map_err(|_| LoadDirectError {
dependency: path.clone(),
error: AssetLoadError::RequestedHandleTypeMismatch {
path,
requested: TypeId::of::<A>(),
actual_asset_name: loader.asset_type_name(),
loader_name: loader.type_name(),
},
})
})
}
async fn load_direct_untyped_with_transform(
&mut self,
path: AssetPath<'static>,
meta_transform: impl FnOnce(&mut dyn AssetMetaDyn),
) -> Result<ErasedLoadedAsset, LoadDirectError> {
let (mut meta, loader, mut reader) = self
.asset_server
.get_meta_loader_and_reader(&path, None)
.await
.map_err(|error| LoadDirectError {
dependency: path.clone(),
error,
})?;
meta_transform(&mut *meta);
self.load_direct_untyped_internal(path.clone(), meta, &*loader, &mut *reader)
.await
}
async fn load_direct_with_transform<A: Asset>(
&mut self,
path: AssetPath<'static>,
meta_transform: impl FnOnce(&mut dyn AssetMetaDyn),
) -> Result<LoadedAsset<A>, LoadDirectError> {
let (mut meta, loader, mut reader) = self
.asset_server
.get_meta_loader_and_reader(&path, Some(TypeId::of::<A>()))
.await
.map_err(|error| LoadDirectError {
dependency: path.clone(),
error,
})?;
meta_transform(&mut *meta);
self.load_direct_internal(path.clone(), meta, &*loader, &mut *reader)
.await
}
/// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before
/// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are
/// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a
@ -535,42 +666,54 @@ impl<'a> LoadContext<'a> {
///
/// [`Process`]: crate::processor::Process
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
pub async fn load_direct<'b>(
pub async fn load_direct<'b, A: Asset>(
&mut self,
path: impl Into<AssetPath<'b>>,
) -> Result<LoadedAsset<A>, LoadDirectError> {
self.load_direct_with_transform(path.into().into_owned(), |_| {})
.await
}
/// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before
/// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are
/// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a
/// "load dependency".
///
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor,
/// changing a "load dependency" will result in re-processing of the asset.
///
/// If the settings type `S` does not match the settings expected by `A`'s asset loader, an error will be printed to the log
/// and the asset load will fail.
///
/// [`Process`]: crate::processor::Process
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
pub async fn load_direct_with_settings<'b, A: Asset, S: Settings + Default>(
&mut self,
path: impl Into<AssetPath<'b>>,
settings: impl Fn(&mut S) + Send + Sync + 'static,
) -> Result<LoadedAsset<A>, LoadDirectError> {
self.load_direct_with_transform(path.into().into_owned(), move |meta| {
meta_transform_settings(meta, &settings);
})
.await
}
/// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before
/// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are
/// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a
/// "load dependency".
///
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor,
/// changing a "load dependency" will result in re-processing of the asset.
///
/// [`Process`]: crate::processor::Process
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
pub async fn load_direct_untyped<'b>(
&mut self,
path: impl Into<AssetPath<'b>>,
) -> Result<ErasedLoadedAsset, LoadDirectError> {
let path = path.into().into_owned();
let to_error = |e: AssetLoadError| -> LoadDirectError {
LoadDirectError {
dependency: path.clone(),
error: e,
}
};
let loaded_asset = {
let (meta, loader, mut reader) = self
.asset_server
.get_meta_loader_and_reader(&path, None)
.await
.map_err(to_error)?;
self.asset_server
.load_with_meta_loader_and_reader(
&path,
meta,
&*loader,
&mut *reader,
false,
self.populate_hashes,
)
.await
.map_err(to_error)?
};
let info = loaded_asset
.meta
.as_ref()
.and_then(|m| m.processed_info().as_ref());
let hash = info.map(|i| i.full_hash).unwrap_or(Default::default());
self.loader_dependencies.insert(path, hash);
Ok(loaded_asset)
self.load_direct_untyped_with_transform(path.into().into_owned(), |_| {})
.await
}
/// Loads the asset at the given `path` directly from the provided `reader`. This is an async function that will wait until the asset is fully loaded before
@ -583,7 +726,76 @@ impl<'a> LoadContext<'a> {
///
/// [`Process`]: crate::processor::Process
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
pub async fn load_direct_with_reader<'b>(
pub async fn load_direct_with_reader<'b, A: Asset>(
&mut self,
reader: &mut Reader<'_>,
path: impl Into<AssetPath<'b>>,
) -> Result<LoadedAsset<A>, LoadDirectError> {
let path = path.into().into_owned();
let loader = self
.asset_server
.get_asset_loader_with_asset_type::<A>()
.await
.map_err(|error| LoadDirectError {
dependency: path.clone(),
error: error.into(),
})?;
let meta = loader.default_meta();
self.load_direct_internal(path, meta, &*loader, reader)
.await
}
/// Loads the asset at the given `path` directly from the provided `reader`. This is an async function that will wait until the asset is fully loaded before
/// returning. Use this if you need the _value_ of another asset in order to load the current asset, and that value comes from your [`Reader`].
/// For example, if you are deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a
/// "load dependency".
///
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor,
/// changing a "load dependency" will result in re-processing of the asset.
///
/// If the settings type `S` does not match the settings expected by `A`'s asset loader, an error will be printed to the log
/// and the asset load will fail.
///
/// [`Process`]: crate::processor::Process
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
pub async fn load_direct_with_reader_and_settings<'b, A: Asset, S: Settings + Default>(
&mut self,
reader: &mut Reader<'_>,
path: impl Into<AssetPath<'b>>,
settings: impl Fn(&mut S) + Send + Sync + 'static,
) -> Result<LoadedAsset<A>, LoadDirectError> {
let path = path.into().into_owned();
let loader = self
.asset_server
.get_asset_loader_with_asset_type::<A>()
.await
.map_err(|error| LoadDirectError {
dependency: path.clone(),
error: error.into(),
})?;
let mut meta = loader.default_meta();
meta_transform_settings(&mut *meta, &settings);
self.load_direct_internal(path, meta, &*loader, reader)
.await
}
/// Loads the asset at the given `path` directly from the provided `reader`. This is an async function that will wait until the asset is fully loaded before
/// returning. Use this if you need the _value_ of another asset in order to load the current asset, and that value comes from your [`Reader`].
/// For example, if you are deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a
/// "load dependency".
///
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor,
/// changing a "load dependency" will result in re-processing of the asset.
///
/// [`Process`]: crate::processor::Process
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
pub async fn load_direct_untyped_with_reader<'b>(
&mut self,
reader: &mut Reader<'_>,
path: impl Into<AssetPath<'b>>,
@ -601,32 +813,8 @@ impl<'a> LoadContext<'a> {
let meta = loader.default_meta();
let loaded_asset = self
.asset_server
.load_with_meta_loader_and_reader(
&path,
meta,
&*loader,
reader,
false,
self.populate_hashes,
)
self.load_direct_untyped_internal(path, meta, &*loader, reader)
.await
.map_err(|error| LoadDirectError {
dependency: path.clone(),
error,
})?;
let info = loaded_asset
.meta
.as_ref()
.and_then(|m| m.processed_info().as_ref());
let hash = info.map(|i| i.full_hash).unwrap_or_default();
self.loader_dependencies.insert(path, hash);
Ok(loaded_asset)
}
}

View file

@ -208,21 +208,26 @@ impl AssetLoader for () {
}
}
pub(crate) fn meta_transform_settings<S: Settings>(
meta: &mut dyn AssetMetaDyn,
settings: &(impl Fn(&mut S) + Send + Sync + 'static),
) {
if let Some(loader_settings) = meta.loader_settings_mut() {
if let Some(loader_settings) = loader_settings.downcast_mut::<S>() {
settings(loader_settings);
} else {
error!(
"Configured settings type {} does not match AssetLoader settings type",
std::any::type_name::<S>(),
);
}
}
}
pub(crate) fn loader_settings_meta_transform<S: Settings>(
settings: impl Fn(&mut S) + Send + Sync + 'static,
) -> MetaTransform {
Box::new(move |meta| {
if let Some(loader_settings) = meta.loader_settings_mut() {
if let Some(loader_settings) = loader_settings.downcast_mut::<S>() {
settings(loader_settings);
} else {
error!(
"Configured settings type {} does not match AssetLoader settings type",
std::any::type_name::<S>(),
);
}
}
})
Box::new(move |meta| meta_transform_settings(meta, &settings))
}
pub type AssetHash = [u8; 32];

View file

@ -72,7 +72,7 @@ impl AssetLoader for GzAssetLoader {
let mut reader = VecReader::new(bytes_uncompressed);
let uncompressed = load_context
.load_direct_with_reader(&mut reader, contained_path)
.load_direct_untyped_with_reader(&mut reader, contained_path)
.await?;
Ok(GzAsset { uncompressed })

View file

@ -72,7 +72,7 @@ struct Text(String);
#[derive(Default)]
struct TextLoader;
#[derive(Default, Serialize, Deserialize)]
#[derive(Clone, Default, Serialize, Deserialize)]
struct TextSettings {
text_override: Option<String>,
}
@ -107,6 +107,7 @@ struct CoolTextRon {
text: String,
dependencies: Vec<String>,
embedded_dependencies: Vec<String>,
dependencies_with_settings: Vec<(String, TextSettings)>,
}
#[derive(Asset, TypePath, Debug)]
@ -145,9 +146,16 @@ impl AssetLoader for CoolTextLoader {
let ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
let mut base_text = ron.text;
for embedded in ron.embedded_dependencies {
let loaded = load_context.load_direct(&embedded).await?;
let text = loaded.get::<Text>().unwrap();
base_text.push_str(&text.0);
let loaded = load_context.load_direct::<Text>(&embedded).await?;
base_text.push_str(&loaded.get().0);
}
for (path, settings_override) in ron.dependencies_with_settings {
let loaded = load_context
.load_direct_with_settings::<Text, _>(&path, move |settings| {
*settings = settings_override.clone();
})
.await?;
base_text.push_str(&loaded.get().0);
}
Ok(CoolText {
text: base_text,

View file

@ -5,4 +5,5 @@
"foo/c.cool.ron",
],
embedded_dependencies: [],
)
dependencies_with_settings: [],
)

View file

@ -6,4 +6,7 @@
"foo/c.cool.ron",
"embedded://asset_processing/e.txt"
],
)
dependencies_with_settings: [
("embedded://asset_processing/e.txt", (text_override: Some("E"))),
],
)

View file

@ -2,4 +2,5 @@
text: "b",
dependencies: [],
embedded_dependencies: [],
)
dependencies_with_settings: [],
)

View file

@ -2,4 +2,5 @@
text: "c",
dependencies: [],
embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"],
)
dependencies_with_settings: [],
)