use crate::{
io::{AssetWriterError, MissingAssetSourceError, MissingAssetWriterError, Writer},
meta::{AssetAction, AssetMeta, AssetMetaDyn, Settings},
transformer::TransformedAsset,
Asset, AssetContainer, AssetLoader, AssetPath, AssetServer, ErasedLoadedAsset, Handle,
LabeledAsset, UntypedHandle,
};
use alloc::{boxed::Box, string::ToString, sync::Arc};
use atomicow::CowArc;
use bevy_ecs::error::BevyError;
use bevy_platform::collections::HashMap;
use bevy_reflect::TypePath;
use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
use core::{any::TypeId, borrow::Borrow, ops::Deref};
use futures_lite::AsyncWriteExt;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub trait AssetSaver: TypePath + Send + Sync + 'static {
type Asset: Asset;
type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
type OutputLoader: AssetLoader;
type Error: Into<BevyError>;
fn save(
&self,
writer: &mut Writer,
asset: SavedAsset<'_, '_, Self::Asset>,
settings: &Self::Settings,
) -> impl ConditionalSendFuture<
Output = Result<<Self::OutputLoader as AssetLoader>::Settings, Self::Error>,
>;
}
pub trait ErasedAssetSaver: Send + Sync + 'static {
fn save<'a>(
&'a self,
writer: &'a mut Writer,
asset: &'a ErasedLoadedAsset,
settings: &'a dyn Settings,
) -> BoxedFuture<'a, Result<(), BevyError>>;
fn type_name(&self) -> &'static str;
}
impl<S: AssetSaver> ErasedAssetSaver for S {
fn save<'a>(
&'a self,
writer: &'a mut Writer,
asset: &'a ErasedLoadedAsset,
settings: &'a dyn Settings,
) -> BoxedFuture<'a, Result<(), BevyError>> {
Box::pin(async move {
let settings = settings
.downcast_ref::<S::Settings>()
.expect("AssetLoader settings should match the loader type");
let saved_asset = SavedAsset::<S::Asset>::from_loaded(asset).unwrap();
if let Err(err) = self.save(writer, saved_asset, settings).await {
return Err(err.into());
}
Ok(())
})
}
fn type_name(&self) -> &'static str {
core::any::type_name::<S>()
}
}
#[derive(Clone)]
pub struct SavedAsset<'a, 'b, A: Asset> {
value: &'a A,
labeled_assets: Moo<'b, HashMap<CowArc<'a, str>, LabeledSavedAsset<'a>>>,
}
impl<A: Asset> Deref for SavedAsset<'_, '_, A> {
type Target = A;
fn deref(&self) -> &Self::Target {
self.value
}
}
impl<'a, 'b, A: Asset> SavedAsset<'a, 'b, A> {
fn from_value_and_labeled_saved_assets(
value: &'a A,
labeled_saved_assets: &'b HashMap<CowArc<'a, str>, LabeledSavedAsset<'a>>,
) -> Self {
Self {
value,
labeled_assets: Moo::Borrowed(labeled_saved_assets),
}
}
fn from_value_and_labeled_assets(
value: &'a A,
labeled_assets: &'a HashMap<CowArc<'static, str>, LabeledAsset>,
) -> Self {
Self {
value,
labeled_assets: Moo::Owned(
labeled_assets
.iter()
.map(|(label, labeled_asset)| {
(
CowArc::Borrowed(label.borrow()),
LabeledSavedAsset::from_labeled_asset(labeled_asset),
)
})
.collect(),
),
}
}
pub fn from_loaded(asset: &'a ErasedLoadedAsset) -> Option<Self> {
let value = asset.value.downcast_ref::<A>()?;
Some(Self::from_value_and_labeled_assets(
value,
&asset.labeled_assets,
))
}
pub fn from_transformed(asset: &'a TransformedAsset<A>) -> Self {
Self::from_value_and_labeled_assets(&asset.value, &asset.labeled_assets)
}
pub fn from_asset(value: &'a A) -> Self {
Self {
value,
labeled_assets: Moo::Owned(HashMap::default()),
}
}
pub fn upcast(self) -> ErasedSavedAsset<'a, 'a>
where
'b: 'a,
{
ErasedSavedAsset {
value: self.value,
labeled_assets: self.labeled_assets,
}
}
#[inline]
pub fn get(&self) -> &'a A {
self.value
}
pub fn get_labeled<B: Asset>(&self, label: impl AsRef<str>) -> Option<SavedAsset<'a, '_, B>> {
let labeled = self.labeled_assets.get(label.as_ref())?;
labeled.asset.downcast()
}
pub fn get_erased_labeled(&self, label: impl AsRef<str>) -> Option<&ErasedSavedAsset<'a, '_>> {
let labeled = self.labeled_assets.get(label.as_ref())?;
Some(&labeled.asset)
}
pub fn get_untyped_handle(&self, label: impl AsRef<str>) -> Option<UntypedHandle> {
let labeled = self.labeled_assets.get(label.as_ref())?;
Some(labeled.handle.clone())
}
pub fn get_handle<B: Asset>(&self, label: impl AsRef<str>) -> Option<Handle<B>> {
let labeled = self.labeled_assets.get(label.as_ref())?;
if let Ok(handle) = labeled.handle.clone().try_typed::<B>() {
return Some(handle);
}
None
}
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
self.labeled_assets.keys().map(|s| &**s)
}
}
#[derive(Clone)]
pub struct ErasedSavedAsset<'a: 'b, 'b> {
value: &'a dyn AssetContainer,
labeled_assets: Moo<'b, HashMap<CowArc<'a, str>, LabeledSavedAsset<'a>>>,
}
impl<'a> ErasedSavedAsset<'a, '_> {
fn from_loaded(asset: &'a ErasedLoadedAsset) -> Self {
Self {
value: &*asset.value,
labeled_assets: Moo::Owned(
asset
.labeled_assets
.iter()
.map(|(label, asset)| {
(
CowArc::Borrowed(label.borrow()),
LabeledSavedAsset::from_labeled_asset(asset),
)
})
.collect(),
),
}
}
}
impl<'a> ErasedSavedAsset<'a, '_> {
pub fn downcast<'b, A: Asset>(&'b self) -> Option<SavedAsset<'a, 'b, A>> {
let value = self.value.downcast_ref::<A>()?;
Some(SavedAsset::from_value_and_labeled_saved_assets(
value,
&self.labeled_assets,
))
}
}
#[derive(Clone)]
struct LabeledSavedAsset<'a> {
asset: ErasedSavedAsset<'a, 'a>,
handle: UntypedHandle,
}
impl<'a> LabeledSavedAsset<'a> {
fn from_labeled_asset(asset: &'a LabeledAsset) -> Self {
Self {
asset: ErasedSavedAsset::from_loaded(&asset.asset),
handle: asset.handle.clone(),
}
}
}
pub struct SavedAssetBuilder<'a> {
labeled_assets: HashMap<CowArc<'a, str>, LabeledSavedAsset<'a>>,
asset_path: AssetPath<'static>,
asset_server: AssetServer,
}
impl<'a> SavedAssetBuilder<'a> {
pub fn new(asset_server: AssetServer, mut asset_path: AssetPath<'static>) -> Self {
asset_path.remove_label();
Self {
asset_server,
asset_path,
labeled_assets: Default::default(),
}
}
#[must_use]
pub fn add_labeled_asset_with_new_handle<'b: 'a, A: Asset>(
&mut self,
label: impl Into<CowArc<'b, str>>,
asset: SavedAsset<'a, 'a, A>,
) -> Handle<A> {
let label = label.into();
let handle = Handle::Strong(
self.asset_server
.read_infos()
.handle_providers
.get(&TypeId::of::<A>())
.expect("asset type has been initialized")
.reserve_handle_internal(
false,
Some(self.asset_path.clone().with_label(label.to_string())),
None,
),
);
self.add_labeled_asset_with_existing_handle(label, asset, handle.clone());
handle
}
pub fn add_labeled_asset_with_existing_handle<'b: 'a, A: Asset>(
&mut self,
label: impl Into<CowArc<'b, str>>,
asset: SavedAsset<'a, 'a, A>,
handle: Handle<A>,
) {
self.add_labeled_asset_with_existing_handle_erased(
label.into(),
asset.upcast(),
handle.untyped(),
);
}
#[must_use]
pub fn add_labeled_asset_with_new_handle_erased<'b: 'a>(
&mut self,
label: impl Into<CowArc<'b, str>>,
asset: ErasedSavedAsset<'a, 'a>,
) -> UntypedHandle {
let label = label.into();
let handle = UntypedHandle::Strong(
self.asset_server
.read_infos()
.handle_providers
.get(&asset.value.type_id())
.expect("asset type has been initialized")
.reserve_handle_internal(
false,
Some(self.asset_path.clone().with_label(label.to_string())),
None,
),
);
self.add_labeled_asset_with_existing_handle_erased(label, asset, handle.clone());
handle
}
pub fn add_labeled_asset_with_existing_handle_erased<'b: 'a>(
&mut self,
label: impl Into<CowArc<'b, str>>,
asset: ErasedSavedAsset<'a, 'a>,
handle: UntypedHandle,
) {
self.labeled_assets
.insert(label.into(), LabeledSavedAsset { asset, handle });
}
pub fn build<'b, A: Asset>(self, asset: &'b A) -> SavedAsset<'b, 'b, A>
where
'a: 'b,
{
SavedAsset {
value: asset,
labeled_assets: Moo::Owned(self.labeled_assets),
}
}
}
#[derive(Clone)]
enum Moo<'a, T> {
Owned(T),
Borrowed(&'a T),
}
impl<T> Deref for Moo<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
Self::Owned(t) => t,
Self::Borrowed(t) => t,
}
}
}
pub async fn save_using_saver<S: AssetSaver>(
asset_server: AssetServer,
saver: &S,
path: &AssetPath<'_>,
asset: SavedAsset<'_, '_, S::Asset>,
settings: &S::Settings,
) -> Result<(), SaveAssetError> {
let source = asset_server.get_source(path.source())?;
let writer = source.writer()?;
let mut file_writer = writer.write(path.path()).await?;
let loader_settings = saver
.save(&mut file_writer, asset, settings)
.await
.map_err(|err| SaveAssetError::SaverError(Arc::new(err.into())))?;
file_writer.flush().await.map_err(AssetWriterError::Io)?;
let meta = AssetMeta::<S::OutputLoader, ()>::new(AssetAction::Load {
loader: S::OutputLoader::type_path().into(),
settings: loader_settings,
});
let meta = AssetMetaDyn::serialize(&meta);
writer.write_meta_bytes(path.path(), &meta).await?;
Ok(())
}
#[derive(Error, Debug)]
pub enum SaveAssetError {
#[error(transparent)]
MissingSource(#[from] MissingAssetSourceError),
#[error(transparent)]
MissingWriter(#[from] MissingAssetWriterError),
#[error(transparent)]
WriterError(#[from] AssetWriterError),
#[error("Failed to save asset due to error from saver: {0}")]
SaverError(Arc<BevyError>),
}
#[cfg(test)]
pub(crate) mod tests {
use alloc::{string::ToString, vec, vec::Vec};
use bevy_reflect::TypePath;
use bevy_tasks::block_on;
use futures_lite::AsyncWriteExt;
use ron::ser::PrettyConfig;
use crate::{
saver::{save_using_saver, AssetSaver, SavedAsset, SavedAssetBuilder},
tests::{create_app, run_app_until, CoolText, CoolTextLoader, CoolTextRon, SubText},
AssetApp, AssetServer, Assets,
};
fn new_subtext(text: &str) -> SubText {
SubText {
text: text.to_string(),
}
}
#[derive(TypePath)]
pub struct CoolTextSaver;
impl AssetSaver for CoolTextSaver {
type Asset = CoolText;
type Settings = ();
type OutputLoader = CoolTextLoader;
type Error = std::io::Error;
async fn save(
&self,
writer: &mut crate::io::Writer,
asset: SavedAsset<'_, '_, Self::Asset>,
_: &Self::Settings,
) -> Result<(), Self::Error> {
assert!(asset.embedded.is_empty());
let ron = CoolTextRon {
text: asset.text.clone(),
sub_texts: asset
.iter_labels()
.map(|label| asset.get_labeled::<SubText>(label).unwrap().text.clone())
.collect(),
dependencies: asset
.dependencies
.iter()
.map(|handle| handle.path().unwrap().path())
.map(|path| path.to_str().unwrap().to_string())
.collect(),
embedded_dependencies: vec![],
};
let ron = ron::ser::to_string_pretty(&ron, PrettyConfig::new().new_line("\n")).unwrap();
writer.write_all(ron.as_bytes()).await?;
Ok(())
}
}
#[test]
fn builds_saved_asset_for_new_asset() {
let mut app = create_app().0;
app.init_asset::<CoolText>()
.init_asset::<SubText>()
.register_asset_loader(CoolTextLoader);
app.update();
app.update();
app.update();
let hiya_subasset = new_subtext("hiya");
let goodbye_subasset = new_subtext("goodbye");
let idk_subasset = new_subtext("idk");
let asset_server = app.world().resource::<AssetServer>().clone();
let mut saved_asset_builder =
SavedAssetBuilder::new(asset_server.clone(), "some/target/path.cool.ron".into());
let hiya_handle = saved_asset_builder
.add_labeled_asset_with_new_handle("hiya", SavedAsset::from_asset(&hiya_subasset));
let goodbye_handle = saved_asset_builder.add_labeled_asset_with_new_handle(
"goodbye",
SavedAsset::from_asset(&goodbye_subasset),
);
let idk_handle = saved_asset_builder
.add_labeled_asset_with_new_handle("idk", SavedAsset::from_asset(&idk_subasset));
let main_asset = CoolText {
text: "wassup".into(),
sub_texts: vec![hiya_handle, goodbye_handle, idk_handle],
..Default::default()
};
let saved_asset = saved_asset_builder.build(&main_asset);
let mut asset_labels = saved_asset
.labeled_assets
.keys()
.map(|label| label.as_ref().to_string())
.collect::<Vec<_>>();
asset_labels.sort();
assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]);
{
let asset_server = asset_server.clone();
block_on(async move {
save_using_saver(
asset_server,
&CoolTextSaver,
&"some/target/path.cool.ron".into(),
saved_asset,
&(),
)
.await
})
.unwrap();
}
let readback = asset_server.load("some/target/path.cool.ron");
run_app_until(&mut app, |_| {
asset_server.is_loaded(&readback).then_some(())
});
let cool_text = app
.world()
.resource::<Assets<CoolText>>()
.get(&readback)
.unwrap();
let subtexts = app.world().resource::<Assets<SubText>>();
let mut asset_labels = cool_text
.sub_texts
.iter()
.map(|handle| subtexts.get(handle).unwrap().text.clone())
.collect::<Vec<_>>();
asset_labels.sort();
assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]);
}
#[test]
fn builds_saved_asset_for_existing_asset() {
let (mut app, _) = create_app();
app.init_asset::<CoolText>()
.init_asset::<SubText>()
.register_asset_loader(CoolTextLoader);
let mut subtexts = app.world_mut().resource_mut::<Assets<SubText>>();
let hiya_handle = subtexts.add(new_subtext("hiya"));
let goodbye_handle = subtexts.add(new_subtext("goodbye"));
let idk_handle = subtexts.add(new_subtext("idk"));
let mut cool_texts = app.world_mut().resource_mut::<Assets<CoolText>>();
let cool_text_handle = cool_texts.add(CoolText {
text: "wassup".into(),
sub_texts: vec![
hiya_handle.clone(),
goodbye_handle.clone(),
idk_handle.clone(),
],
..Default::default()
});
let subtexts = app.world().resource::<Assets<SubText>>();
let cool_texts = app.world().resource::<Assets<CoolText>>();
let asset_server = app.world().resource::<AssetServer>().clone();
let mut saved_asset_builder =
SavedAssetBuilder::new(asset_server.clone(), "some/target/path.cool.ron".into());
saved_asset_builder.add_labeled_asset_with_existing_handle(
"hiya",
SavedAsset::from_asset(subtexts.get(&hiya_handle).unwrap()),
hiya_handle,
);
saved_asset_builder.add_labeled_asset_with_existing_handle(
"goodbye",
SavedAsset::from_asset(subtexts.get(&goodbye_handle).unwrap()),
goodbye_handle,
);
saved_asset_builder.add_labeled_asset_with_existing_handle(
"idk",
SavedAsset::from_asset(subtexts.get(&idk_handle).unwrap()),
idk_handle,
);
let saved_asset = saved_asset_builder.build(cool_texts.get(&cool_text_handle).unwrap());
let mut asset_labels = saved_asset
.labeled_assets
.keys()
.map(|label| label.as_ref().to_string())
.collect::<Vec<_>>();
asset_labels.sort();
assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]);
{
let asset_server = asset_server.clone();
block_on(async move {
save_using_saver(
asset_server,
&CoolTextSaver,
&"some/target/path.cool.ron".into(),
saved_asset,
&(),
)
.await
})
.unwrap();
}
let readback = asset_server.load("some/target/path.cool.ron");
run_app_until(&mut app, |_| {
asset_server.is_loaded(&readback).then_some(())
});
let cool_text = app
.world()
.resource::<Assets<CoolText>>()
.get(&readback)
.unwrap();
let subtexts = app.world().resource::<Assets<SubText>>();
let mut asset_labels = cool_text
.sub_texts
.iter()
.map(|handle| subtexts.get(handle).unwrap().text.clone())
.collect::<Vec<_>>();
asset_labels.sort();
assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]);
}
}