mirror of
https://github.com/bevyengine/bevy
synced 2025-01-08 03:08:55 +00:00
1df8238e8d
# Objective The `NestedLoader` API as it stands right now is somewhat lacking: - It consists of several types `NestedLoader`, `UntypedNestedLoader`, `DirectNestedLoader`, and `UntypedDirectNestedLoader`, where a typestate pattern on `NestedLoader` would be make it more obvious what it does, and allow centralising the documentation - The term "untyped" in the asset loader code is overloaded. It can mean either: - we have literally no idea what the type of this asset will be when we load it (I dub this "unknown type") - we know what type of asset it will be, but we don't know it statically - we only have a TypeId (I dub this "dynamic type" / "erased") - There is no way to get an `UntypedHandle` (erased) given a `TypeId` ## Solution Changes `NestedLoader` into a type-state pattern, adding two type params: - `T` determines the typing - `StaticTyped`, the default, where you pass in `A` statically into `fn load<A>() -> ..` - `DynamicTyped`, where you give a `TypeId`, giving you a `UntypedHandle` - `UnknownTyped`, where you have literally no idea what type of asset you're loading, giving you a `Handle<LoadedUntypedAsset>` - `M` determines the "mode" (bikeshedding TBD, I couldn't come up with a better name) - `Deferred`, the default, won't load the asset when you call `load`, but it does give you a `Handle` to it (this is nice since it can be a sync fn) - `Immediate` will load the asset as soon as you call it, and give you access to it, but you must be in an async context to call it Changes some naming of internals in `AssetServer` to fit the new definitions of "dynamic type" and "unknown type". Note that I didn't do a full pass over this code to keep the diff small. That can probably be done in a new PR - I think the definiton I laid out of unknown type vs. erased makes it pretty clear where each one applies. <details> <summary>Old issue</summary> The only real problem I have with this PR is the requirement to pass in `type_name` (from `core::any::type_name`) into Erased. Users might not have that type name, only the ID, and it just seems sort of weird to *have* to give an asset type name. However, the reason we need it is because of this: ```rs pub(crate) fn get_or_create_path_handle_erased( &mut self, path: AssetPath<'static>, type_id: TypeId, type_name: &str, loading_mode: HandleLoadingMode, meta_transform: Option<MetaTransform>, ) -> (UntypedHandle, bool) { let result = self.get_or_create_path_handle_internal( path, Some(type_id), loading_mode, meta_transform, ); // it is ok to unwrap because TypeId was specified above unwrap_with_context(result, type_name).unwrap() } pub(crate) fn unwrap_with_context<T>( result: Result<T, GetOrCreateHandleInternalError>, type_name: &str, ) -> Option<T> { match result { Ok(value) => Some(value), Err(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified) => None, Err(GetOrCreateHandleInternalError::MissingHandleProviderError(_)) => { panic!("Cannot allocate an Asset Handle of type '{type_name}' because the asset type has not been initialized. \ Make sure you have called app.init_asset::<{type_name}>()") } } } ``` This `unwrap_with_context` is literally the only reason we need the `type_name`. Potentially, this can be turned into an `impl Into<Option<&str>>`, and output a different error message if the type name is missing. Since if we are loading an asset where we only know the type ID, by definition we can't output that error message, since we don't have the type name. I'm open to suggestions on this. </details> ## Testing Not sure how to test this, since I kept most of the actual NestedLoader logic the same. The only new API is loading an `UntypedHandle` when in the `DynamicTyped, Immediate` state. ## Migration Guide Code which uses `bevy_asset`'s `LoadContext::loader` / `NestedLoader` will see some naming changes: - `untyped` is replaced by `with_unknown_type` - `with_asset_type` is replaced by `with_static_type` - `with_asset_type_id` is replaced by `with_dynamic_type` - `direct` is replaced by `immediate` (the opposite of "immediate" is "deferred")
501 lines
18 KiB
Rust
501 lines
18 KiB
Rust
//! Implementations of the builder-pattern used for loading dependent assets via
|
|
//! [`LoadContext::loader`].
|
|
|
|
use crate::{
|
|
io::Reader,
|
|
meta::{meta_transform_settings, AssetMetaDyn, MetaTransform, Settings},
|
|
Asset, AssetLoadError, AssetPath, ErasedAssetLoader, ErasedLoadedAsset, Handle, LoadContext,
|
|
LoadDirectError, LoadedAsset, LoadedUntypedAsset, UntypedHandle,
|
|
};
|
|
use alloc::sync::Arc;
|
|
use core::any::TypeId;
|
|
|
|
// Utility type for handling the sources of reader references
|
|
enum ReaderRef<'a> {
|
|
Borrowed(&'a mut dyn Reader),
|
|
Boxed(Box<dyn Reader + 'a>),
|
|
}
|
|
|
|
impl ReaderRef<'_> {
|
|
pub fn as_mut(&mut self) -> &mut dyn Reader {
|
|
match self {
|
|
ReaderRef::Borrowed(r) => &mut **r,
|
|
ReaderRef::Boxed(b) => &mut **b,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A builder for loading nested assets inside a [`LoadContext`].
|
|
///
|
|
/// # Loader state
|
|
///
|
|
/// The type parameters `T` and `M` determine how this will load assets:
|
|
/// - `T`: the typing of this loader. How do we know what type of asset to load?
|
|
///
|
|
/// See [`StaticTyped`] (the default), [`DynamicTyped`], and [`UnknownTyped`].
|
|
///
|
|
/// - `M`: the load mode. Do we want to load this asset right now (in which case
|
|
/// you will have to `await` the operation), or do we just want a [`Handle`],
|
|
/// and leave the actual asset loading to later?
|
|
///
|
|
/// See [`Deferred`] (the default) and [`Immediate`].
|
|
///
|
|
/// When configuring this builder, you can freely switch between these modes
|
|
/// via functions like [`deferred`] and [`immediate`].
|
|
///
|
|
/// ## Typing
|
|
///
|
|
/// To inform the loader of what type of asset to load:
|
|
/// - in [`StaticTyped`]: statically providing a type parameter `A: Asset` to
|
|
/// [`load`].
|
|
///
|
|
/// This is the simplest way to get a [`Handle<A>`] to the loaded asset, as
|
|
/// long as you know the type of `A` at compile time.
|
|
///
|
|
/// - in [`DynamicTyped`]: providing the [`TypeId`] of the asset at runtime.
|
|
///
|
|
/// If you know the type ID of the asset at runtime, but not at compile time,
|
|
/// use [`with_dynamic_type`] followed by [`load`] to start loading an asset
|
|
/// of that type. This lets you get an [`UntypedHandle`] (via [`Deferred`]),
|
|
/// or a [`ErasedLoadedAsset`] (via [`Immediate`]).
|
|
///
|
|
/// - in [`UnknownTyped`]: loading either a type-erased version of the asset
|
|
/// ([`ErasedLoadedAsset`]), or a handle *to a handle* of the actual asset
|
|
/// ([`LoadedUntypedAsset`]).
|
|
///
|
|
/// If you have no idea what type of asset you will be loading (not even at
|
|
/// runtime with a [`TypeId`]), use this.
|
|
///
|
|
/// ## Load mode
|
|
///
|
|
/// To inform the loader how you want to load the asset:
|
|
/// - in [`Deferred`]: when you request to load the asset, you get a [`Handle`]
|
|
/// for it, but the actual loading won't be completed until later.
|
|
///
|
|
/// Use this if you only need a [`Handle`] or [`UntypedHandle`].
|
|
///
|
|
/// - in [`Immediate`]: the load request will load the asset right then and
|
|
/// there, waiting until the asset is fully loaded and giving you access to
|
|
/// it.
|
|
///
|
|
/// Note that this requires you to `await` a future, so you must be in an
|
|
/// async context to use direct loading. In an asset loader, you will be in
|
|
/// an async context.
|
|
///
|
|
/// 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 of the asset 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.
|
|
///
|
|
/// # Load kickoff
|
|
///
|
|
/// 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.
|
|
///
|
|
/// If the current context is configured to not load dependencies automatically
|
|
/// (ex: [`AssetProcessor`]), a load will not be kicked off automatically. It is
|
|
/// then the calling context's responsibility to begin a load if necessary.
|
|
///
|
|
/// # Lifetimes
|
|
///
|
|
/// - `ctx`: the lifetime of the associated [`AssetServer`](crate::AssetServer) reference
|
|
/// - `builder`: the lifetime of the temporary builder structs
|
|
///
|
|
/// [`deferred`]: Self::deferred
|
|
/// [`immediate`]: Self::immediate
|
|
/// [`load`]: Self::load
|
|
/// [`with_dynamic_type`]: Self::with_dynamic_type
|
|
/// [`AssetServer::load`]: crate::AssetServer::load
|
|
/// [`AssetProcessor`]: crate::processor::AssetProcessor
|
|
/// [`Process`]: crate::processor::Process
|
|
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
|
|
pub struct NestedLoader<'ctx, 'builder, T, M> {
|
|
load_context: &'builder mut LoadContext<'ctx>,
|
|
meta_transform: Option<MetaTransform>,
|
|
typing: T,
|
|
mode: M,
|
|
}
|
|
|
|
mod sealed {
|
|
pub trait Typing {}
|
|
|
|
pub trait Mode {}
|
|
}
|
|
|
|
/// [`NestedLoader`] will be provided the type of asset as a type parameter on
|
|
/// [`load`].
|
|
///
|
|
/// [`load`]: NestedLoader::load
|
|
pub struct StaticTyped(());
|
|
|
|
impl sealed::Typing for StaticTyped {}
|
|
|
|
/// [`NestedLoader`] has been configured with info on what type of asset to load
|
|
/// at runtime.
|
|
pub struct DynamicTyped {
|
|
asset_type_id: TypeId,
|
|
}
|
|
|
|
impl sealed::Typing for DynamicTyped {}
|
|
|
|
/// [`NestedLoader`] does not know what type of asset it will be loading.
|
|
pub struct UnknownTyped(());
|
|
|
|
impl sealed::Typing for UnknownTyped {}
|
|
|
|
/// [`NestedLoader`] will create and return asset handles immediately, but only
|
|
/// actually load the asset later.
|
|
pub struct Deferred(());
|
|
|
|
impl sealed::Mode for Deferred {}
|
|
|
|
/// [`NestedLoader`] will immediately load an asset when requested.
|
|
pub struct Immediate<'builder, 'reader> {
|
|
reader: Option<&'builder mut (dyn Reader + 'reader)>,
|
|
}
|
|
|
|
impl sealed::Mode for Immediate<'_, '_> {}
|
|
|
|
// common to all states
|
|
|
|
impl<'ctx, 'builder> NestedLoader<'ctx, 'builder, StaticTyped, Deferred> {
|
|
pub(crate) fn new(load_context: &'builder mut LoadContext<'ctx>) -> Self {
|
|
NestedLoader {
|
|
load_context,
|
|
meta_transform: None,
|
|
typing: StaticTyped(()),
|
|
mode: Deferred(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'ctx, 'builder, T: sealed::Typing, M: sealed::Mode> NestedLoader<'ctx, 'builder, T, M> {
|
|
fn with_transform(
|
|
mut self,
|
|
transform: impl Fn(&mut dyn AssetMetaDyn) + Send + Sync + 'static,
|
|
) -> Self {
|
|
if let Some(prev_transform) = self.meta_transform {
|
|
self.meta_transform = Some(Box::new(move |meta| {
|
|
prev_transform(meta);
|
|
transform(meta);
|
|
}));
|
|
} else {
|
|
self.meta_transform = Some(Box::new(transform));
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Configure the settings used to load 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.
|
|
#[must_use]
|
|
pub fn with_settings<S: Settings>(
|
|
self,
|
|
settings: impl Fn(&mut S) + Send + Sync + 'static,
|
|
) -> Self {
|
|
self.with_transform(move |meta| meta_transform_settings(meta, &settings))
|
|
}
|
|
|
|
// convert between `T`s
|
|
|
|
/// When [`load`]ing, you must pass in the asset type as a type parameter
|
|
/// statically.
|
|
///
|
|
/// If you don't know the type statically (at compile time), consider
|
|
/// [`with_dynamic_type`] or [`with_unknown_type`].
|
|
///
|
|
/// [`load`]: Self::load
|
|
/// [`with_dynamic_type`]: Self::with_dynamic_type
|
|
/// [`with_unknown_type`]: Self::with_unknown_type
|
|
#[must_use]
|
|
pub fn with_static_type(self) -> NestedLoader<'ctx, 'builder, StaticTyped, M> {
|
|
NestedLoader {
|
|
load_context: self.load_context,
|
|
meta_transform: self.meta_transform,
|
|
typing: StaticTyped(()),
|
|
mode: self.mode,
|
|
}
|
|
}
|
|
|
|
/// When [`load`]ing, the loader will attempt to load an asset with the
|
|
/// given [`TypeId`].
|
|
///
|
|
/// [`load`]: Self::load
|
|
#[must_use]
|
|
pub fn with_dynamic_type(
|
|
self,
|
|
asset_type_id: TypeId,
|
|
) -> NestedLoader<'ctx, 'builder, DynamicTyped, M> {
|
|
NestedLoader {
|
|
load_context: self.load_context,
|
|
meta_transform: self.meta_transform,
|
|
typing: DynamicTyped { asset_type_id },
|
|
mode: self.mode,
|
|
}
|
|
}
|
|
|
|
/// When [`load`]ing, we will infer what type of asset to load from
|
|
/// metadata.
|
|
///
|
|
/// [`load`]: Self::load
|
|
#[must_use]
|
|
pub fn with_unknown_type(self) -> NestedLoader<'ctx, 'builder, UnknownTyped, M> {
|
|
NestedLoader {
|
|
load_context: self.load_context,
|
|
meta_transform: self.meta_transform,
|
|
typing: UnknownTyped(()),
|
|
mode: self.mode,
|
|
}
|
|
}
|
|
|
|
// convert between `M`s
|
|
|
|
/// When [`load`]ing, create only asset handles, rather than returning the
|
|
/// actual asset.
|
|
///
|
|
/// [`load`]: Self::load
|
|
pub fn deferred(self) -> NestedLoader<'ctx, 'builder, T, Deferred> {
|
|
NestedLoader {
|
|
load_context: self.load_context,
|
|
meta_transform: self.meta_transform,
|
|
typing: self.typing,
|
|
mode: Deferred(()),
|
|
}
|
|
}
|
|
|
|
/// The [`load`] call itself will load an asset, rather than scheduling the
|
|
/// loading to happen later.
|
|
///
|
|
/// This gives you access to the loaded asset, but requires you to be in an
|
|
/// async context, and be able to `await` the resulting future.
|
|
///
|
|
/// [`load`]: Self::load
|
|
#[must_use]
|
|
pub fn immediate<'c>(self) -> NestedLoader<'ctx, 'builder, T, Immediate<'builder, 'c>> {
|
|
NestedLoader {
|
|
load_context: self.load_context,
|
|
meta_transform: self.meta_transform,
|
|
typing: self.typing,
|
|
mode: Immediate { reader: None },
|
|
}
|
|
}
|
|
}
|
|
|
|
// deferred loading logic
|
|
|
|
impl NestedLoader<'_, '_, StaticTyped, Deferred> {
|
|
/// Retrieves a handle for the asset at the given path and adds that path as
|
|
/// a dependency of this asset.
|
|
///
|
|
/// This requires you to know the type of asset statically.
|
|
/// - If you have runtime info for what type of asset you're loading (e.g. a
|
|
/// [`TypeId`]), use [`with_dynamic_type`].
|
|
/// - If you do not know at all what type of asset you're loading, use
|
|
/// [`with_unknown_type`].
|
|
///
|
|
/// [`with_dynamic_type`]: Self::with_dynamic_type
|
|
/// [`with_unknown_type`]: Self::with_unknown_type
|
|
pub fn load<'c, A: Asset>(self, path: impl Into<AssetPath<'c>>) -> Handle<A> {
|
|
let path = path.into().to_owned();
|
|
let handle = if self.load_context.should_load_dependencies {
|
|
self.load_context
|
|
.asset_server
|
|
.load_with_meta_transform(path, self.meta_transform, ())
|
|
} else {
|
|
self.load_context
|
|
.asset_server
|
|
.get_or_create_path_handle(path, None)
|
|
};
|
|
self.load_context.dependencies.insert(handle.id().untyped());
|
|
handle
|
|
}
|
|
}
|
|
|
|
impl NestedLoader<'_, '_, DynamicTyped, Deferred> {
|
|
/// Retrieves a handle for the asset at the given path and adds that path as
|
|
/// a dependency of this asset.
|
|
///
|
|
/// This requires you to pass in the asset type ID into
|
|
/// [`with_dynamic_type`].
|
|
///
|
|
/// [`with_dynamic_type`]: Self::with_dynamic_type
|
|
pub fn load<'p>(self, path: impl Into<AssetPath<'p>>) -> UntypedHandle {
|
|
let path = path.into().to_owned();
|
|
let handle = if self.load_context.should_load_dependencies {
|
|
self.load_context
|
|
.asset_server
|
|
.load_erased_with_meta_transform(
|
|
path,
|
|
self.typing.asset_type_id,
|
|
self.meta_transform,
|
|
(),
|
|
)
|
|
} else {
|
|
self.load_context
|
|
.asset_server
|
|
.get_or_create_path_handle_erased(
|
|
path,
|
|
self.typing.asset_type_id,
|
|
self.meta_transform,
|
|
)
|
|
};
|
|
self.load_context.dependencies.insert(handle.id());
|
|
handle
|
|
}
|
|
}
|
|
|
|
impl NestedLoader<'_, '_, UnknownTyped, Deferred> {
|
|
/// Retrieves a handle for the asset at the given path and adds that path as
|
|
/// a dependency of this asset.
|
|
///
|
|
/// This will infer the asset type from metadata.
|
|
pub fn load<'p>(self, path: impl Into<AssetPath<'p>>) -> Handle<LoadedUntypedAsset> {
|
|
let path = path.into().to_owned();
|
|
let handle = if self.load_context.should_load_dependencies {
|
|
self.load_context
|
|
.asset_server
|
|
.load_unknown_type_with_meta_transform(path, self.meta_transform)
|
|
} else {
|
|
self.load_context
|
|
.asset_server
|
|
.get_or_create_path_handle(path, self.meta_transform)
|
|
};
|
|
self.load_context.dependencies.insert(handle.id().untyped());
|
|
handle
|
|
}
|
|
}
|
|
|
|
// immediate loading logic
|
|
|
|
impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> {
|
|
/// Specify the reader to use to read the asset data.
|
|
#[must_use]
|
|
pub fn with_reader(mut self, reader: &'builder mut (dyn Reader + 'reader)) -> Self {
|
|
self.mode.reader = Some(reader);
|
|
self
|
|
}
|
|
|
|
async fn load_internal(
|
|
self,
|
|
path: &AssetPath<'static>,
|
|
asset_type_id: Option<TypeId>,
|
|
) -> Result<(Arc<dyn ErasedAssetLoader>, ErasedLoadedAsset), LoadDirectError> {
|
|
let (mut meta, loader, mut reader) = if let Some(reader) = self.mode.reader {
|
|
let loader = if let Some(asset_type_id) = asset_type_id {
|
|
self.load_context
|
|
.asset_server
|
|
.get_asset_loader_with_asset_type_id(asset_type_id)
|
|
.await
|
|
.map_err(|error| LoadDirectError {
|
|
dependency: path.clone(),
|
|
error: error.into(),
|
|
})?
|
|
} else {
|
|
self.load_context
|
|
.asset_server
|
|
.get_path_asset_loader(path)
|
|
.await
|
|
.map_err(|error| LoadDirectError {
|
|
dependency: path.clone(),
|
|
error: error.into(),
|
|
})?
|
|
};
|
|
let meta = loader.default_meta();
|
|
(meta, loader, ReaderRef::Borrowed(reader))
|
|
} else {
|
|
let (meta, loader, reader) = self
|
|
.load_context
|
|
.asset_server
|
|
.get_meta_loader_and_reader(path, asset_type_id)
|
|
.await
|
|
.map_err(|error| LoadDirectError {
|
|
dependency: path.clone(),
|
|
error,
|
|
})?;
|
|
(meta, loader, ReaderRef::Boxed(reader))
|
|
};
|
|
|
|
if let Some(meta_transform) = self.meta_transform {
|
|
meta_transform(&mut *meta);
|
|
}
|
|
|
|
let asset = self
|
|
.load_context
|
|
.load_direct_internal(path.clone(), meta, &*loader, reader.as_mut())
|
|
.await?;
|
|
Ok((loader, asset))
|
|
}
|
|
}
|
|
|
|
impl NestedLoader<'_, '_, StaticTyped, Immediate<'_, '_>> {
|
|
/// Attempts to load the asset at the given `path` immediately.
|
|
///
|
|
/// This requires you to know the type of asset statically.
|
|
/// - If you have runtime info for what type of asset you're loading (e.g. a
|
|
/// [`TypeId`]), use [`with_dynamic_type`].
|
|
/// - If you do not know at all what type of asset you're loading, use
|
|
/// [`with_unknown_type`].
|
|
///
|
|
/// [`with_dynamic_type`]: Self::with_dynamic_type
|
|
/// [`with_unknown_type`]: Self::with_unknown_type
|
|
pub async fn load<'p, A: Asset>(
|
|
self,
|
|
path: impl Into<AssetPath<'p>>,
|
|
) -> Result<LoadedAsset<A>, LoadDirectError> {
|
|
let path = path.into().into_owned();
|
|
self.load_internal(&path, Some(TypeId::of::<A>()))
|
|
.await
|
|
.and_then(move |(loader, 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(),
|
|
},
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
impl NestedLoader<'_, '_, DynamicTyped, Immediate<'_, '_>> {
|
|
/// Attempts to load the asset at the given `path` immediately.
|
|
///
|
|
/// This requires you to pass in the asset type ID into
|
|
/// [`with_dynamic_type`].
|
|
///
|
|
/// [`with_dynamic_type`]: Self::with_dynamic_type
|
|
pub async fn load<'p>(
|
|
self,
|
|
path: impl Into<AssetPath<'p>>,
|
|
) -> Result<ErasedLoadedAsset, LoadDirectError> {
|
|
let path = path.into().into_owned();
|
|
let asset_type_id = Some(self.typing.asset_type_id);
|
|
self.load_internal(&path, asset_type_id)
|
|
.await
|
|
.map(|(_, asset)| asset)
|
|
}
|
|
}
|
|
|
|
impl NestedLoader<'_, '_, UnknownTyped, Immediate<'_, '_>> {
|
|
/// Attempts to load the asset at the given `path` immediately.
|
|
///
|
|
/// This will infer the asset type from metadata.
|
|
pub async fn load<'p>(
|
|
self,
|
|
path: impl Into<AssetPath<'p>>,
|
|
) -> Result<ErasedLoadedAsset, LoadDirectError> {
|
|
let path = path.into().into_owned();
|
|
self.load_internal(&path, None)
|
|
.await
|
|
.map(|(_, asset)| asset)
|
|
}
|
|
}
|