1use core::any::TypeId;
19use core::time::Duration;
20use serde::ser::SerializeMap;
21use std::collections::HashMap;
22
23use bevy_app::{App, Plugin, PostUpdate};
24use bevy_ecs::{
25 change_detection::Tick,
26 reflect::{AppTypeRegistry, ReflectComponent, ReflectResource},
27 resource::Resource,
28 system::{Command, Commands, Res, ResMut},
29 world::World,
30};
31pub use bevy_ecs_macros::SettingsGroup;
32use bevy_log::warn;
33use bevy_reflect::{
34 enums::DynamicEnum,
35 prelude::ReflectDefault,
36 serde::{ReflectSerializerProcessor, TypedReflectDeserializer, TypedReflectSerializer},
37 CreateTypeData, FromReflect, PartialReflect, Reflect, ReflectMut, TypeInfo, TypePath,
38 TypeRegistration, TypeRegistry,
39};
40
41#[cfg(not(target_arch = "wasm32"))]
42mod store_fs;
43
44#[cfg(target_arch = "wasm32")]
45mod store_wasm;
46
47use bevy_time::{Real, Time, Timer, TimerMode};
48use serde::de::DeserializeSeed;
49#[cfg(not(target_arch = "wasm32"))]
50use store_fs::SettingsStore;
51
52#[cfg(target_arch = "wasm32")]
53use store_wasm::SettingsStore;
54
55pub struct SettingsPlugin {
132 pub app_name: String,
134}
135
136impl SettingsPlugin {
137 pub fn new(app_name: &str) -> Self {
139 Self {
140 app_name: app_name.to_string(),
141 }
142 }
143}
144
145impl Plugin for SettingsPlugin {
146 fn build(&self, app: &mut App) {
147 let app_name = self.app_name.clone();
148 let world = app.world();
149 let last_save = world.read_change_tick();
150
151 let Some(app_types) = world.get_resource::<AppTypeRegistry>() else {
153 return;
154 };
155 let app_types = app_types.clone();
156 let types = app_types.read();
157
158 let world = app.world_mut();
159 let file_index = build_settings_registry(&app_name, &types, last_save);
160
161 for (filename, manifest) in file_index.files.iter() {
164 load_settings_file(world, &app_name, filename, manifest, &types);
165 }
166
167 drop(types);
170 world.insert_resource::<SettingsFileRegistry>(file_index);
171
172 app.add_systems(PostUpdate, handle_delayed_save);
173 }
174}
175
176pub trait SettingsGroup: Resource + Reflect + Default {
196 fn settings_group_name() -> &'static str;
198
199 fn settings_key_name() -> Option<&'static str>;
204
205 fn settings_source() -> Option<&'static str>;
208}
209
210#[derive(Clone)]
212pub struct ReflectSettingsGroup {
213 settings_group_name: &'static str,
215 settings_key_name: Option<&'static str>,
217 settings_source: Option<&'static str>,
219}
220
221impl ReflectSettingsGroup {
222 pub fn settings_group_name(&self) -> &'static str {
224 self.settings_group_name
225 }
226
227 pub fn settings_key_name(&self) -> Option<&'static str> {
229 self.settings_key_name
230 }
231
232 pub fn settings_source(&self) -> Option<&'static str> {
234 self.settings_source
235 }
236}
237
238impl<T: SettingsGroup + FromReflect + TypePath> CreateTypeData<T> for ReflectSettingsGroup {
239 fn create_type_data(_input: ()) -> Self {
240 ReflectSettingsGroup {
241 settings_group_name: T::settings_group_name(),
242 settings_key_name: T::settings_key_name(),
243 settings_source: T::settings_source(),
244 }
245 }
246
247 fn insert_dependencies(type_registration: &mut TypeRegistration) {
248 type_registration.register_type_data::<ReflectResource, T>();
249 }
250}
251
252#[derive(Default)]
255struct SettingsFileManifest {
256 last_save: Tick,
257 resource_types: Vec<TypeId>,
258}
259
260#[derive(Resource)]
264struct SettingsFileRegistry {
265 app_name: String,
267
268 files: HashMap<&'static str, SettingsFileManifest>,
270
271 save_timer: Timer,
273}
274
275#[derive(Default, PartialEq)]
278pub enum SaveSettingsSync {
279 #[default]
281 IfChanged,
282 Always,
284}
285
286impl Command for SaveSettingsSync {
287 type Out = ();
288
289 fn apply(self, world: &mut World) {
290 save_settings(world, false, self == SaveSettingsSync::Always);
291 }
292}
293
294#[derive(Default, PartialEq)]
296pub enum SaveSettings {
297 #[default]
299 IfChanged,
300 Always,
302}
303
304impl Command for SaveSettings {
305 type Out = ();
306
307 fn apply(self, world: &mut World) {
308 save_settings(world, true, self == SaveSettings::Always);
309 }
310}
311
312pub struct SaveSettingsDeferred(pub Duration);
317
318impl Default for SaveSettingsDeferred {
319 fn default() -> Self {
320 Self(Duration::from_secs(1))
321 }
322}
323
324impl Command for SaveSettingsDeferred {
325 type Out = ();
326
327 fn apply(self, world: &mut World) {
328 let Some(mut registry) = world.get_resource_mut::<SettingsFileRegistry>() else {
329 return;
330 };
331
332 registry.save_timer.set_duration(self.0);
333 registry.save_timer.reset();
334 registry.save_timer.unpause();
335 }
336}
337
338fn save_settings(world: &mut World, use_async: bool, force: bool) {
339 let this_run = world.change_tick();
340 let Some(registry) = world.get_resource::<SettingsFileRegistry>() else {
341 warn!("Settings registry not found - did you forget to install the SettingsPlugin?");
342 return;
343 };
344 let Some(app_types) = world.get_resource::<AppTypeRegistry>() else {
345 return;
346 };
347 let app_types = app_types.clone();
348 let types = app_types.read();
349
350 for (filename, manifest) in registry.files.iter() {
351 if force || has_settings_changed(world, manifest) {
352 let table = resources_to_toml(world, &types, manifest);
353 let store = SettingsStore::new(®istry.app_name);
354 if use_async {
355 store.save_async(filename, table);
356 } else {
357 store.save(filename, table);
358 }
359 }
360 }
361
362 let mut registry = world.get_resource_mut::<SettingsFileRegistry>().unwrap();
364 for manifest in registry.files.values_mut() {
365 manifest.last_save = this_run;
366 }
367}
368
369fn has_settings_changed(world: &World, manifest: &SettingsFileManifest) -> bool {
370 let this_run = world.read_change_tick();
371 manifest.resource_types.iter().any(|r| {
372 let Some(component_id) = world.components().get_id(*r) else {
373 return false;
374 };
375 if let Some(resource_change) = world.get_resource_change_ticks_by_id(component_id) {
376 return resource_change.is_changed(manifest.last_save, this_run);
377 }
378 false
379 })
380}
381
382struct SettingsSerializerProcessor;
383
384impl ReflectSerializerProcessor for SettingsSerializerProcessor {
385 fn try_serialize<S>(
386 &self,
387 value: &dyn PartialReflect,
388 _registry: &TypeRegistry,
389 serializer: S,
390 ) -> Result<Result<S::Ok, S>, S::Error>
391 where
392 S: serde::Serializer,
393 {
394 let Some(type_info) = value.get_represented_type_info() else {
395 return Ok(Err(serializer));
396 };
397
398 let is_option = type_info.type_path_table().module_path() == Some("core::option")
399 && type_info.type_path_table().ident() == Some("Option");
400
401 let is_none = is_option
402 && value
403 .reflect_ref()
404 .as_enum()
405 .is_ok_and(|enum_value| enum_value.variant_name() == "None");
406
407 if is_none {
409 let map = serializer.serialize_map(Some(0))?;
410 return Ok(Ok(SerializeMap::end(map)?));
411 }
412
413 Ok(Err(serializer))
414 }
415}
416
417fn resources_to_toml(
418 world: &World,
419 types: &TypeRegistry,
420 manifest: &SettingsFileManifest,
421) -> toml::map::Map<String, toml::Value> {
422 let mut table = toml::Table::new();
423
424 for tid in manifest.resource_types.iter() {
425 let ty = types.get(*tid).unwrap();
426
427 let Some(cmp) = ty.data::<ReflectComponent>() else {
428 continue;
429 };
430
431 let Some(reflect_settings_group) = ty.data::<ReflectSettingsGroup>() else {
432 continue;
433 };
434
435 let settings_group = reflect_settings_group.settings_group_name;
436 let settings_key = reflect_settings_group.settings_key_name;
437
438 let Some(component_id) = world.components().get_id(*tid) else {
439 continue;
440 };
441
442 let Some(res_entity) = world.resource_entities().get(component_id) else {
443 continue;
444 };
445 let res_entity_ref = world.entity(res_entity);
446 let Some(reflect) = cmp.reflect(res_entity_ref) else {
447 continue;
448 };
449
450 let serializer = TypedReflectSerializer::with_processor(
451 reflect.as_partial_reflect(),
452 types,
453 &SettingsSerializerProcessor,
454 );
455
456 let toml_value = if let Some(settings_key) = settings_key {
457 toml::Value::Table(toml::Table::from_iter([(
459 settings_key.to_string(),
460 toml::Value::try_from(serializer).unwrap(),
461 )]))
462 } else {
463 toml::Value::try_from(serializer).unwrap()
465 };
466
467 match (
468 toml_value.as_table(),
469 table
470 .get_mut(settings_group)
471 .and_then(|value| value.as_table_mut()),
472 ) {
473 (Some(from), Some(to)) => {
474 for (key, value) in from.iter() {
476 to.insert(key.clone(), value.clone());
477 }
478 }
479 _ => {
480 table.insert(settings_group.to_string(), toml_value);
481 }
482 };
483 }
484
485 table
486}
487
488fn build_settings_registry(
494 app_name: &str,
495 types: &TypeRegistry,
496 last_save: Tick,
497) -> SettingsFileRegistry {
498 let mut file_index = SettingsFileRegistry {
501 app_name: app_name.to_string(),
502 files: HashMap::new(),
503 save_timer: Timer::new(Duration::from_secs(1), TimerMode::Once),
504 };
505 file_index.save_timer.pause(); let mut errors = Vec::new();
508 for ty in types.iter() {
511 let Some(reflect_group) = ty.data::<ReflectSettingsGroup>() else {
513 continue;
514 };
515
516 if !ty.contains::<ReflectDefault>() {
517 errors.push(format!(
520 "Type {} has #[reflect(SettingsGroup)], which requires #[reflect(Default)] in order to save or load.",
521 ty.type_info().type_path()
522 ));
523 continue;
524 };
525
526 let filename = reflect_group.settings_source.unwrap_or("settings");
528 let pending_file = file_index
529 .files
530 .entry(filename)
531 .or_insert(SettingsFileManifest {
532 last_save,
533 resource_types: Vec::new(),
534 });
535 pending_file.last_save = last_save;
536 pending_file.resource_types.push(ty.type_id());
537 }
538 if !errors.is_empty() {
539 panic!("{}", errors.join("\n"));
540 }
541
542 file_index
543}
544
545fn load_settings_file(
547 world: &mut World,
548 app_name: &str,
549 filename: &str,
550 manifest: &SettingsFileManifest,
551 types: &TypeRegistry,
552) {
553 let store = SettingsStore::new(app_name);
555 let toml = store.load(filename);
556 if toml.is_none() {
557 warn!("Filename {filename}.toml not found");
558 }
559
560 apply_settings_to_world(world, toml.as_ref(), manifest, types);
561}
562
563fn apply_settings_to_world(
570 world: &mut World,
571 toml: Option<&toml::Table>,
572 manifest: &SettingsFileManifest,
573 types: &TypeRegistry,
574) {
575 for tid in manifest.resource_types.iter() {
576 let ty = types.get(*tid).unwrap();
577 let Some(reflect_settings_group) = ty.data::<ReflectSettingsGroup>() else {
578 continue;
579 };
580
581 let settings_group = reflect_settings_group.settings_group_name;
582 let settings_key = reflect_settings_group.settings_key_name;
583
584 let reflect_component = ty.data::<ReflectComponent>().unwrap();
585 let component_id = world.components().get_id(*tid);
586 let res_entity = component_id.and_then(|cid| world.resource_entities().get(cid));
587
588 if let Some(res_entity) = res_entity {
589 let res_entity_mut = world.entity_mut(res_entity);
591 let Some(mut reflect) = reflect_component.reflect_mut(res_entity_mut) else {
592 continue;
593 };
594
595 if let Some(toml) = toml
596 && let Some(value) = toml.get(settings_group)
597 {
598 let value = if let Some(settings_key) = settings_key {
599 value.get(settings_key).unwrap_or(value)
602 } else {
603 value
605 };
606
607 load_properties(value, &mut *reflect, types);
608 }
609 } else {
610 let reflect_default = ty.data::<ReflectDefault>().unwrap();
612 let mut default_value = reflect_default.default();
613 let mut res_entity = world.spawn_empty();
614
615 if let Some(toml) = toml
616 && let Some(value) = toml.get(settings_group)
617 {
618 let value = if let Some(settings_key) = settings_key {
619 value.get(settings_key).unwrap_or(value)
622 } else {
623 value
625 };
626
627 load_properties(value, &mut *default_value, types);
628 }
629
630 reflect_component.insert(&mut res_entity, default_value.as_partial_reflect(), types);
632 }
633 }
634}
635
636fn is_option_type(type_info: &TypeInfo) -> bool {
637 type_info.type_path_table().module_path() == Some("core::option")
638 && type_info.type_path_table().ident() == Some("Option")
639}
640
641fn load_properties(value: &toml::Value, resource: &mut dyn PartialReflect, types: &TypeRegistry) {
642 let Some(tinfo) = resource.get_represented_type_info() else {
643 return;
644 };
645
646 match tinfo {
647 TypeInfo::Struct(stinfo) => {
648 if let Some(table) = value.as_table()
649 && let ReflectMut::Struct(st_reflect) = resource.reflect_mut()
650 {
651 for (idx, field) in stinfo.field_names().iter().enumerate() {
653 if let Some(toml_field_value) = table.get(*field)
654 && let Some(field_info) = stinfo.field_at(idx)
655 && let Some(field_type) = types.get(field_info.type_id())
656 {
657 let field = st_reflect.field_at_mut(idx).unwrap();
658 if is_option_type(field_type.type_info())
659 && toml_field_value
660 .as_table()
661 .is_some_and(toml::Table::is_empty)
662 {
663 let mut none = DynamicEnum::new_with_index(0, "None", ());
664 none.set_represented_type(Some(field_type.type_info()));
665 field.apply(none.as_partial_reflect());
666 } else {
667 let deserializer = TypedReflectDeserializer::new(field_type, types);
668 if let Ok(field_value) =
669 deserializer.deserialize(toml_field_value.clone())
670 {
671 field.apply(&*field_value);
672 }
673 }
674 }
675 }
676 }
677 }
678 TypeInfo::TupleStruct(tstinfo) => {
679 if let ReflectMut::TupleStruct(tst_reflect) = resource.reflect_mut() {
680 if tst_reflect.field_len() > 1
682 && let Some(array) = value.as_array()
683 {
684 for (idx, toml_field_value) in array.iter().enumerate() {
685 if let Some(field_info) = tstinfo.field_at(idx)
686 && let Some(field_type) = types.get(field_info.type_id())
687 {
688 let deserializer = TypedReflectDeserializer::new(field_type, types);
689 if let Ok(field_value) =
690 deserializer.deserialize(toml_field_value.clone())
691 {
692 tst_reflect.field_mut(idx).unwrap().apply(&*field_value);
694 }
695 }
696 }
697 } else if tst_reflect.field_len() == 1
698 && let Some(field_info) = tstinfo.field_at(0)
699 && let Some(field_type) = types.get(field_info.type_id())
700 {
701 let deserializer = TypedReflectDeserializer::new(field_type, types);
702 if let Ok(field_value) = deserializer.deserialize(value.clone()) {
703 tst_reflect.field_mut(0).unwrap().apply(&*field_value);
705 }
706 }
707 }
708 }
709 TypeInfo::Enum(einfo) => {
710 if let ReflectMut::Enum(en_reflect) = resource.reflect_mut()
711 && let Some(variant_type) = types.get(einfo.type_id())
712 {
713 let deserializer = TypedReflectDeserializer::new(variant_type, types);
714
715 if let Ok(variant_value) = deserializer.deserialize(value.clone()) {
716 en_reflect.apply(&*variant_value);
717 }
718 }
719 }
720 _ => {}
721 }
722}
723
724fn handle_delayed_save(
725 mut settings: ResMut<SettingsFileRegistry>,
726 time: Res<Time<Real>>,
727 mut commands: Commands,
728) {
729 settings.save_timer.tick(time.delta());
730 if settings.save_timer.just_finished() {
731 commands.queue(SaveSettings::IfChanged);
732 }
733}
734
735#[cfg(test)]
736mod tests {
737 use super::*;
738 use bevy_ecs::{change_detection::Tick, schedule::Schedule};
739 use bevy_reflect::Reflect;
740 use bevy_time::Virtual;
741 extern crate self as bevy_settings;
743
744 #[derive(Resource, SettingsGroup, Reflect, Default)]
746 #[reflect(Resource, SettingsGroup, Default)]
747 struct CounterSettings {
748 count: i32,
749 }
750
751 #[derive(Resource, SettingsGroup, Reflect, Default)]
753 #[reflect(Resource, SettingsGroup, Default)]
754 #[settings_group(group = "counter_settings")]
755 struct ExtraCounterSettings {
756 enabled: bool,
757 }
758
759 #[derive(Resource, SettingsGroup, Reflect, Debug, Default, PartialEq)]
760 #[reflect(Resource, SettingsGroup, Default)]
761 #[settings_group(group = "counter_settings", key = "refresh_rate")]
762 enum CounterRefreshRateSettings {
763 #[default]
764 Slow,
765 Fast,
766 }
767
768 #[derive(Resource, SettingsGroup, Reflect, Default)]
770 #[reflect(Resource, SettingsGroup, Default)]
771 #[settings_group(file = "audio")]
772 struct AudioSettings {
773 volume: f32,
774 }
775
776 #[derive(Resource, SettingsGroup, Reflect, Default)]
777 #[reflect(Resource, SettingsGroup, Default)]
778 struct OptionalSettings {
779 value: Option<u32>,
780 }
781
782 #[test]
783 fn test_build_registry_single_struct_resource() {
784 let mut types = TypeRegistry::default();
785 types.register::<CounterSettings>();
786
787 let registry = build_settings_registry("test_app", &types, Tick::new(0));
788
789 assert_eq!(registry.app_name, "test_app");
790 assert_eq!(registry.files.len(), 1);
791 assert!(registry.files.contains_key("settings"));
792
793 let manifest = registry.files.get("settings").unwrap();
794 assert_eq!(manifest.resource_types.len(), 1);
795 }
796
797 #[test]
798 fn test_build_registry_single_enum_resource() {
799 let mut types = TypeRegistry::default();
800 types.register::<CounterRefreshRateSettings>();
801
802 let registry = build_settings_registry("test_app", &types, Tick::new(0));
803
804 assert_eq!(registry.app_name, "test_app");
805 assert_eq!(registry.files.len(), 1);
806 assert!(registry.files.contains_key("settings"));
807
808 let manifest = registry.files.get("settings").unwrap();
809 assert_eq!(manifest.resource_types.len(), 1);
810 }
811
812 #[test]
813 fn test_build_registry_merged_groups() {
814 let mut types = TypeRegistry::default();
815 types.register::<CounterSettings>();
816 types.register::<ExtraCounterSettings>();
817
818 let registry = build_settings_registry("test_app", &types, Tick::new(0));
819
820 assert_eq!(registry.files.len(), 1);
822 assert!(registry.files.contains_key("settings"));
823
824 let manifest = registry.files.get("settings").unwrap();
825 assert_eq!(manifest.resource_types.len(), 2);
827 }
828
829 #[test]
830 fn test_build_registry_separate_files() {
831 let mut types = TypeRegistry::default();
832 types.register::<CounterSettings>();
833 types.register::<AudioSettings>();
834
835 let registry = build_settings_registry("test_app", &types, Tick::new(0));
836
837 assert_eq!(registry.files.len(), 2);
839 assert!(registry.files.contains_key("settings"));
840 assert!(registry.files.contains_key("audio"));
841
842 let settings_manifest = registry.files.get("settings").unwrap();
843 assert_eq!(settings_manifest.resource_types.len(), 1);
844
845 let audio_manifest = registry.files.get("audio").unwrap();
846 assert_eq!(audio_manifest.resource_types.len(), 1);
847 }
848
849 #[test]
850 fn test_resources_to_toml_merges_same_group() {
851 let mut world = World::new();
852 let mut types = TypeRegistry::default();
853 types.register::<CounterSettings>();
854 types.register::<ExtraCounterSettings>();
855 types.register::<CounterRefreshRateSettings>();
856
857 world.insert_resource(CounterSettings { count: 42 });
859 world.insert_resource(ExtraCounterSettings { enabled: true });
860 world.insert_resource(CounterRefreshRateSettings::Fast);
861
862 let manifest = SettingsFileManifest {
864 last_save: Tick::new(0),
865 resource_types: vec![
866 TypeId::of::<CounterSettings>(),
867 TypeId::of::<ExtraCounterSettings>(),
868 TypeId::of::<CounterRefreshRateSettings>(),
869 ],
870 };
871
872 let table = resources_to_toml(&world, &types, &manifest);
873
874 assert!(table.contains_key("counter_settings"));
876 let counter_section = table.get("counter_settings").unwrap().as_table().unwrap();
877
878 assert_eq!(
880 counter_section.get("count").unwrap().as_integer().unwrap(),
881 42
882 );
883 assert!(counter_section.get("enabled").unwrap().as_bool().unwrap());
884 assert_eq!(
885 counter_section
886 .get("refresh_rate")
887 .unwrap()
888 .as_str()
889 .unwrap(),
890 "Fast"
891 );
892 }
893
894 #[test]
895 fn test_round_trip_serialization() {
896 #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug, Default)]
897 #[reflect(Resource, SettingsGroup, Default)]
898 struct SingleFieldTupleStruct(u8);
899
900 #[derive(Reflect, PartialEq, Debug, Default)]
901 #[reflect(Default)]
902 struct NestedStruct {
903 a: u8,
904 b: u16,
905 }
906
907 #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug, Default)]
908 #[reflect(Resource, SettingsGroup, Default)]
909 struct MultiFieldTupleStruct(u8, NestedStruct);
910
911 #[derive(Resource, SettingsGroup, Reflect, Default)]
912 #[reflect(Resource, SettingsGroup, Default)]
913 struct NewTypeSingleTupleStruct(SingleFieldTupleStruct);
914
915 #[derive(Resource, SettingsGroup, Reflect, Default)]
916 #[reflect(Resource, SettingsGroup, Default)]
917 struct NewTypeMultiTupleStruct(SingleFieldTupleStruct, MultiFieldTupleStruct);
918
919 #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug, Default)]
920 #[reflect(Resource, SettingsGroup, Default)]
921 enum EnumUnitVariant {
922 #[default]
923 A,
924 }
925
926 #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
927 #[reflect(Resource, SettingsGroup, Default)]
928 enum EnumSingleTupleVariant {
929 A(u8),
930 }
931
932 impl Default for EnumSingleTupleVariant {
933 fn default() -> Self {
934 EnumSingleTupleVariant::A(0)
935 }
936 }
937
938 #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
939 #[reflect(Resource, SettingsGroup, Default)]
940 enum EnumMultiTupleVariant {
941 A(u16, u32),
942 }
943
944 impl Default for EnumMultiTupleVariant {
945 fn default() -> Self {
946 EnumMultiTupleVariant::A(0, 0)
947 }
948 }
949
950 #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
951 #[reflect(Resource, SettingsGroup, Default)]
952 enum EnumStructVariant {
953 A { x: u8, y: u16 },
954 }
955
956 impl Default for EnumStructVariant {
957 fn default() -> Self {
958 EnumStructVariant::A { x: 0, y: 0 }
959 }
960 }
961
962 #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
963 #[reflect(Resource, SettingsGroup, Default)]
964 enum EnumSingleNewTypeVariant {
965 A(SingleFieldTupleStruct),
966 }
967
968 impl Default for EnumSingleNewTypeVariant {
969 fn default() -> Self {
970 EnumSingleNewTypeVariant::A(SingleFieldTupleStruct(0))
971 }
972 }
973
974 #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
975 #[reflect(Resource, SettingsGroup, Default)]
976 enum EnumMultiNewTypeVariant {
977 A(SingleFieldTupleStruct, MultiFieldTupleStruct),
978 }
979
980 impl Default for EnumMultiNewTypeVariant {
981 fn default() -> Self {
982 EnumMultiNewTypeVariant::A(
983 SingleFieldTupleStruct(0),
984 MultiFieldTupleStruct(0, NestedStruct { a: 0, b: 0 }),
985 )
986 }
987 }
988
989 let mut world = World::new();
990 let mut types = TypeRegistry::default();
991
992 types.register::<CounterSettings>();
993 types.register::<ExtraCounterSettings>();
994 types.register::<CounterRefreshRateSettings>();
995 types.register::<SingleFieldTupleStruct>();
996 types.register::<MultiFieldTupleStruct>();
997 types.register::<NewTypeSingleTupleStruct>();
998 types.register::<NewTypeMultiTupleStruct>();
999 types.register::<EnumUnitVariant>();
1000 types.register::<EnumSingleTupleVariant>();
1001 types.register::<EnumMultiTupleVariant>();
1002 types.register::<EnumStructVariant>();
1003 types.register::<EnumSingleNewTypeVariant>();
1004 types.register::<EnumMultiNewTypeVariant>();
1005
1006 world.insert_resource(CounterSettings { count: 123 });
1008 world.insert_resource(ExtraCounterSettings { enabled: false });
1009 world.insert_resource(CounterRefreshRateSettings::Fast);
1010 world.insert_resource(SingleFieldTupleStruct(1));
1011 world.insert_resource(MultiFieldTupleStruct(2, NestedStruct { a: 1, b: 2 }));
1012 world.insert_resource(NewTypeSingleTupleStruct(SingleFieldTupleStruct(1)));
1013 world.insert_resource(NewTypeMultiTupleStruct(
1014 SingleFieldTupleStruct(1),
1015 MultiFieldTupleStruct(2, NestedStruct { a: 1, b: 2 }),
1016 ));
1017 world.insert_resource(EnumUnitVariant::A);
1018 world.insert_resource(EnumSingleTupleVariant::A(1));
1019 world.insert_resource(EnumMultiTupleVariant::A(1, 2));
1020 world.insert_resource(EnumStructVariant::A { x: 1, y: 2 });
1021 world.insert_resource(EnumSingleNewTypeVariant::A(SingleFieldTupleStruct(1)));
1022 world.insert_resource(EnumMultiNewTypeVariant::A(
1023 SingleFieldTupleStruct(1),
1024 MultiFieldTupleStruct(2, NestedStruct { a: 1, b: 2 }),
1025 ));
1026
1027 let manifest = SettingsFileManifest {
1029 last_save: Tick::new(0),
1030 resource_types: vec![
1031 TypeId::of::<CounterSettings>(),
1032 TypeId::of::<ExtraCounterSettings>(),
1033 TypeId::of::<CounterRefreshRateSettings>(),
1034 TypeId::of::<SingleFieldTupleStruct>(),
1035 TypeId::of::<MultiFieldTupleStruct>(),
1036 TypeId::of::<NewTypeSingleTupleStruct>(),
1037 TypeId::of::<NewTypeMultiTupleStruct>(),
1038 TypeId::of::<EnumUnitVariant>(),
1039 TypeId::of::<EnumSingleTupleVariant>(),
1040 TypeId::of::<EnumMultiTupleVariant>(),
1041 TypeId::of::<EnumStructVariant>(),
1042 TypeId::of::<EnumSingleNewTypeVariant>(),
1043 TypeId::of::<EnumMultiNewTypeVariant>(),
1044 ],
1045 };
1046
1047 let table = resources_to_toml(&world, &types, &manifest);
1049
1050 let mut new_world = World::new();
1052 apply_settings_to_world(&mut new_world, Some(&table), &manifest, &types);
1053
1054 let counter = new_world.get_resource::<CounterSettings>().unwrap();
1056 assert_eq!(counter.count, 123);
1057
1058 let extra = new_world.get_resource::<ExtraCounterSettings>().unwrap();
1059 assert!(!extra.enabled);
1060
1061 let refresh_rate = new_world
1062 .get_resource::<CounterRefreshRateSettings>()
1063 .unwrap();
1064 assert_eq!(*refresh_rate, CounterRefreshRateSettings::Fast);
1065
1066 let single_field_tuple_struct = new_world.get_resource::<SingleFieldTupleStruct>().unwrap();
1067 assert_eq!(single_field_tuple_struct.0, 1);
1068
1069 let multi_field_tuple_struct = new_world.get_resource::<MultiFieldTupleStruct>().unwrap();
1070 assert_eq!(multi_field_tuple_struct.0, 2);
1071 assert_eq!(multi_field_tuple_struct.1.a, 1);
1072 assert_eq!(multi_field_tuple_struct.1.b, 2);
1073
1074 let new_type_single_tuple_struct = new_world
1075 .get_resource::<NewTypeSingleTupleStruct>()
1076 .unwrap();
1077 assert_eq!(new_type_single_tuple_struct.0 .0, 1);
1078
1079 let new_type_multi_tuple_struct =
1080 new_world.get_resource::<NewTypeMultiTupleStruct>().unwrap();
1081 assert_eq!(new_type_multi_tuple_struct.0 .0, 1);
1082 assert_eq!(new_type_multi_tuple_struct.1 .0, 2);
1083 assert_eq!(new_type_multi_tuple_struct.1 .1.a, 1);
1084 assert_eq!(new_type_multi_tuple_struct.1 .1.b, 2);
1085
1086 let enum_unit_variant = new_world.get_resource::<EnumUnitVariant>().unwrap();
1087 assert_eq!(*enum_unit_variant, EnumUnitVariant::A);
1088
1089 let enum_single_tuple_variant = new_world.get_resource::<EnumSingleTupleVariant>().unwrap();
1090 assert_eq!(*enum_single_tuple_variant, EnumSingleTupleVariant::A(1));
1091
1092 let enum_multi_tuple_variant = new_world.get_resource::<EnumMultiTupleVariant>().unwrap();
1093 assert_eq!(*enum_multi_tuple_variant, EnumMultiTupleVariant::A(1, 2));
1094
1095 let enum_struct_variant = new_world.get_resource::<EnumStructVariant>().unwrap();
1096 assert_eq!(*enum_struct_variant, EnumStructVariant::A { x: 1, y: 2 });
1097
1098 let enum_single_new_type_variant = new_world
1099 .get_resource::<EnumSingleNewTypeVariant>()
1100 .unwrap();
1101 assert_eq!(
1102 *enum_single_new_type_variant,
1103 EnumSingleNewTypeVariant::A(SingleFieldTupleStruct(1))
1104 );
1105
1106 let enum_multi_new_type_variant =
1107 new_world.get_resource::<EnumMultiNewTypeVariant>().unwrap();
1108 assert_eq!(
1109 *enum_multi_new_type_variant,
1110 EnumMultiNewTypeVariant::A(
1111 SingleFieldTupleStruct(1),
1112 MultiFieldTupleStruct(2, NestedStruct { a: 1, b: 2 })
1113 )
1114 );
1115 }
1116
1117 #[test]
1118 fn test_round_trip_with_existing_resources() {
1119 let mut world = World::new();
1120 let mut types = TypeRegistry::default();
1121 types.register::<CounterSettings>();
1122 types.register::<CounterRefreshRateSettings>();
1123
1124 world.insert_resource(CounterSettings { count: 100 });
1126 world.insert_resource(CounterRefreshRateSettings::Fast);
1127
1128 let manifest = SettingsFileManifest {
1129 last_save: Tick::new(0),
1130 resource_types: vec![
1131 TypeId::of::<CounterSettings>(),
1132 TypeId::of::<CounterRefreshRateSettings>(),
1133 ],
1134 };
1135
1136 let table = resources_to_toml(&world, &types, &manifest);
1138
1139 world.resource_mut::<CounterSettings>().count = 999;
1141 *world.resource_mut::<CounterRefreshRateSettings>() = CounterRefreshRateSettings::Slow;
1142
1143 apply_settings_to_world(&mut world, Some(&table), &manifest, &types);
1145
1146 let counter = world.get_resource::<CounterSettings>().unwrap();
1147 assert_eq!(counter.count, 100);
1148 let refresh_rate = world.get_resource::<CounterRefreshRateSettings>().unwrap();
1149 assert_eq!(*refresh_rate, CounterRefreshRateSettings::Fast);
1150 }
1151
1152 #[test]
1153 fn test_partial_toml_preserves_missing_fields() {
1154 let mut world = World::new();
1155 let mut types = TypeRegistry::default();
1156 types.register::<CounterSettings>();
1157 types.register::<ExtraCounterSettings>();
1158 types.register::<CounterRefreshRateSettings>();
1159
1160 world.insert_resource(CounterSettings { count: 50 });
1162 world.insert_resource(ExtraCounterSettings { enabled: true });
1163 world.insert_resource(CounterRefreshRateSettings::Fast);
1164
1165 let mut table = toml::Table::new();
1167 let mut counter_section = toml::Table::new();
1168 counter_section.insert("count".to_string(), toml::Value::Integer(999));
1169 table.insert(
1170 "counter_settings".to_string(),
1171 toml::Value::Table(counter_section),
1172 );
1173 let manifest = SettingsFileManifest {
1176 last_save: Tick::new(0),
1177 resource_types: vec![
1178 TypeId::of::<CounterSettings>(),
1179 TypeId::of::<ExtraCounterSettings>(),
1180 TypeId::of::<CounterRefreshRateSettings>(),
1181 ],
1182 };
1183
1184 apply_settings_to_world(&mut world, Some(&table), &manifest, &types);
1186
1187 let counter = world.get_resource::<CounterSettings>().unwrap();
1189 assert_eq!(counter.count, 999);
1190
1191 let extra = world.get_resource::<ExtraCounterSettings>().unwrap();
1193 assert!(extra.enabled);
1194
1195 let refresh_rate = world.get_resource::<CounterRefreshRateSettings>().unwrap();
1197 assert_eq!(*refresh_rate, CounterRefreshRateSettings::Fast);
1198 }
1199
1200 #[test]
1201 fn test_handle_delayed_save_ticks_with_real_time_while_paused() {
1202 let mut world = World::new();
1203 world.insert_resource(Time::<Real>::default());
1204 world.insert_resource(Time::<Virtual>::default());
1205 world.resource_mut::<Time<Virtual>>().pause();
1207
1208 world.insert_resource(SettingsFileRegistry {
1210 app_name: "test_app".to_string(),
1211 files: HashMap::new(),
1212 save_timer: {
1213 let mut timer = Timer::new(Duration::from_millis(500), TimerMode::Once);
1214 timer.unpause();
1215 timer
1216 },
1217 });
1218
1219 world
1221 .resource_mut::<Time<Real>>()
1222 .advance_by(Duration::from_millis(600));
1223
1224 let mut schedule = Schedule::default();
1225 schedule.add_systems(handle_delayed_save);
1226 schedule.run(&mut world);
1227
1228 let registry = world.resource::<SettingsFileRegistry>();
1229 assert!(registry.save_timer.just_finished());
1230 }
1231
1232 #[test]
1233 fn test_none_value_apply_settings() {
1234 let mut world = World::new();
1235 let mut other_world = World::new();
1236 let mut types = TypeRegistry::default();
1237
1238 types.register::<OptionalSettings>();
1239
1240 let manifest = SettingsFileManifest {
1241 last_save: Tick::new(0),
1242 resource_types: vec![TypeId::of::<OptionalSettings>()],
1243 };
1244
1245 world.insert_resource(OptionalSettings { value: None });
1246 let table = resources_to_toml(&world, &types, &manifest);
1247
1248 other_world.insert_resource(OptionalSettings { value: Some(12) });
1249 apply_settings_to_world(&mut other_world, Some(&table), &manifest, &types);
1250
1251 let settings = other_world.get_resource::<OptionalSettings>().unwrap();
1252 assert_eq!(settings.value, None);
1253 }
1254}