Skip to main content

bevy_asset/
saver.rs

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