1use 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
25pub trait AssetSaver: TypePath + Send + Sync + 'static {
36 type Asset: Asset;
38 type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
40 type OutputLoader: AssetLoader;
42 type Error: Into<BevyError>;
44
45 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
58pub trait ErasedAssetSaver: Send + Sync + 'static {
60 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 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#[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 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 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 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 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 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 #[inline]
204 pub fn get(&self) -> &'a A {
205 self.value
206 }
207
208 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 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 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 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 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 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 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 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 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#[derive(Clone)]
324struct LabeledSavedAsset<'a> {
325 asset: ErasedSavedAsset<'a, 'a>,
327 handle: UntypedHandle,
329}
330
331impl<'a> LabeledSavedAsset<'a> {
332 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
341pub struct SavedAssetBuilder<'a> {
345 labeled_assets: Vec<LabeledSavedAsset<'a>>,
347 label_to_asset_index: HashMap<CowArc<'a, str>, usize>,
349 asset_id_to_asset_index: HashMap<UntypedAssetId, usize>,
354 asset_path: AssetPath<'static>,
360 asset_server: AssetServer,
362}
363
364impl<'a> SavedAssetBuilder<'a> {
365 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 #[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 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 #[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 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 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 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#[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
532pub 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#[derive(Error, Debug)]
565pub enum SaveAssetError {
566 #[error(transparent)]
567 MissingSource(#[from] MissingAssetSourceError),
568 #[error(transparent)]
569 MissingWriter(#[from] MissingAssetWriterError),
570 #[error(transparent)]
572 WriterError(#[from] AssetWriterError),
573 #[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 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 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 {
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}