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
19pub trait AssetSaver: TypePath + Send + Sync + 'static {
30 type Asset: Asset;
32 type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
34 type OutputLoader: AssetLoader;
36 type Error: Into<BevyError>;
38
39 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
52pub trait ErasedAssetSaver: Send + Sync + 'static {
54 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 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#[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 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 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 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 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 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 #[inline]
198 pub fn get(&self) -> &'a A {
199 self.value
200 }
201
202 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 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 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 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 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 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 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 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 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#[derive(Clone)]
318struct LabeledSavedAsset<'a> {
319 asset: ErasedSavedAsset<'a, 'a>,
321 handle: UntypedHandle,
323}
324
325impl<'a> LabeledSavedAsset<'a> {
326 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
335pub struct SavedAssetBuilder<'a> {
339 labeled_assets: Vec<LabeledSavedAsset<'a>>,
341 label_to_asset_index: HashMap<CowArc<'a, str>, usize>,
343 asset_id_to_asset_index: HashMap<UntypedAssetId, usize>,
348 asset_path: AssetPath<'static>,
354 asset_server: AssetServer,
356}
357
358impl<'a> SavedAssetBuilder<'a> {
359 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 #[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 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 #[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 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 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 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#[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
528pub 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#[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 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 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 {
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}