1use 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
47pub 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#[derive(Clone, Debug, Hash, PartialEq, Eq, SystemSet)]
69pub enum SaveSystem {
70 Save,
72 PostSave,
74}
75
76#[derive(Resource)]
78pub struct Saved {
79 pub scene: DynamicScene,
80 pub mapper: SceneMapper,
81}
82
83#[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 entities: EntityFilter::any(),
140 components: SceneFilter::allow_all(),
142 resources: SceneFilter::deny_all(),
144 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 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
198pub 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
227pub 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
242pub 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
280pub 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
291pub 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
300pub 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
335pub struct SavePipelineBuilder<F: QueryFilter> {
339 query: PhantomData<F>,
340 input: SaveInput,
341}
342
343pub fn save<F: QueryFilter>() -> SavePipelineBuilder<F> {
357 SavePipelineBuilder {
358 query: PhantomData,
359 input: Default::default(),
360 }
361}
362
363pub fn save_default() -> SavePipelineBuilder<With<Save>> {
375 save()
376}
377
378pub fn save_all() -> SavePipelineBuilder<()> {
393 save()
394}
395
396impl<F: QueryFilter> SavePipelineBuilder<F>
397where
398 F: 'static,
399{
400 pub fn include_resource<R: Resource>(mut self) -> Self {
424 self.input.resources = self.input.resources.allow::<R>();
425 self
426 }
427
428 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 pub fn exclude_component<T: Component>(mut self) -> Self {
458 self.input.components = self.input.components.deny::<T>();
459 self
460 }
461
462 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 #[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 #[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
512pub 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 #[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 #[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 #[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
564pub 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
591pub 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
617pub 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); #[derive(Component, Default, Reflect)]
1001 #[reflect(Component)]
1002 struct Bar(u32); 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}