Skip to main content

bevy_asset/
saver.rs

1//! Tools to save processed [`Asset`]s to a byte format that can then be written with [`AssetWriter`].
2//!
3//! See [`AssetSaver`] for details.
4//!
5//! [`AssetWriter`]: crate::io::AssetWriter
6
7use crate::{
8    io::{AssetWriterError, MissingAssetSourceError, MissingAssetWriterError, Writer},
9    meta::{AssetAction, AssetMeta, AssetMetaDyn, Settings},
10    transformer::TransformedAsset,
11    Asset, AssetContainer, AssetId, AssetLoader, AssetPath, AssetServer, ErasedLoadedAsset, Handle,
12    LabeledAsset, UntypedAssetId, UntypedHandle,
13};
14use alloc::{boxed::Box, string::ToString, sync::Arc, vec::Vec};
15use atomicow::CowArc;
16use bevy_ecs::error::BevyError;
17use bevy_platform::collections::{hash_map::Entry, HashMap};
18use bevy_reflect::TypePath;
19use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
20use core::{any::TypeId, borrow::Borrow, ops::Deref};
21use futures_lite::AsyncWriteExt;
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24
25/// Saves an [`Asset`] of a given [`AssetSaver::Asset`] type. [`AssetSaver::OutputLoader`] will then be used to load the saved asset
26/// in the final deployed application. The saver should produce asset bytes in a format that [`AssetSaver::OutputLoader`] can read.
27///
28/// This trait is generally used in concert with [`AssetWriter`](crate::io::AssetWriter) to write assets as bytes.
29///
30/// For a version of this trait that can load assets, see [`AssetLoader`].
31///
32/// Note: This is currently only leveraged by the [`AssetProcessor`](crate::processor::AssetProcessor), and does not provide a
33/// suitable interface for general purpose asset persistence. See [github issue #11216](https://github.com/bevyengine/bevy/issues/11216).
34///
35pub trait AssetSaver: TypePath + Send + Sync + 'static {
36    /// The top level [`Asset`] saved by this [`AssetSaver`].
37    type Asset: Asset;
38    /// The settings type used by this [`AssetSaver`].
39    type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
40    /// The type of [`AssetLoader`] used to load this [`Asset`]
41    type OutputLoader: AssetLoader;
42    /// The type of [error](`std::error::Error`) which could be encountered by this saver.
43    type Error: Into<BevyError>;
44
45    /// Saves the given runtime [`Asset`] by writing it to a byte format using `writer`. The passed in `settings` can influence how the
46    /// `asset` is saved.
47    fn save(
48        &self,
49        writer: &mut Writer,
50        asset: SavedAsset<'_, '_, Self::Asset>,
51        settings: &Self::Settings,
52        asset_path: AssetPath<'_>,
53    ) -> impl ConditionalSendFuture<
54        Output = Result<<Self::OutputLoader as AssetLoader>::Settings, Self::Error>,
55    >;
56}
57
58/// A type-erased dynamic variant of [`AssetSaver`] that allows callers to save assets without knowing the actual type of the [`AssetSaver`].
59pub trait ErasedAssetSaver: Send + Sync + 'static {
60    /// Saves the given runtime [`ErasedLoadedAsset`] by writing it to a byte format using `writer`. The passed in `settings` can influence how the
61    /// `asset` is saved.
62    fn save<'a>(
63        &'a self,
64        writer: &'a mut Writer,
65        asset: &'a ErasedLoadedAsset,
66        settings: &'a dyn Settings,
67        asset_path: AssetPath<'a>,
68    ) -> BoxedFuture<'a, Result<(), BevyError>>;
69
70    /// The type name of the [`AssetSaver`].
71    fn type_name(&self) -> &'static str;
72}
73
74impl<S: AssetSaver> ErasedAssetSaver for S {
75    fn save<'a>(
76        &'a self,
77        writer: &'a mut Writer,
78        asset: &'a ErasedLoadedAsset,
79        settings: &'a dyn Settings,
80        asset_path: AssetPath<'a>,
81    ) -> BoxedFuture<'a, Result<(), BevyError>> {
82        Box::pin(async move {
83            let settings = settings
84                .downcast_ref::<S::Settings>()
85                .expect("AssetLoader settings should match the loader type");
86            let saved_asset = SavedAsset::<S::Asset>::from_loaded(asset).unwrap();
87            if let Err(err) = self.save(writer, saved_asset, settings, asset_path).await {
88                return Err(err.into());
89            }
90            Ok(())
91        })
92    }
93    fn type_name(&self) -> &'static str {
94        core::any::type_name::<S>()
95    }
96}
97
98/// An [`Asset`] (and any labeled "sub assets") intended to be saved.
99#[derive(Clone)]
100pub struct SavedAsset<'a, 'b, A: Asset> {
101    value: &'a A,
102    labeled_assets: Moo<'b, Vec<LabeledSavedAsset<'a>>>,
103    label_to_asset_index: Moo<'b, HashMap<CowArc<'a, str>, usize>>,
104    /// The mapping from a subasset asset IDs to their index in [`Self::labeled_assets`].
105    ///
106    /// This is entirely redundant with [`Self::labeled_assets`], but it allows looking up the
107    /// labeled asset by its asset ID.
108    asset_id_to_asset_index: Moo<'b, HashMap<UntypedAssetId, usize>>,
109}
110
111impl<A: Asset> Deref for SavedAsset<'_, '_, A> {
112    type Target = A;
113
114    fn deref(&self) -> &Self::Target {
115        self.value
116    }
117}
118
119impl<'a, 'b, A: Asset> SavedAsset<'a, 'b, A> {
120    fn from_value_and_labeled_saved_assets(
121        value: &'a A,
122        labeled_saved_assets: &'b Vec<LabeledSavedAsset<'a>>,
123        label_to_asset_index: &'b HashMap<CowArc<'a, str>, usize>,
124        asset_id_to_asset_index: &'b HashMap<UntypedAssetId, usize>,
125    ) -> Self {
126        Self {
127            value,
128            labeled_assets: Moo::Borrowed(labeled_saved_assets),
129            label_to_asset_index: Moo::Borrowed(label_to_asset_index),
130            asset_id_to_asset_index: Moo::Borrowed(asset_id_to_asset_index),
131        }
132    }
133
134    fn from_value_and_labeled_assets(
135        value: &'a A,
136        labeled_assets: &'a [LabeledAsset],
137        label_to_asset_index: &'a HashMap<CowArc<'static, str>, usize>,
138        asset_id_to_asset_index: &'a HashMap<UntypedAssetId, usize>,
139    ) -> Self {
140        Self {
141            value,
142            labeled_assets: Moo::Owned(
143                labeled_assets
144                    .iter()
145                    .map(LabeledSavedAsset::from_labeled_asset)
146                    .collect(),
147            ),
148            label_to_asset_index: Moo::Owned(
149                label_to_asset_index
150                    .iter()
151                    .map(|(label, &index)| (CowArc::Borrowed(label.borrow()), index))
152                    .collect(),
153            ),
154            asset_id_to_asset_index: Moo::Borrowed(asset_id_to_asset_index),
155        }
156    }
157
158    /// Creates a new [`SavedAsset`] from `asset` if its internal value matches `A`.
159    pub fn from_loaded(asset: &'a ErasedLoadedAsset) -> Option<Self> {
160        let value = asset.value.downcast_ref::<A>()?;
161        Some(Self::from_value_and_labeled_assets(
162            value,
163            &asset.labeled_assets,
164            &asset.label_to_asset_index,
165            &asset.asset_id_to_asset_index,
166        ))
167    }
168
169    /// Creates a new [`SavedAsset`] from the a [`TransformedAsset`]
170    pub fn from_transformed(asset: &'a TransformedAsset<A>) -> Self {
171        Self::from_value_and_labeled_assets(
172            &asset.value,
173            &asset.labeled_assets,
174            &asset.label_to_asset_index,
175            &asset.asset_id_to_asset_index,
176        )
177    }
178
179    /// Creates a new [`SavedAsset`] holding only the provided value with no labeled assets.
180    pub fn from_asset(value: &'a A) -> Self {
181        Self {
182            value,
183            labeled_assets: Moo::Owned(Vec::default()),
184            label_to_asset_index: Moo::Owned(HashMap::default()),
185            asset_id_to_asset_index: Moo::Owned(HashMap::default()),
186        }
187    }
188
189    /// Casts this typed asset into its type-erased form.
190    pub fn upcast(self) -> ErasedSavedAsset<'a, 'a>
191    where
192        'b: 'a,
193    {
194        ErasedSavedAsset {
195            value: self.value,
196            labeled_assets: self.labeled_assets,
197            label_to_asset_index: self.label_to_asset_index,
198            asset_id_to_asset_index: self.asset_id_to_asset_index,
199        }
200    }
201
202    /// Retrieves the value of this asset.
203    #[inline]
204    pub fn get(&self) -> &'a A {
205        self.value
206    }
207
208    /// Returns the labeled asset, if it exists and matches this type.
209    pub fn get_labeled<B: Asset>(&self, label: impl AsRef<str>) -> Option<SavedAsset<'a, '_, B>> {
210        let index = self.label_to_asset_index.get(label.as_ref())?;
211        let labeled = &self.labeled_assets[*index];
212        labeled.asset.downcast()
213    }
214
215    /// Returns the type-erased labeled asset, if it exists and matches this type.
216    pub fn get_erased_labeled(&self, label: impl AsRef<str>) -> Option<&ErasedSavedAsset<'a, '_>> {
217        let index = self.label_to_asset_index.get(label.as_ref())?;
218        let labeled = &self.labeled_assets[*index];
219        Some(&labeled.asset)
220    }
221
222    /// Returns the labeled asset given its asset ID if it exists and matches the type.
223    ///
224    /// This can be used to get the asset from its handle since `&Handle` implements
225    /// [`Into<AssetId<B>>`].
226    pub fn get_labeled_by_id<B: Asset>(
227        &self,
228        id: impl Into<AssetId<B>>,
229    ) -> Option<SavedAsset<'a, '_, B>> {
230        let index = self.asset_id_to_asset_index.get(&id.into().untyped())?;
231        let labeled = &self.labeled_assets[*index];
232        labeled.asset.downcast()
233    }
234
235    /// Returns the type-erased labeled asset given its asset ID if it exists.
236    ///
237    /// This can be used to get the asset from its handle since `&UntypedHandle` implements
238    /// [`Into<UntypedAssetId>`].
239    pub fn get_erased_labeled_by_id(
240        &self,
241        id: impl Into<UntypedAssetId>,
242    ) -> Option<&ErasedSavedAsset<'a, '_>> {
243        let index = self.asset_id_to_asset_index.get(&id.into())?;
244        let labeled = &self.labeled_assets[*index];
245        Some(&labeled.asset)
246    }
247
248    /// Returns the [`UntypedHandle`] of the labeled asset with the provided 'label', if it exists.
249    pub fn get_untyped_handle(&self, label: impl AsRef<str>) -> Option<UntypedHandle> {
250        let index = self.label_to_asset_index.get(label.as_ref())?;
251        let labeled = &self.labeled_assets[*index];
252        Some(labeled.handle.clone())
253    }
254
255    /// Returns the [`Handle`] of the labeled asset with the provided 'label', if it exists and is an asset of type `B`
256    pub fn get_handle<B: Asset>(&self, label: impl AsRef<str>) -> Option<Handle<B>> {
257        let index = self.label_to_asset_index.get(label.as_ref())?;
258        let labeled = &self.labeled_assets[*index];
259        if let Ok(handle) = labeled.handle.clone().try_typed::<B>() {
260            return Some(handle);
261        }
262        None
263    }
264
265    /// Iterate over all labels for "labeled assets" in the loaded asset
266    pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
267        self.label_to_asset_index.keys().map(|s| &**s)
268    }
269}
270
271#[derive(Clone)]
272pub struct ErasedSavedAsset<'a: 'b, 'b> {
273    value: &'a dyn AssetContainer,
274    labeled_assets: Moo<'b, Vec<LabeledSavedAsset<'a>>>,
275    label_to_asset_index: Moo<'b, HashMap<CowArc<'a, str>, usize>>,
276    /// The mapping from a subasset asset IDs to their index in [`Self::labeled_assets`].
277    ///
278    /// This is entirely redundant with [`Self::labeled_assets`], but it allows looking up the
279    /// labeled asset by its asset ID.
280    asset_id_to_asset_index: Moo<'b, HashMap<UntypedAssetId, usize>>,
281}
282
283impl<'a> ErasedSavedAsset<'a, '_> {
284    fn from_loaded(asset: &'a ErasedLoadedAsset) -> Self {
285        Self {
286            value: &*asset.value,
287            labeled_assets: Moo::Owned(
288                asset
289                    .labeled_assets
290                    .iter()
291                    .map(LabeledSavedAsset::from_labeled_asset)
292                    .collect(),
293            ),
294            label_to_asset_index: Moo::Owned(
295                asset
296                    .label_to_asset_index
297                    .iter()
298                    .map(|(label, &index)| (CowArc::Borrowed(label.borrow()), index))
299                    .collect(),
300            ),
301            asset_id_to_asset_index: Moo::Borrowed(&asset.asset_id_to_asset_index),
302        }
303    }
304}
305
306impl<'a> ErasedSavedAsset<'a, '_> {
307    /// Attempts to downcast this erased asset into type `A`.
308    ///
309    /// Returns [`None`] if the asset is the wrong type.
310    pub fn downcast<'b, A: Asset>(&'b self) -> Option<SavedAsset<'a, 'b, A>> {
311        let value = self.value.downcast_ref::<A>()?;
312        Some(SavedAsset::from_value_and_labeled_saved_assets(
313            value,
314            &self.labeled_assets,
315            &self.label_to_asset_index,
316            &self.asset_id_to_asset_index,
317        ))
318    }
319}
320
321/// Container for a single labeled asset (which also includes its labeled assets, for nested
322/// assets).
323#[derive(Clone)]
324struct LabeledSavedAsset<'a> {
325    /// The asset and its labeled assets.
326    asset: ErasedSavedAsset<'a, 'a>,
327    /// The handle of this labeled asset.
328    handle: UntypedHandle,
329}
330
331impl<'a> LabeledSavedAsset<'a> {
332    /// Creates an instance that corresponds to the same data as [`LabeledAsset`].
333    fn from_labeled_asset(asset: &'a LabeledAsset) -> Self {
334        Self {
335            asset: ErasedSavedAsset::from_loaded(&asset.asset),
336            handle: asset.handle.clone(),
337        }
338    }
339}
340
341/// A builder for creating [`SavedAsset`] instances (for use with asset saving).
342///
343/// This is commonly used in tandem with [`save_using_saver`].
344pub struct SavedAssetBuilder<'a> {
345    /// The labeled assets for this saved asset.
346    labeled_assets: Vec<LabeledSavedAsset<'a>>,
347    /// Maps the labels of subassets to their index in [`Self::labeled_assets`].
348    label_to_asset_index: HashMap<CowArc<'a, str>, usize>,
349    /// The mapping from a subasset asset IDs to their index in [`Self::labeled_assets`].
350    ///
351    /// This is entirely redundant with [`Self::labeled_assets`], but it allows looking up the
352    /// labeled asset by its asset ID.
353    asset_id_to_asset_index: HashMap<UntypedAssetId, usize>,
354    /// The asset path (with no label) that this saved asset is "tied" to.
355    ///
356    /// All labeled assets will use this asset path (with their substituted labels). Note labeled
357    /// assets **of labeled assets** may not use the same asset path (to represent nested-loaded
358    /// assets).
359    asset_path: AssetPath<'static>,
360    /// The asset server to use for creating handles.
361    asset_server: AssetServer,
362}
363
364impl<'a> SavedAssetBuilder<'a> {
365    /// Creates a new builder for the given `asset_path` and using the `asset_server` to back its
366    /// handles.
367    pub fn new(asset_server: AssetServer, mut asset_path: AssetPath<'static>) -> Self {
368        asset_path.remove_label();
369        Self {
370            asset_server,
371            asset_path,
372            labeled_assets: Default::default(),
373            label_to_asset_index: Default::default(),
374            asset_id_to_asset_index: Default::default(),
375        }
376    }
377
378    /// Adds a labeled asset, creates a handle for it, and returns the handle (for use in creating
379    /// an asset).
380    ///
381    /// This is primarily used when **constructing** a new asset to be saved. Since assets commonly
382    /// store handles to their subassets, this function returns a handle that can be stored in your
383    /// root asset.
384    ///
385    /// If you already have a root asset instance (which already contains a subasset handle), use
386    /// [`Self::add_labeled_asset_with_existing_handle`] instead.
387    #[must_use]
388    pub fn add_labeled_asset_with_new_handle<'b: 'a, A: Asset>(
389        &mut self,
390        label: impl Into<CowArc<'b, str>>,
391        asset: SavedAsset<'a, 'a, A>,
392    ) -> Handle<A> {
393        let label = label.into();
394        let handle = Handle::Strong(
395            self.asset_server
396                .read_infos()
397                .handle_providers
398                .get(&TypeId::of::<A>())
399                .expect("asset type has been initialized")
400                .reserve_handle_internal(
401                    false,
402                    Some(self.asset_path.clone().with_label(label.to_string())),
403                ),
404        );
405        self.add_labeled_asset_with_existing_handle(label, asset, handle.clone());
406        handle
407    }
408
409    /// Adds a labeled asset with a pre-existing handle.
410    ///
411    /// This is primarily used when attempting to save a (root) asset that you already have an
412    /// instance of. Since this root asset instance already must have its fields populated
413    /// (including any subasset handles), this function allows you to record the subasset that
414    /// should be associated with that handle.
415    ///
416    /// If you do not have a root asset instance (you're creating one from scratch), use
417    /// [`Self::add_labeled_asset_with_new_handle`] instead.
418    pub fn add_labeled_asset_with_existing_handle<'b: 'a, A: Asset>(
419        &mut self,
420        label: impl Into<CowArc<'b, str>>,
421        asset: SavedAsset<'a, 'a, A>,
422        handle: Handle<A>,
423    ) {
424        self.add_labeled_asset_with_existing_handle_erased(
425            label.into(),
426            asset.upcast(),
427            handle.untyped(),
428        );
429    }
430
431    /// Same as [`Self::add_labeled_asset_with_new_handle`], but type-erased to allow for dynamic
432    /// types.
433    #[must_use]
434    pub fn add_labeled_asset_with_new_handle_erased<'b: 'a>(
435        &mut self,
436        label: impl Into<CowArc<'b, str>>,
437        asset: ErasedSavedAsset<'a, 'a>,
438    ) -> UntypedHandle {
439        let label = label.into();
440        let handle = UntypedHandle::Strong(
441            self.asset_server
442                .read_infos()
443                .handle_providers
444                .get(&asset.value.type_id())
445                .expect("asset type has been initialized")
446                .reserve_handle_internal(
447                    false,
448                    Some(self.asset_path.clone().with_label(label.to_string())),
449                ),
450        );
451        self.add_labeled_asset_with_existing_handle_erased(label, asset, handle.clone());
452        handle
453    }
454
455    /// Same as [`Self::add_labeled_asset_with_existing_handle`], but type-erased to allow for
456    /// dynamic types.
457    pub fn add_labeled_asset_with_existing_handle_erased<'b: 'a>(
458        &mut self,
459        label: impl Into<CowArc<'b, str>>,
460        asset: ErasedSavedAsset<'a, 'a>,
461        handle: UntypedHandle,
462    ) {
463        // TODO: Check asset and handle have the same type.
464        let labeled = LabeledSavedAsset { asset, handle };
465        match self.label_to_asset_index.entry(label.into()) {
466            Entry::Occupied(entry) => {
467                let labeled_entry = &mut self.labeled_assets[*entry.get()];
468                if labeled.handle != labeled_entry.handle {
469                    self.asset_id_to_asset_index
470                        .remove(&labeled_entry.handle.id());
471                    self.asset_id_to_asset_index
472                        .insert(labeled.handle.id(), *entry.get());
473                }
474                *labeled_entry = labeled;
475            }
476            Entry::Vacant(entry) => {
477                entry.insert(self.labeled_assets.len());
478                self.asset_id_to_asset_index
479                    .insert(labeled.handle.id(), self.labeled_assets.len());
480                self.labeled_assets.push(labeled);
481            }
482        }
483    }
484
485    /// Creates the final saved asset from this builder.
486    pub fn build<'b, A: Asset>(self, asset: &'b A) -> SavedAsset<'b, 'b, A>
487    where
488        'a: 'b,
489    {
490        SavedAsset {
491            value: asset,
492            labeled_assets: Moo::Owned(self.labeled_assets),
493            label_to_asset_index: Moo::Owned(self.label_to_asset_index),
494            asset_id_to_asset_index: Moo::Owned(self.asset_id_to_asset_index),
495        }
496    }
497}
498
499/// An alternative to [`Cow`] but simplified to just a `T` or `&T`.
500///
501/// Associated types are **always** considered "invariant" (see
502/// <https://doc.rust-lang.org/nomicon/subtyping.html>). Since [`Cow`] uses the [`ToOwned`] trait
503/// and its associated type of [`ToOwned::Owned`], this means [`Cow`] types are invariant (which
504/// TL;DR means that in some cases Rust is not allowed to shorten lifetimes, causing lifetime
505/// errors).
506///
507/// This type also allows working with any type, not just those that implement [`ToOwned`] - at the
508/// cost of losing the ability to mutate the value.
509///
510/// `Moo` stands for maybe-owned-object.
511///
512/// [`Cow`]: alloc::borrow::Cow
513/// [`ToOwned`]: alloc::borrow::ToOwned
514/// [`ToOwned::Owned`]: alloc::borrow::ToOwned::Owned
515#[derive(Clone)]
516enum Moo<'a, T> {
517    Owned(T),
518    Borrowed(&'a T),
519}
520
521impl<T> Deref for Moo<'_, T> {
522    type Target = T;
523
524    fn deref(&self) -> &Self::Target {
525        match self {
526            Self::Owned(t) => t,
527            Self::Borrowed(t) => t,
528        }
529    }
530}
531
532/// Saves `asset` to `path` using the provided `saver` and `settings`.
533pub async fn save_using_saver<S: AssetSaver>(
534    asset_server: AssetServer,
535    saver: &S,
536    path: &AssetPath<'_>,
537    asset: SavedAsset<'_, '_, S::Asset>,
538    settings: &S::Settings,
539) -> Result<(), SaveAssetError> {
540    let source = asset_server.get_source(path.source())?;
541    let writer = source.writer()?;
542
543    let mut file_writer = writer.write(path.path()).await?;
544
545    let loader_settings = saver
546        .save(&mut file_writer, asset, settings, path.clone())
547        .await
548        .map_err(|err| SaveAssetError::SaverError(Arc::new(err.into())))?;
549
550    file_writer.flush().await.map_err(AssetWriterError::Io)?;
551
552    let meta = AssetMeta::<S::OutputLoader, ()>::new(AssetAction::Load {
553        loader: S::OutputLoader::type_path().into(),
554        settings: loader_settings,
555    });
556
557    let meta = AssetMetaDyn::serialize(&meta);
558    writer.write_meta_bytes(path.path(), &meta).await?;
559
560    Ok(())
561}
562
563/// An error occurring when saving an asset.
564#[derive(Error, Debug)]
565pub enum SaveAssetError {
566    #[error(transparent)]
567    MissingSource(#[from] MissingAssetSourceError),
568    #[error(transparent)]
569    MissingWriter(#[from] MissingAssetWriterError),
570    /// Encountered an [`AssetWriterError`] while saving the asset.
571    #[error(transparent)]
572    WriterError(#[from] AssetWriterError),
573    /// Failed to save the asset due to an error from the saver.
574    #[error("Failed to save asset due to error from saver: {0}")]
575    SaverError(Arc<BevyError>),
576}
577
578#[cfg(test)]
579pub(crate) mod tests {
580    use alloc::{string::ToString, vec, vec::Vec};
581    use bevy_reflect::TypePath;
582    use bevy_tasks::block_on;
583    use futures_lite::AsyncWriteExt;
584    use ron::ser::PrettyConfig;
585
586    use crate::{
587        saver::{save_using_saver, AssetSaver, SavedAsset, SavedAssetBuilder},
588        tests::{create_app, run_app_until, CoolText, CoolTextLoader, CoolTextRon, SubText},
589        AssetApp, AssetPath, AssetServer, Assets,
590    };
591
592    fn new_subtext(text: &str) -> SubText {
593        SubText {
594            text: text.to_string(),
595        }
596    }
597
598    #[derive(TypePath)]
599    pub struct CoolTextSaver;
600
601    impl AssetSaver for CoolTextSaver {
602        type Asset = CoolText;
603        type Settings = ();
604        type OutputLoader = CoolTextLoader;
605        type Error = std::io::Error;
606
607        async fn save(
608            &self,
609            writer: &mut crate::io::Writer,
610            asset: SavedAsset<'_, '_, Self::Asset>,
611            _: &Self::Settings,
612            _: AssetPath<'_>,
613        ) -> Result<(), Self::Error> {
614            // NOTE: We can't handle embedded dependencies in any way, since we need to write to
615            // another file to do so.
616            assert!(asset.embedded.is_empty());
617            let ron = CoolTextRon {
618                text: asset.text.clone(),
619                sub_texts: asset
620                    .iter_labels()
621                    .map(|label| asset.get_labeled::<SubText>(label).unwrap().text.clone())
622                    .collect(),
623                dependencies: asset
624                    .dependencies
625                    .iter()
626                    .map(|handle| handle.path().unwrap().path())
627                    .map(|path| path.to_str().unwrap().to_string())
628                    .collect(),
629                embedded_dependencies: vec![],
630            };
631            let ron = ron::ser::to_string_pretty(&ron, PrettyConfig::new().new_line("\n")).unwrap();
632            writer.write_all(ron.as_bytes()).await?;
633            Ok(())
634        }
635    }
636
637    #[test]
638    fn builds_saved_asset_for_new_asset() {
639        let mut app = create_app().0;
640
641        app.init_asset::<CoolText>()
642            .init_asset::<SubText>()
643            .register_asset_loader(CoolTextLoader);
644
645        // Update a few times before saving to show that assets can be entirely created from
646        // scratch.
647        app.update();
648        app.update();
649        app.update();
650
651        let hiya_subasset = new_subtext("hiya");
652        let goodbye_subasset = new_subtext("goodbye");
653        let idk_subasset = new_subtext("idk");
654
655        let asset_server = app.world().resource::<AssetServer>().clone();
656        let mut saved_asset_builder =
657            SavedAssetBuilder::new(asset_server.clone(), "some/target/path.cool.ron".into());
658        let hiya_handle = saved_asset_builder
659            .add_labeled_asset_with_new_handle("hiya", SavedAsset::from_asset(&hiya_subasset));
660        let goodbye_handle = saved_asset_builder.add_labeled_asset_with_new_handle(
661            "goodbye",
662            SavedAsset::from_asset(&goodbye_subasset),
663        );
664        let idk_handle = saved_asset_builder
665            .add_labeled_asset_with_new_handle("idk", SavedAsset::from_asset(&idk_subasset));
666
667        let main_asset = CoolText {
668            text: "wassup".into(),
669            sub_texts: vec![hiya_handle, goodbye_handle, idk_handle],
670            ..Default::default()
671        };
672
673        let saved_asset = saved_asset_builder.build(&main_asset);
674        let mut asset_labels = saved_asset
675            .label_to_asset_index
676            .keys()
677            .map(|label| label.as_ref().to_string())
678            .collect::<Vec<_>>();
679        asset_labels.sort();
680        assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]);
681
682        {
683            let asset_server = asset_server.clone();
684            block_on(async move {
685                save_using_saver(
686                    asset_server,
687                    &CoolTextSaver,
688                    &"some/target/path.cool.ron".into(),
689                    saved_asset,
690                    &(),
691                )
692                .await
693            })
694            .unwrap();
695        }
696
697        let readback = asset_server.load("some/target/path.cool.ron");
698        run_app_until(&mut app, |_| {
699            asset_server.is_loaded(&readback).then_some(())
700        });
701
702        let cool_text = app
703            .world()
704            .resource::<Assets<CoolText>>()
705            .get(&readback)
706            .unwrap();
707
708        let subtexts = app.world().resource::<Assets<SubText>>();
709        let mut asset_labels = cool_text
710            .sub_texts
711            .iter()
712            .map(|handle| subtexts.get(handle).unwrap().text.clone())
713            .collect::<Vec<_>>();
714        asset_labels.sort();
715        assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]);
716    }
717
718    #[test]
719    fn builds_saved_asset_for_existing_asset() {
720        let (mut app, _) = create_app();
721
722        app.init_asset::<CoolText>()
723            .init_asset::<SubText>()
724            .register_asset_loader(CoolTextLoader);
725
726        let mut subtexts = app.world_mut().resource_mut::<Assets<SubText>>();
727        let hiya_handle = subtexts.add(new_subtext("hiya"));
728        let goodbye_handle = subtexts.add(new_subtext("goodbye"));
729        let idk_handle = subtexts.add(new_subtext("idk"));
730
731        let mut cool_texts = app.world_mut().resource_mut::<Assets<CoolText>>();
732        let cool_text_handle = cool_texts.add(CoolText {
733            text: "wassup".into(),
734            sub_texts: vec![
735                hiya_handle.clone(),
736                goodbye_handle.clone(),
737                idk_handle.clone(),
738            ],
739            ..Default::default()
740        });
741
742        let subtexts = app.world().resource::<Assets<SubText>>();
743        let cool_texts = app.world().resource::<Assets<CoolText>>();
744        let asset_server = app.world().resource::<AssetServer>().clone();
745        let mut saved_asset_builder =
746            SavedAssetBuilder::new(asset_server.clone(), "some/target/path.cool.ron".into());
747        saved_asset_builder.add_labeled_asset_with_existing_handle(
748            "hiya",
749            SavedAsset::from_asset(subtexts.get(&hiya_handle).unwrap()),
750            hiya_handle,
751        );
752        saved_asset_builder.add_labeled_asset_with_existing_handle(
753            "goodbye",
754            SavedAsset::from_asset(subtexts.get(&goodbye_handle).unwrap()),
755            goodbye_handle,
756        );
757        saved_asset_builder.add_labeled_asset_with_existing_handle(
758            "idk",
759            SavedAsset::from_asset(subtexts.get(&idk_handle).unwrap()),
760            idk_handle,
761        );
762
763        let saved_asset = saved_asset_builder.build(cool_texts.get(&cool_text_handle).unwrap());
764        let mut asset_labels = saved_asset
765            .label_to_asset_index
766            .keys()
767            .map(|label| label.as_ref().to_string())
768            .collect::<Vec<_>>();
769        asset_labels.sort();
770        assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]);
771
772        // While this example is supported, it is **not** recommended. This currently blocks the
773        // entire world from updating. A slow write could cause visible stutters. However we do this
774        // here to show it's possible to use assets directly out of the Assets resources.
775        {
776            let asset_server = asset_server.clone();
777            block_on(async move {
778                save_using_saver(
779                    asset_server,
780                    &CoolTextSaver,
781                    &"some/target/path.cool.ron".into(),
782                    saved_asset,
783                    &(),
784                )
785                .await
786            })
787            .unwrap();
788        }
789
790        let readback = asset_server.load("some/target/path.cool.ron");
791        run_app_until(&mut app, |_| {
792            asset_server.is_loaded(&readback).then_some(())
793        });
794
795        let cool_text = app
796            .world()
797            .resource::<Assets<CoolText>>()
798            .get(&readback)
799            .unwrap();
800
801        let subtexts = app.world().resource::<Assets<SubText>>();
802        let mut asset_labels = cool_text
803            .sub_texts
804            .iter()
805            .map(|handle| subtexts.get(handle).unwrap().text.clone())
806            .collect::<Vec<_>>();
807        asset_labels.sort();
808        assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]);
809    }
810}