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