moonshine_save/
save.rs

1//! Elements related to saving world state.
2//!
3//! # Example
4//! ```
5//! use bevy::prelude::*;
6//! use moonshine_save::prelude::*;
7//!
8//! #[derive(Component, Default, Reflect)]
9//! #[reflect(Component)]
10//! struct Data(u32);
11//!
12//! let mut app = App::new();
13//! app.add_plugins((MinimalPlugins, SavePlugin))
14//!     .register_type::<Data>()
15//!     .add_systems(PreUpdate, save_default().into(static_file("example.ron")));
16//!
17//! app.world_mut().spawn((Data(12), Save));
18//! app.update();
19//!
20//! let data = std::fs::read_to_string("example.ron").unwrap();
21//! # assert!(data.contains("(12)"));
22//! # std::fs::remove_file("example.ron");
23//! ```
24
25use std::{
26    any::TypeId,
27    io::{self, Write},
28    marker::PhantomData,
29    path::PathBuf,
30};
31
32use bevy_app::{App, Plugin, PreUpdate};
33use bevy_ecs::{prelude::*, query::QueryFilter, schedule::SystemConfigs};
34use bevy_scene::{DynamicScene, DynamicSceneBuilder, SceneFilter};
35use bevy_utils::{
36    tracing::{error, info, warn},
37    HashSet,
38};
39use moonshine_util::system::*;
40
41use crate::{
42    file_from_event, file_from_resource, static_file, FileFromEvent, FileFromResource, GetFilePath,
43    GetStaticStream, GetStream, MapComponent, Pipeline, SceneMapper, StaticFile, StaticStream,
44    StreamFromEvent, StreamFromResource,
45};
46
47/// A [`Plugin`] which configures [`SaveSystem`] in [`PreUpdate`] schedule.
48pub struct SavePlugin;
49
50impl Plugin for SavePlugin {
51    fn build(&self, app: &mut App) {
52        app.configure_sets(
53            PreUpdate,
54            (
55                SaveSystem::Save,
56                SaveSystem::PostSave.run_if(has_resource::<Saved>),
57            )
58                .chain(),
59        )
60        .add_systems(
61            PreUpdate,
62            remove_resource::<Saved>.in_set(SaveSystem::PostSave),
63        );
64    }
65}
66
67/// A [`SystemSet`] for systems that process saving.
68#[derive(Clone, Debug, Hash, PartialEq, Eq, SystemSet)]
69pub enum SaveSystem {
70    /// Reserved for systems which serialize the world and process the output.
71    Save,
72    /// Runs after [`SaveSystem::Save`].
73    PostSave,
74}
75
76/// A [`Resource`] which contains the saved [`World`] data during [`SaveSystem::PostSave`].
77#[derive(Resource)]
78pub struct Saved {
79    pub scene: DynamicScene,
80    pub mapper: SceneMapper,
81}
82
83/// A [`Component`] which marks its [`Entity`] to be saved.
84#[derive(Component, Default, Clone)]
85pub struct Save;
86
87#[derive(Debug)]
88pub enum SaveError {
89    Ron(ron::Error),
90    Io(io::Error),
91}
92
93impl From<ron::Error> for SaveError {
94    fn from(e: ron::Error) -> Self {
95        Self::Ron(e)
96    }
97}
98
99impl From<io::Error> for SaveError {
100    fn from(e: io::Error) -> Self {
101        Self::Io(e)
102    }
103}
104
105#[derive(Default, Clone)]
106pub enum EntityFilter {
107    #[default]
108    Any,
109    Allow(HashSet<Entity>),
110    Block(HashSet<Entity>),
111}
112
113impl EntityFilter {
114    pub fn any() -> Self {
115        Self::Any
116    }
117
118    pub fn allow(entities: impl IntoIterator<Item = Entity>) -> Self {
119        Self::Allow(entities.into_iter().collect())
120    }
121
122    pub fn block(entities: impl IntoIterator<Item = Entity>) -> Self {
123        Self::Block(entities.into_iter().collect())
124    }
125}
126
127#[derive(Clone)]
128pub struct SaveInput {
129    pub entities: EntityFilter,
130    pub resources: SceneFilter,
131    pub components: SceneFilter,
132    pub mapper: SceneMapper,
133}
134
135impl Default for SaveInput {
136    fn default() -> Self {
137        SaveInput {
138            // By default, select all entities.
139            entities: EntityFilter::any(),
140            // By default, save all components on all saved entities.
141            components: SceneFilter::allow_all(),
142            // By default, do not save any resources. Most Bevy resources are not safely serializable.
143            resources: SceneFilter::deny_all(),
144            // By default, map nothing.
145            mapper: SceneMapper::default(),
146        }
147    }
148}
149
150pub fn filter<F: QueryFilter>(entities: Query<Entity, F>) -> SaveInput {
151    SaveInput {
152        entities: EntityFilter::allow(&entities),
153        // WARNING:
154        // Do not want to save any Bevy resources by default.
155        // They may be serializable, but not deserializable.
156        resources: SceneFilter::deny_all(),
157        ..Default::default()
158    }
159}
160
161pub fn filter_entities<F: 'static + QueryFilter>(
162    In(mut input): In<SaveInput>,
163    entities: Query<Entity, F>,
164) -> SaveInput {
165    input.entities = EntityFilter::allow(&entities);
166    input
167}
168
169pub fn map_scene(In(mut input): In<SaveInput>, world: &mut World) -> SaveInput {
170    if !input.mapper.is_empty() {
171        match &input.entities {
172            EntityFilter::Any => {
173                let entities: Vec<Entity> =
174                    world.iter_entities().map(|entity| entity.id()).collect();
175                for entity in entities {
176                    input.mapper.apply(world.entity_mut(entity));
177                }
178            }
179            EntityFilter::Allow(entities) => {
180                for entity in entities {
181                    input.mapper.apply(world.entity_mut(*entity));
182                }
183            }
184            EntityFilter::Block(blocked) => {
185                let entities: Vec<Entity> = world
186                    .iter_entities()
187                    .filter_map(|entity| (!blocked.contains(&entity.id())).then_some(entity.id()))
188                    .collect();
189                for entity in entities {
190                    input.mapper.apply(world.entity_mut(entity));
191                }
192            }
193        }
194    }
195    input
196}
197
198/// A [`System`] which creates [`Saved`] data from all entities with given `Filter`.
199///
200/// # Usage
201///
202/// All save pipelines should start with this system.
203pub fn save_scene(In(input): In<SaveInput>, world: &World) -> Saved {
204    let mut builder = DynamicSceneBuilder::from_world(world)
205        .with_component_filter(input.components)
206        .with_resource_filter(input.resources)
207        .extract_resources();
208    match input.entities {
209        EntityFilter::Any => {}
210        EntityFilter::Allow(entities) => {
211            builder = builder.extract_entities(entities.into_iter());
212        }
213        EntityFilter::Block(entities) => {
214            builder =
215                builder.extract_entities(world.iter_entities().filter_map(|entity| {
216                    (!entities.contains(&entity.id())).then_some(entity.id())
217                }));
218        }
219    }
220    let scene = builder.build();
221    Saved {
222        scene,
223        mapper: input.mapper,
224    }
225}
226
227/// A [`System`] which writes [`Saved`] data into a file at given `path`.
228pub fn write_static_file(
229    path: PathBuf,
230) -> impl Fn(In<Saved>, Res<AppTypeRegistry>) -> Result<Saved, SaveError> {
231    move |In(saved), type_registry| {
232        if let Some(parent) = path.parent() {
233            std::fs::create_dir_all(parent)?;
234        }
235        let data = saved.scene.serialize(&type_registry.read())?;
236        std::fs::write(&path, data.as_bytes())?;
237        info!("saved into file: {path:?}");
238        Ok(saved)
239    }
240}
241
242/// A [`System`] which writes [`Saved`] data into a file with its path defined at runtime.
243pub fn write_file(
244    In((path, saved)): In<(PathBuf, Saved)>,
245    type_registry: Res<AppTypeRegistry>,
246) -> Result<Saved, SaveError> {
247    if let Some(parent) = path.parent() {
248        std::fs::create_dir_all(parent)?;
249    }
250    let data = saved.scene.serialize(&type_registry.read())?;
251    std::fs::write(&path, data.as_bytes())?;
252    info!("saved into file: {path:?}");
253    Ok(saved)
254}
255
256pub fn write_stream<S: Write>(
257    In((mut stream, saved)): In<(S, Saved)>,
258    type_registry: Res<AppTypeRegistry>,
259) -> Result<Saved, SaveError> {
260    let data = saved.scene.serialize(&type_registry.read())?;
261    stream.write_all(data.as_bytes())?;
262    info!("saved into stream");
263    Ok(saved)
264}
265
266pub fn unmap_scene(
267    In(mut result): In<Result<Saved, SaveError>>,
268    world: &mut World,
269) -> Result<Saved, SaveError> {
270    if let Ok(saved) = &mut result {
271        if !saved.mapper.is_empty() {
272            for entity in saved.scene.entities.iter().map(|e| e.entity) {
273                saved.mapper.undo(world.entity_mut(entity));
274            }
275        }
276    }
277    result
278}
279
280/// A [`System`] which finishes the save process.
281///
282/// # Usage
283/// All save pipelines should end with this system.
284pub fn insert_saved(In(result): In<Result<Saved, SaveError>>, world: &mut World) {
285    match result {
286        Ok(saved) => world.insert_resource(saved),
287        Err(why) => error!("save failed: {why:?}"),
288    }
289}
290
291/// A [`System`] which extracts the path from a [`SaveIntoFileRequest`] [`Resource`].
292pub fn get_file_from_resource<R>(In(saved): In<Saved>, request: Res<R>) -> (PathBuf, Saved)
293where
294    R: GetFilePath + Resource,
295{
296    let path = request.path().to_owned();
297    (path, saved)
298}
299
300/// A [`System`] which extracts the path from a [`SaveIntoFileRequest`] [`Event`].
301///
302/// # Warning
303///
304/// If multiple events are sent in a single update cycle, only the first one is processed.
305///
306/// This system assumes that at least one event has been sent. It must be used in conjunction with [`has_event`].
307pub fn get_file_from_event<E>(In(saved): In<Saved>, mut events: EventReader<E>) -> (PathBuf, Saved)
308where
309    E: GetFilePath + Event,
310{
311    let mut iter = events.read();
312    let event = iter.next().unwrap();
313    if iter.next().is_some() {
314        warn!("multiple save request events received; only the first one is processed.");
315    }
316    let path = event.path().to_owned();
317    (path, saved)
318}
319
320pub fn get_stream_from_event<E>(
321    In(saved): In<Saved>,
322    mut events: EventReader<E>,
323) -> (<E as GetStream>::Stream, Saved)
324where
325    E: GetStream + Event,
326{
327    let mut iter = events.read();
328    let event = iter.next().unwrap();
329    if iter.next().is_some() {
330        warn!("multiple save request events received; only the first one is processed.");
331    }
332    (event.stream(), saved)
333}
334
335/// A convenient builder for defining a [`SavePipeline`].
336///
337/// See [`save`], [`save_default`], [`save_all`] on how to create an instance of this type.
338pub struct SavePipelineBuilder<F: QueryFilter> {
339    query: PhantomData<F>,
340    input: SaveInput,
341}
342
343/// Creates a [`SavePipelineBuilder`] which saves all entities with given [`QueryFilter`] `F`.
344///
345/// During the save process, all entities that match the given query will be selected for saving.
346///
347/// # Example
348/// ```
349/// use bevy::prelude::*;
350/// use moonshine_save::prelude::*;
351///
352/// let mut app = App::new();
353/// app.add_plugins((MinimalPlugins, SavePlugin))
354///     .add_systems(PreUpdate, save::<With<Save>>().into_file("example.ron"));
355/// ```
356pub fn save<F: QueryFilter>() -> SavePipelineBuilder<F> {
357    SavePipelineBuilder {
358        query: PhantomData,
359        input: Default::default(),
360    }
361}
362
363/// Creates a [`SavePipelineBuilder`] which saves all entities with a [`Save`] component.
364///
365/// # Example
366/// ```
367/// use bevy::prelude::*;
368/// use moonshine_save::prelude::*;
369///
370/// let mut app = App::new();
371/// app.add_plugins((MinimalPlugins, SavePlugin))
372///     .add_systems(PreUpdate, save_default().into_file("example.ron"));
373/// ```
374pub fn save_default() -> SavePipelineBuilder<With<Save>> {
375    save()
376}
377
378/// Creates a [`SavePipelineBuilder`] which saves all entities unconditionally.
379///
380/// # Warning
381/// Be careful about using this builder as some entities and/or components may not be safely serializable.
382///
383/// # Example
384/// ```
385/// use bevy::prelude::*;
386/// use moonshine_save::prelude::*;
387///
388/// let mut app = App::new();
389/// app.add_plugins((MinimalPlugins, SavePlugin))
390///     .add_systems(PreUpdate, save_all().into_file("example.ron"));
391/// ```
392pub fn save_all() -> SavePipelineBuilder<()> {
393    save()
394}
395
396impl<F: QueryFilter> SavePipelineBuilder<F>
397where
398    F: 'static,
399{
400    /// Includes a given [`Resource`] type into the save pipeline.
401    ///
402    /// By default, all resources are *excluded* from the save pipeline.
403    ///
404    /// # Example
405    /// ```
406    /// use bevy::prelude::*;
407    /// use moonshine_save::prelude::*;
408    ///
409    /// #[derive(Resource, Default, Reflect)]
410    /// #[reflect(Resource)]
411    /// struct R;
412    ///
413    /// let mut app = App::new();
414    /// app.register_type::<R>()
415    ///     .insert_resource(R)
416    ///     .add_plugins((MinimalPlugins, SavePlugin))
417    ///     .add_systems(
418    ///         PreUpdate,
419    ///         save_default()
420    ///             .include_resource::<R>()
421    ///             .into_file("example.ron"));
422    /// ```
423    pub fn include_resource<R: Resource>(mut self) -> Self {
424        self.input.resources = self.input.resources.allow::<R>();
425        self
426    }
427
428    /// Includes a given [`Resource`] type into the save pipeline by its [`TypeId`].
429    pub fn include_resource_by_id(mut self, type_id: TypeId) -> Self {
430        self.input.resources = self.input.resources.allow_by_id(type_id);
431        self
432    }
433
434    /// Excludes a given [`Component`] type from the save pipeline.
435    ///
436    /// By default, all components which derive `Reflect` are *included* in the save pipeline.
437    ///
438    /// # Example
439    /// ```
440    /// use bevy::prelude::*;
441    /// use moonshine_save::prelude::*;
442    ///
443    /// #[derive(Resource, Default, Reflect)]
444    /// #[reflect(Resource)]
445    /// struct R;
446    ///
447    /// let mut app = App::new();
448    /// app.register_type::<R>()
449    ///     .insert_resource(R)
450    ///     .add_plugins((MinimalPlugins, SavePlugin))
451    ///     .add_systems(
452    ///         PreUpdate,
453    ///         save_default()
454    ///             .exclude_component::<Transform>()
455    ///             .into_file("example.ron"));
456    /// ```
457    pub fn exclude_component<T: Component>(mut self) -> Self {
458        self.input.components = self.input.components.deny::<T>();
459        self
460    }
461
462    /// Excludes a given [`Component`] type from the save pipeline by its [`TypeId`].
463    pub fn exclude_component_by_id(mut self, type_id: TypeId) -> Self {
464        self.input.components = self.input.components.deny_by_id(type_id);
465        self
466    }
467
468    pub fn map_component<T: Component>(mut self, m: impl MapComponent<T>) -> Self {
469        self.input.mapper = self.input.mapper.map(m);
470        self
471    }
472
473    pub fn into(self, p: impl SavePipeline) -> SystemConfigs {
474        let Self { input, .. } = self;
475        let system = (move || input.clone())
476            .pipe(filter_entities::<F>)
477            .pipe(map_scene)
478            .pipe(save_scene);
479        let system = p
480            .save(IntoSystem::into_system(system))
481            .pipe(unmap_scene)
482            .pipe(insert_saved);
483        p.finish(IntoSystem::into_system(system))
484            .in_set(SaveSystem::Save)
485    }
486
487    #[deprecated(note = "use `into` instead")]
488    pub fn into_file(self, path: impl Into<PathBuf>) -> SystemConfigs {
489        self.into(static_file(path))
490    }
491
492    /// Finishes the save pipeline by writing the saved data into a file with its path derived from a resource of type `R`.
493    ///
494    /// The save pipeline will only be triggered if a resource of type `R` is present.
495    #[deprecated(note = "use `into` instead")]
496    pub fn into_file_on_request<R: GetFilePath + Resource>(self) -> SystemConfigs {
497        self.into(file_from_resource::<R>())
498    }
499
500    /// Finishes the save pipeline by writing the saved data into a file with its path derived from an event of type `R`.
501    ///
502    /// The save pipeline will only be triggered if an event of type `R` is sent.
503    ///
504    /// # Warning
505    /// If multiple events are sent in a single update cycle, only the first one is processed.
506    #[deprecated(note = "use `into` instead")]
507    pub fn into_file_on_event<R: GetFilePath + Event>(self) -> SystemConfigs {
508        self.into(file_from_event::<R>())
509    }
510}
511
512/// A convenient builder for defining a [`SavePipeline`] with a dynamic [`SaveInput`] which can be provided from any [`System`].
513///
514/// See [`save_with`], [`save_default_with`], and [`save_all_with`] on how to create an instance of this type.
515pub struct DynamicSavePipelineBuilder<F: QueryFilter, S: System<In = (), Out = SaveInput>> {
516    query: PhantomData<F>,
517    input_source: S,
518}
519
520impl<F: QueryFilter, S: System<In = (), Out = SaveInput>> DynamicSavePipelineBuilder<F, S>
521where
522    F: 'static,
523{
524    pub fn into(self, p: impl SavePipeline) -> SystemConfigs {
525        let Self { input_source, .. } = self;
526        let system = input_source
527            .pipe(filter_entities::<F>)
528            .pipe(map_scene)
529            .pipe(save_scene);
530        let system = p
531            .save(IntoSystem::into_system(system))
532            .pipe(unmap_scene)
533            .pipe(insert_saved);
534        p.finish(IntoSystem::into_system(system))
535            .in_set(SaveSystem::Save)
536    }
537
538    /// Finishes the save pipeline by writing the saved data into a file at given `path`.
539    #[deprecated(note = "use `into` instead")]
540    pub fn into_file(self, path: impl Into<PathBuf>) -> SystemConfigs {
541        self.into(static_file(path))
542    }
543
544    /// Finishes the save pipeline by writing the saved data into a file with its path derived from a resource of type `R`.
545    ///
546    /// The save pipeline will only be triggered if a resource of type `R` is present.
547    #[deprecated(note = "use `into` instead")]
548    pub fn into_file_on_request<R: GetFilePath + Resource>(self) -> SystemConfigs {
549        self.into(file_from_resource::<R>())
550    }
551
552    /// Finishes the save pipeline by writing the saved data into a file with its path derived from an event of type `R`.
553    ///
554    /// The save pipeline will only be triggered if an event of type `R` is sent.
555    ///
556    /// # Warning
557    /// If multiple events are sent in a single update cycle, only the first one is processed.
558    #[deprecated(note = "use `into` instead")]
559    pub fn into_file_on_event<R: GetFilePath + Event>(self) -> SystemConfigs {
560        self.into(file_from_event::<R>())
561    }
562}
563
564/// Creates a [`DynamicSavePipelineBuilder`] which saves all entities with given [`QueryFilter`] `F` and an input source `S`.
565///
566/// During the save process, all entities that match the given query will be selected for saving.
567/// Additionally, any valid system which returns a [`SaveInput`] may be used to provide the initial save input dynamically.
568///
569/// # Example
570/// ```
571/// use bevy::prelude::*;
572/// use moonshine_save::prelude::*;
573///
574/// fn save_input(/* ... */) -> SaveInput {
575///     todo!()
576/// }
577///
578/// let mut app = App::new();
579/// app.add_plugins((MinimalPlugins, SavePlugin))
580///     .add_systems(PreUpdate, save_with::<With<Save>, _, _>(save_input).into(static_file("example.ron")));
581/// ```
582pub fn save_with<F: QueryFilter, S: IntoSystem<(), SaveInput, M>, M>(
583    input_source: S,
584) -> DynamicSavePipelineBuilder<F, S::System> {
585    DynamicSavePipelineBuilder {
586        query: PhantomData,
587        input_source: IntoSystem::into_system(input_source),
588    }
589}
590
591/// Creates a [`DynamicSavePipelineBuilder`] which saves all entities with a [`Save`] component and a filter source `S`.
592///
593/// Additionally, any valid system which returns a [`SaveInput`] may be used to provide the initial save input dynamically.
594///
595/// # Example
596/// ```
597/// use bevy::prelude::*;
598/// use moonshine_save::prelude::*;
599///
600/// fn save_input(/* ... */) -> SaveInput {
601///     todo!()
602/// }
603///
604/// let mut app = App::new();
605/// app.add_plugins((MinimalPlugins, SavePlugin))
606///     .add_systems(PreUpdate, save_default_with(save_input).into(static_file("example.ron")));
607/// ```
608pub fn save_default_with<S: IntoSystem<(), SaveInput, M>, M>(
609    s: S,
610) -> DynamicSavePipelineBuilder<With<Save>, S::System> {
611    DynamicSavePipelineBuilder {
612        query: PhantomData,
613        input_source: IntoSystem::into_system(s),
614    }
615}
616
617/// Creates a [`DynamicSavePipelineBuilder`] which saves all entities unconditionally and a filter source `S`.
618///
619/// Additionally, any valid system which returns a [`SaveInput`] may be used to provide the initial save input dynamically.
620///
621/// # Warning
622/// Be careful about using this builder as some entities and/or components may not be safely serializable.
623///
624/// # Example
625/// ```
626/// use bevy::prelude::*;
627/// use moonshine_save::prelude::*;
628///
629/// fn save_input(/* ... */) -> SaveInput {
630///     todo!()
631/// }
632///
633/// let mut app = App::new();
634/// app.add_plugins((MinimalPlugins, SavePlugin))
635///     .add_systems(PreUpdate, save_all_with(save_input).into(static_file("example.ron")));
636/// ```
637pub fn save_all_with<S: IntoSystem<(), SaveInput, M>, M>(
638    input_source: S,
639) -> DynamicSavePipelineBuilder<(), S::System> {
640    DynamicSavePipelineBuilder {
641        query: PhantomData,
642        input_source: IntoSystem::into_system(input_source),
643    }
644}
645
646pub trait SavePipeline: Pipeline {
647    fn save(
648        &self,
649        system: impl System<In = (), Out = Saved>,
650    ) -> impl System<In = (), Out = Result<Saved, SaveError>>;
651}
652
653impl SavePipeline for StaticFile {
654    fn save(
655        &self,
656        system: impl System<In = (), Out = Saved>,
657    ) -> impl System<In = (), Out = Result<Saved, SaveError>> {
658        IntoSystem::into_system(system.pipe(write_static_file(self.0.clone())))
659    }
660}
661
662impl<S: GetStaticStream> SavePipeline for StaticStream<S>
663where
664    S::Stream: Write,
665{
666    fn save(
667        &self,
668        system: impl System<In = (), Out = Saved>,
669    ) -> impl System<In = (), Out = Result<Saved, SaveError>> {
670        IntoSystem::into_system(
671            system
672                .pipe(move |In(saved): In<Saved>| (S::stream(), saved))
673                .pipe(write_stream),
674        )
675    }
676}
677
678impl<R: GetFilePath + Resource> SavePipeline for FileFromResource<R> {
679    fn save(
680        &self,
681        system: impl System<In = (), Out = Saved>,
682    ) -> impl System<In = (), Out = Result<Saved, SaveError>> {
683        IntoSystem::into_system(system.pipe(get_file_from_resource::<R>).pipe(write_file))
684    }
685}
686
687impl<R: GetStream + Resource> SavePipeline for StreamFromResource<R>
688where
689    R::Stream: Write,
690{
691    fn save(
692        &self,
693        system: impl System<In = (), Out = Saved>,
694    ) -> impl System<In = (), Out = Result<Saved, SaveError>> {
695        IntoSystem::into_system(
696            system
697                .pipe(move |In(saved): In<Saved>, resource: Res<R>| (resource.stream(), saved))
698                .pipe(write_stream),
699        )
700    }
701}
702
703impl<E: GetFilePath + Event> SavePipeline for FileFromEvent<E> {
704    fn save(
705        &self,
706        system: impl System<In = (), Out = Saved>,
707    ) -> impl System<In = (), Out = Result<Saved, SaveError>> {
708        IntoSystem::into_system(system.pipe(get_file_from_event::<E>).pipe(write_file))
709    }
710}
711
712impl<E: GetStream + Event> SavePipeline for StreamFromEvent<E>
713where
714    E::Stream: Write,
715{
716    fn save(
717        &self,
718        system: impl System<In = (), Out = Saved>,
719    ) -> impl System<In = (), Out = Result<Saved, SaveError>> {
720        IntoSystem::into_system(system.pipe(get_stream_from_event::<E>).pipe(write_stream))
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use std::{fs::*, path::Path};
727
728    use bevy::prelude::*;
729
730    use super::*;
731    use crate::*;
732
733    #[derive(Component, Default, Reflect)]
734    #[reflect(Component)]
735    struct Dummy;
736
737    fn app() -> App {
738        let mut app = App::new();
739        app.add_plugins((MinimalPlugins, SavePlugin))
740            .register_type::<Dummy>();
741        app
742    }
743
744    #[test]
745    fn test_save_into_file() {
746        pub const PATH: &str = "test_save_into_file.ron";
747        let mut app = app();
748        app.add_systems(PreUpdate, save_default().into(static_file(PATH)));
749
750        app.world_mut().spawn((Dummy, Save));
751        app.update();
752
753        let data = read_to_string(PATH).unwrap();
754        assert!(data.contains("Dummy"));
755        assert!(!app.world().contains_resource::<Saved>());
756
757        remove_file(PATH).unwrap();
758    }
759
760    #[test]
761    fn test_save_into_stream() {
762        pub const PATH: &str = "test_save_to_stream.ron";
763
764        struct SaveStream;
765
766        impl GetStaticStream for SaveStream {
767            type Stream = File;
768
769            fn stream() -> Self::Stream {
770                File::create(PATH).unwrap()
771            }
772        }
773
774        let mut app = app();
775        app.add_systems(PreUpdate, save_default().into(static_stream(SaveStream)));
776
777        app.world_mut().spawn((Dummy, Save));
778        app.update();
779
780        let data = read_to_string(PATH).unwrap();
781        assert!(data.contains("Dummy"));
782        assert!(!app.world().contains_resource::<Saved>());
783
784        remove_file(PATH).unwrap();
785    }
786
787    #[test]
788    fn test_save_into_file_from_resource() {
789        pub const PATH: &str = "test_save_into_file_from_resource.ron";
790
791        #[derive(Resource)]
792        struct SaveRequest;
793
794        impl GetFilePath for SaveRequest {
795            fn path(&self) -> &Path {
796                PATH.as_ref()
797            }
798        }
799
800        let mut app = app();
801        app.add_systems(
802            PreUpdate,
803            save_default().into(file_from_resource::<SaveRequest>()),
804        );
805
806        app.world_mut().insert_resource(SaveRequest);
807        app.world_mut().spawn((Dummy, Save));
808        app.update();
809
810        let data = read_to_string(PATH).unwrap();
811        assert!(data.contains("Dummy"));
812        assert!(!app.world().contains_resource::<SaveRequest>());
813
814        remove_file(PATH).unwrap();
815    }
816
817    #[test]
818    fn test_save_into_stream_from_resource() {
819        pub const PATH: &str = "test_save_into_stream_from_resource.ron";
820
821        #[derive(Resource)]
822        struct SaveRequest(&'static str);
823
824        impl GetStream for SaveRequest {
825            type Stream = File;
826
827            fn stream(&self) -> Self::Stream {
828                File::create(self.0).unwrap()
829            }
830        }
831
832        let mut app = app();
833        app.add_systems(
834            PreUpdate,
835            save_default().into(stream_from_resource::<SaveRequest>()),
836        );
837
838        app.world_mut().insert_resource(SaveRequest(PATH));
839        app.world_mut().spawn((Dummy, Save));
840        app.update();
841
842        let data = read_to_string(PATH).unwrap();
843        assert!(data.contains("Dummy"));
844        assert!(!app.world().contains_resource::<Saved>());
845        assert!(!app.world().contains_resource::<SaveRequest>());
846
847        remove_file(PATH).unwrap();
848    }
849
850    #[test]
851    fn test_save_into_file_from_event() {
852        pub const PATH: &str = "test_save_into_file_from_event.ron";
853
854        #[derive(Event)]
855        struct SaveRequest;
856
857        impl GetFilePath for SaveRequest {
858            fn path(&self) -> &Path {
859                PATH.as_ref()
860            }
861        }
862
863        let mut app = app();
864        app.add_event::<SaveRequest>().add_systems(
865            PreUpdate,
866            save_default().into(file_from_event::<SaveRequest>()),
867        );
868
869        app.world_mut().send_event(SaveRequest);
870        app.world_mut().spawn((Dummy, Save));
871        app.update();
872
873        let data = read_to_string(PATH).unwrap();
874        assert!(data.contains("Dummy"));
875
876        remove_file(PATH).unwrap();
877    }
878
879    #[test]
880    fn test_save_into_stream_from_event() {
881        pub const PATH: &str = "test_save_into_stream_from_event.ron";
882
883        #[derive(Event)]
884        struct SaveRequest(&'static str);
885
886        impl GetStream for SaveRequest {
887            type Stream = File;
888
889            fn stream(&self) -> Self::Stream {
890                File::create(self.0).unwrap()
891            }
892        }
893
894        let mut app = app();
895        app.add_event::<SaveRequest>().add_systems(
896            PreUpdate,
897            save_default().into(stream_from_event::<SaveRequest>()),
898        );
899
900        app.world_mut().send_event(SaveRequest(PATH));
901        app.world_mut().spawn((Dummy, Save));
902        app.update();
903
904        let data = read_to_string(PATH).unwrap();
905        assert!(data.contains("Dummy"));
906
907        remove_file(PATH).unwrap();
908    }
909
910    #[test]
911    fn test_save_resource() {
912        pub const PATH: &str = "test_save_resource.ron";
913
914        #[derive(Resource, Default, Reflect)]
915        #[reflect(Resource)]
916        struct Dummy;
917
918        let mut app = app();
919        app.register_type::<Dummy>()
920            .insert_resource(Dummy)
921            .add_systems(
922                Update,
923                save_default()
924                    .include_resource::<Dummy>()
925                    .into(static_file(PATH)),
926            );
927
928        app.update();
929
930        let data = read_to_string(PATH).unwrap();
931        assert!(data.contains("Dummy"));
932
933        remove_file(PATH).unwrap();
934    }
935
936    #[test]
937    fn test_save_without_component() {
938        pub const PATH: &str = "test_save_without_component.ron";
939
940        #[derive(Component, Default, Reflect)]
941        #[reflect(Component)]
942        struct Foo;
943
944        let mut app = app();
945        app.add_systems(
946            PreUpdate,
947            save_default()
948                .exclude_component::<Foo>()
949                .into(static_file(PATH)),
950        );
951
952        app.world_mut().spawn((Dummy, Foo, Save));
953        app.update();
954
955        let data = read_to_string(PATH).unwrap();
956        assert!(data.contains("Dummy"));
957        assert!(!data.contains("Foo"));
958
959        remove_file(PATH).unwrap();
960    }
961
962    #[test]
963    fn test_save_without_component_dynamic() {
964        pub const PATH: &str = "test_save_without_component_dynamic.ron";
965
966        #[derive(Component, Default, Reflect)]
967        #[reflect(Component)]
968        struct Foo;
969
970        fn deny_foo() -> SaveInput {
971            SaveInput {
972                components: SceneFilter::default().deny::<Foo>(),
973                ..Default::default()
974            }
975        }
976
977        let mut app = app();
978        app.add_systems(
979            PreUpdate,
980            save_default_with(deny_foo).into(static_file(PATH)),
981        );
982
983        app.world_mut().spawn((Dummy, Foo, Save));
984        app.update();
985
986        let data = read_to_string(PATH).unwrap();
987        assert!(data.contains("Dummy"));
988        assert!(!data.contains("Foo"));
989
990        remove_file(PATH).unwrap();
991    }
992
993    #[test]
994    fn test_save_map_component() {
995        pub const PATH: &str = "test_save_map_component.ron";
996
997        #[derive(Component, Default)]
998        struct Foo(#[allow(dead_code)] u32); // Not serializable
999
1000        #[derive(Component, Default, Reflect)]
1001        #[reflect(Component)]
1002        struct Bar(u32); // Serializable
1003
1004        let mut app = app();
1005        app.register_type::<Bar>().add_systems(
1006            PreUpdate,
1007            save_default()
1008                .map_component::<Foo>(|Foo(i): &Foo| Bar(*i))
1009                .into(static_file(PATH)),
1010        );
1011
1012        let entity = app.world_mut().spawn((Foo(12), Save)).id();
1013        app.update();
1014
1015        let data = read_to_string(PATH).unwrap();
1016        assert!(data.contains("Bar"));
1017        assert!(data.contains("(12)"));
1018        assert!(!data.contains("Foo"));
1019        assert!(app.world().entity(entity).contains::<Foo>());
1020        assert!(!app.world().entity(entity).contains::<Bar>());
1021
1022        remove_file(PATH).unwrap();
1023    }
1024}