Skip to main content

bevy_settings/
lib.rs

1//! Framework for saving and loading user settings files in Bevy
2//! applications.
3//!
4//! Refer to [`SettingsPlugin`] for detailed usage information.
5
6use core::any::TypeId;
7use core::time::Duration;
8use std::collections::HashMap;
9
10use bevy_app::{App, Plugin, PostUpdate};
11use bevy_ecs::{
12    change_detection::Tick,
13    reflect::{AppTypeRegistry, ReflectComponent, ReflectResource},
14    resource::Resource,
15    system::{Command, Commands, Res, ResMut},
16    world::World,
17};
18pub use bevy_ecs_macros::SettingsGroup;
19use bevy_log::warn;
20use bevy_reflect::{
21    prelude::ReflectDefault,
22    serde::{TypedReflectDeserializer, TypedReflectSerializer},
23    FromReflect, FromType, PartialReflect, ReflectMut, TypeInfo, TypePath, TypeRegistration,
24    TypeRegistry,
25};
26
27#[cfg(not(target_arch = "wasm32"))]
28mod store_fs;
29
30#[cfg(target_arch = "wasm32")]
31mod store_wasm;
32
33use bevy_time::{Time, Timer, TimerMode};
34use serde::de::DeserializeSeed;
35#[cfg(not(target_arch = "wasm32"))]
36use store_fs::SettingsStore;
37
38#[cfg(target_arch = "wasm32")]
39use store_wasm::SettingsStore;
40
41/// Plugin to orchestrate loading and saving settings.
42///
43/// You are required to provide a unique application name, so that your settings don't overwrite
44/// those of other apps. To ensure global uniqueness, it is recommended to use a
45/// [reverse domain name](https://en.wikipedia.org/wiki/Reverse_domain_name_notation),
46/// e.g. "com.example.myapp". The plugin will create a directory with that name in the
47/// appropriate filesystem location (depending on platform) for app settings. For platforms
48/// without filesystems, other storage mechanisms will be used.
49///
50/// If you are do not have a domain name and cannot
51/// afford one, use a reverse domain based on the URL of your repo (GitHub, GitLab, Codeberg
52/// and so on).
53///
54/// Adding this plugin causes an immediate load of settings (from either the filesystem or
55/// browser local storage, depending on platform).
56///
57/// When using this plugin, care must be taken to ensure that plugins execute in the proper order.
58/// Loading settings causes registered settings to be inserted into the world as bevy resources.
59/// You cannot access these values before they are loaded, but you may want to use the loaded values
60/// when configuring other plugins. For this reason, it's generally a good idea to initialize and
61/// load settings before other plugins. The settings plugin does not depend on any other
62/// plugins.
63///
64/// In many cases, you may want to introduce additional "glue" plugins that copy setting
65/// properties after they are loaded. For example, the
66/// [`WindowPlugin`](https://docs.rs/bevy/latest/bevy/prelude/struct.WindowPlugin.html) plugin knows
67/// nothing about settings, but if you want the window size and position to persist between runs
68/// you can add an additional plugin which copies the window settings from the resource to the
69/// actual window entity.
70///
71/// Saving of settings is not automatic; the recommended practice is to issue a
72/// [`SaveSettingsDeferred`] command after modifying a settings resource. This will wait for
73/// a short interval and then spawn an i/o task to write out the changed settings file. You can
74/// also issue a [`SaveSettingsSync::IfChanged`] command immediately before exiting the app.
75/// Note that on some platforms, depending on how the user exits (such as invoking Command-Q on
76/// ``MacOS``) there may be no opportunity to intercept the app exit event, so the most reliable
77/// approach is to use both techniques: deferred save and save-on-exit.
78///
79/// Saving is crash-resistant: if the app crashes in the middle of a save, the settings file
80/// will not be corrupted (it writes to a temporary file first, then uses atomic operations to
81/// replace the previous file).
82pub struct SettingsPlugin {
83    /// The unique name of the application.
84    pub app_name: String,
85}
86
87impl SettingsPlugin {
88    /// Construct a new `SettingsPlugin` for the given application name.
89    pub fn new(app_name: &str) -> Self {
90        Self {
91            app_name: app_name.to_string(),
92        }
93    }
94}
95
96impl Plugin for SettingsPlugin {
97    fn build(&self, app: &mut App) {
98        let app_name = self.app_name.clone();
99        let world = app.world();
100        let last_save = world.read_change_tick();
101
102        // Get the type registry and clone the Arc so we don't have to worry about borrowing.
103        let Some(app_types) = world.get_resource::<AppTypeRegistry>() else {
104            return;
105        };
106        let app_types = app_types.clone();
107        let types = app_types.read();
108
109        let world = app.world_mut();
110        let file_index = build_settings_registry(&app_name, &types, last_save);
111
112        // Now load each of the toml files we discovered, and apply their properties to
113        // the resources in the world.
114        for (filename, manifest) in file_index.files.iter() {
115            load_settings_file(world, &app_name, filename, manifest, &types);
116        }
117
118        // Cache the index so that we don't have to do it again when saving (and also makes
119        // saving more deterministic).
120        drop(types);
121        world.insert_resource::<SettingsFileRegistry>(file_index);
122
123        app.add_systems(PostUpdate, handle_delayed_save);
124    }
125}
126
127/// Trait which identifies a type as corresponding to a section with a settings file.
128///
129/// You can override the name of the section with `settings_group(group = "<name>")`.
130/// For enum `SettingGroup`s, you can also override the name of its key with `settings_group(key = "<name>")`
131/// The name should be in ``snake_case`` to be consistent with TOML style.
132/// If there is a collision between names (multiple resources have the same name) then
133/// the resulting properties will be merged into a single section.
134///
135/// You can also control which file the type gets saved to via
136/// `settings_group(file = "<filename>")`. This should be the base name of the file without the
137/// extension. The default name is `settings`, which will cause the settings to be written out
138/// to `settings.toml` in the app's settings directory.
139pub trait SettingsGroup: Resource {
140    /// The name of the logical section within the settings file.
141    fn settings_group_name() -> &'static str;
142
143    /// The key name within the settings file.
144    /// For structs, this should be set to `None`; The struct’s field names will be used as keys.
145    /// For enums, the `SettingsGroup` will use this key name within the settings file for its sole key-value pair.
146    /// This is typically the same as the group name, but can be customized.
147    fn settings_key_name() -> Option<&'static str>;
148
149    /// The name of the configuration file that contains this settings group.
150    // TODO: Eventually convert this into an enum which represents various configuration sources.
151    fn settings_source() -> Option<&'static str>;
152}
153
154/// Reflected data from a [`SettingsGroup`].
155#[derive(Clone)]
156pub struct ReflectSettingsGroup {
157    /// The name of the logical section within the settings file.
158    settings_group_name: &'static str,
159    /// The key name within the settings file. Should only be `Some` for enums.
160    settings_key_name: Option<&'static str>,
161    /// The name of the settings file, defaults to "settings".
162    settings_source: Option<&'static str>,
163}
164
165impl<T: SettingsGroup + FromReflect + TypePath> FromType<T> for ReflectSettingsGroup {
166    fn from_type() -> Self {
167        ReflectSettingsGroup {
168            settings_group_name: T::settings_group_name(),
169            settings_key_name: T::settings_key_name(),
170            settings_source: T::settings_source(),
171        }
172    }
173
174    fn insert_dependencies(type_registration: &mut TypeRegistration) {
175        type_registration.register_type_data::<ReflectResource, T>();
176    }
177}
178
179/// List of resource types that will be associated with a specific settings file.
180/// Also tracks when that file was last written or read.
181#[derive(Default)]
182struct SettingsFileManifest {
183    last_save: Tick,
184    resource_types: Vec<TypeId>,
185}
186
187/// Records the game tick when settings were last loaded or saved. This is used to determine
188/// which settings files have changed and need to be saved. Also tracks which settings files
189/// are associated with which resource types.
190#[derive(Resource)]
191struct SettingsFileRegistry {
192    /// App name (from plugin)
193    app_name: String,
194
195    /// List of known settings files, determined by scanning reflection registry.
196    files: HashMap<&'static str, SettingsFileManifest>,
197
198    /// Timer used for batched saving.
199    save_timer: Timer,
200}
201
202/// A Command which saves settings to disk. This blocks the command queue until saving
203/// is complete.
204#[derive(Default, PartialEq)]
205pub enum SaveSettingsSync {
206    /// Save settings only if they have changed since the most recent load or save.
207    #[default]
208    IfChanged,
209    /// Save settings unconditionally.
210    Always,
211}
212
213impl Command for SaveSettingsSync {
214    type Out = ();
215
216    fn apply(self, world: &mut World) {
217        save_settings(world, false, self == SaveSettingsSync::Always);
218    }
219}
220
221/// A [`Command`] which saves settings to disk. Actual file system operations happen in another thread.
222#[derive(Default, PartialEq)]
223pub enum SaveSettings {
224    /// Save settings only if they have changed since the most recent load or save.
225    #[default]
226    IfChanged,
227    /// Save settings unconditionally.
228    Always,
229}
230
231impl Command for SaveSettings {
232    type Out = ();
233
234    fn apply(self, world: &mut World) {
235        save_settings(world, true, self == SaveSettings::Always);
236    }
237}
238
239/// A Command which saves changed settings after a delay. This is debounced: issuing this
240/// command multiple times resets the delay timer each time. This is meant to be used for settings
241/// which change at a high frequency, such as dragging a slider which controls the game's audio
242/// volume. The default delay is 1.0 seconds.
243pub struct SaveSettingsDeferred(pub Duration);
244
245impl Default for SaveSettingsDeferred {
246    fn default() -> Self {
247        Self(Duration::from_secs(1))
248    }
249}
250
251impl Command for SaveSettingsDeferred {
252    type Out = ();
253
254    fn apply(self, world: &mut World) {
255        let Some(mut registry) = world.get_resource_mut::<SettingsFileRegistry>() else {
256            return;
257        };
258
259        registry.save_timer.set_duration(self.0);
260        registry.save_timer.reset();
261        registry.save_timer.unpause();
262    }
263}
264
265fn save_settings(world: &mut World, use_async: bool, force: bool) {
266    let this_run = world.change_tick();
267    let Some(registry) = world.get_resource::<SettingsFileRegistry>() else {
268        warn!("Settings registry not found - did you forget to install the SettingsPlugin?");
269        return;
270    };
271    let Some(app_types) = world.get_resource::<AppTypeRegistry>() else {
272        return;
273    };
274    let app_types = app_types.clone();
275    let types = app_types.read();
276
277    for (filename, manifest) in registry.files.iter() {
278        if force || has_settings_changed(world, manifest) {
279            let table = resources_to_toml(world, &types, manifest);
280            let store = SettingsStore::new(&registry.app_name);
281            if use_async {
282                store.save_async(filename, table);
283            } else {
284                store.save(filename, table);
285            }
286        }
287    }
288
289    // Update timestamps
290    let mut registry = world.get_resource_mut::<SettingsFileRegistry>().unwrap();
291    for manifest in registry.files.values_mut() {
292        manifest.last_save = this_run;
293    }
294}
295
296fn has_settings_changed(world: &World, manifest: &SettingsFileManifest) -> bool {
297    let this_run = world.read_change_tick();
298    manifest.resource_types.iter().any(|r| {
299        let Some(component_id) = world.components().get_id(*r) else {
300            return false;
301        };
302        if let Some(resource_change) = world.get_resource_change_ticks_by_id(component_id) {
303            return resource_change.is_changed(manifest.last_save, this_run);
304        }
305        false
306    })
307}
308
309fn resources_to_toml(
310    world: &World,
311    types: &TypeRegistry,
312    manifest: &SettingsFileManifest,
313) -> toml::map::Map<String, toml::Value> {
314    let mut table = toml::Table::new();
315
316    for tid in manifest.resource_types.iter() {
317        let ty = types.get(*tid).unwrap();
318
319        let Some(cmp) = ty.data::<ReflectComponent>() else {
320            continue;
321        };
322
323        let Some(reflect_settings_group) = ty.data::<ReflectSettingsGroup>() else {
324            continue;
325        };
326
327        let settings_group = reflect_settings_group.settings_group_name;
328        let settings_key = reflect_settings_group.settings_key_name;
329
330        let Some(component_id) = world.components().get_id(*tid) else {
331            continue;
332        };
333
334        let Some(res_entity) = world.resource_entities().get(component_id) else {
335            continue;
336        };
337        let res_entity_ref = world.entity(res_entity);
338        let Some(reflect) = cmp.reflect(res_entity_ref) else {
339            continue;
340        };
341
342        let serializer = TypedReflectSerializer::new(reflect.as_partial_reflect(), types);
343
344        let toml_value = if let Some(settings_key) = settings_key {
345            // convert toml value into a key value pair if settings_key is set. settings_key is only set for enums
346            toml::Value::Table(toml::Table::from_iter([(
347                settings_key.to_string(),
348                toml::Value::try_from(serializer).unwrap(),
349            )]))
350        } else {
351            // Otherwise, the whole struct is serialized into toml
352            toml::Value::try_from(serializer).unwrap()
353        };
354
355        match (
356            toml_value.as_table(),
357            table
358                .get_mut(settings_group)
359                .and_then(|value| value.as_table_mut()),
360        ) {
361            (Some(from), Some(to)) => {
362                // Merge the tables
363                for (key, value) in from.iter() {
364                    to.insert(key.clone(), value.clone());
365                }
366            }
367            _ => {
368                table.insert(settings_group.to_string(), toml_value);
369            }
370        };
371    }
372
373    table
374}
375
376/// Builds the settings file registry by scanning the type registry for settings resources.
377/// This is separated from loading to enable testing without file I/O.
378///
379/// Returns the [`SettingsFileRegistry`] that tracks which resources are associated with
380/// which settings files.
381fn build_settings_registry(
382    app_name: &str,
383    types: &TypeRegistry,
384    last_save: Tick,
385) -> SettingsFileRegistry {
386    // Build an index that remembers all of the resource types that are to be saved to
387    // each individual settings file.
388    let mut file_index = SettingsFileRegistry {
389        app_name: app_name.to_string(),
390        files: HashMap::new(),
391        save_timer: Timer::new(Duration::from_secs(1), TimerMode::Once),
392    };
393    file_index.save_timer.pause(); // Ensure timer is initially paused
394
395    // Scan through types looking for resources that have the necessary traits and
396    // annotations.
397    for ty in types.iter() {
398        if !ty.contains::<ReflectDefault>() {
399            continue;
400        };
401
402        let Some(reflect_group) = ty.data::<ReflectSettingsGroup>() else {
403            continue;
404        };
405
406        // If no filename is specified, use "settings"
407        let filename = reflect_group.settings_source.unwrap_or("settings");
408        let pending_file = file_index
409            .files
410            .entry(filename)
411            .or_insert(SettingsFileManifest {
412                last_save,
413                resource_types: Vec::new(),
414            });
415        pending_file.last_save = last_save;
416        pending_file.resource_types.push(ty.type_id());
417    }
418
419    file_index
420}
421
422/// Loads a single settings file and applies its values to the world's resources.
423fn load_settings_file(
424    world: &mut World,
425    app_name: &str,
426    filename: &str,
427    manifest: &SettingsFileManifest,
428    types: &TypeRegistry,
429) {
430    // Load the TOML file
431    let store = SettingsStore::new(app_name);
432    let toml = store.load(filename);
433    if toml.is_none() {
434        warn!("Filename {filename}.toml not found");
435    }
436
437    apply_settings_to_world(world, toml.as_ref(), manifest, types);
438}
439
440/// Applies settings from a TOML table to the world's resources.
441/// This is separated from file loading to enable testing without filesystem access.
442///
443/// For each resource type in the manifest, this function either:
444/// - Updates an existing resource with values from the TOML, or
445/// - Creates a new resource with default values merged with TOML values
446fn apply_settings_to_world(
447    world: &mut World,
448    toml: Option<&toml::Table>,
449    manifest: &SettingsFileManifest,
450    types: &TypeRegistry,
451) {
452    for tid in manifest.resource_types.iter() {
453        let ty = types.get(*tid).unwrap();
454        let Some(reflect_settings_group) = ty.data::<ReflectSettingsGroup>() else {
455            continue;
456        };
457
458        let settings_group = reflect_settings_group.settings_group_name;
459        let settings_key = reflect_settings_group.settings_key_name;
460
461        let reflect_component = ty.data::<ReflectComponent>().unwrap();
462        let component_id = world.components().get_id(*tid);
463        let res_entity = component_id.and_then(|cid| world.resource_entities().get(cid));
464
465        if let Some(res_entity) = res_entity {
466            // Resource already exists, so apply toml properties to it.
467            let res_entity_mut = world.entity_mut(res_entity);
468            let Some(mut reflect) = reflect_component.reflect_mut(res_entity_mut) else {
469                continue;
470            };
471
472            if let Some(toml) = toml
473                && let Some(value) = toml.get(settings_group)
474            {
475                let value = if let Some(settings_key) = settings_key {
476                    // If there is a settings key, then we need to look one level deeper in the TOML
477                    // to find the actual properties to apply to the resource.
478                    value.get(settings_key).unwrap_or(value)
479                } else {
480                    // No settings key, so we can apply the whole section to the resource
481                    value
482                };
483
484                load_properties(value, &mut *reflect, types);
485            }
486        } else {
487            // The resource does not exist, so create a default.
488            let reflect_default = ty.data::<ReflectDefault>().unwrap();
489            let mut default_value = reflect_default.default();
490            let mut res_entity = world.spawn_empty();
491
492            if let Some(toml) = toml
493                && let Some(value) = toml.get(settings_group)
494            {
495                let value = if let Some(settings_key) = settings_key {
496                    // If there is a settings key, then we need to look one level deeper in the TOML
497                    // to find the actual properties to apply to the resource.
498                    value.get(settings_key).unwrap_or(value)
499                } else {
500                    // No settings key, so we can apply the whole section to the resource
501                    value
502                };
503
504                load_properties(value, &mut *default_value, types);
505            }
506
507            // Now add the new resource to the world.
508            reflect_component.insert(&mut res_entity, default_value.as_partial_reflect(), types);
509        }
510    }
511}
512
513fn load_properties(value: &toml::Value, resource: &mut dyn PartialReflect, types: &TypeRegistry) {
514    let Some(tinfo) = resource.get_represented_type_info() else {
515        return;
516    };
517
518    match tinfo {
519        TypeInfo::Struct(stinfo) => {
520            if let Some(table) = value.as_table()
521                && let ReflectMut::Struct(st_reflect) = resource.reflect_mut()
522            {
523                // Deserialize matching field names, ignore ones that don't match.
524                for (idx, field) in stinfo.field_names().iter().enumerate() {
525                    if let Some(toml_field_value) = table.get(*field)
526                        && let Some(field_info) = stinfo.field_at(idx)
527                        && let Some(field_type) = types.get(field_info.type_id())
528                    {
529                        let deserializer = TypedReflectDeserializer::new(field_type, types);
530                        if let Ok(field_value) = deserializer.deserialize(toml_field_value.clone())
531                        {
532                            // Should be safe to unwrap here since we know the field exists (above).
533                            st_reflect.field_at_mut(idx).unwrap().apply(&*field_value);
534                        }
535                    }
536                }
537            }
538        }
539        TypeInfo::TupleStruct(tstinfo) => {
540            if let ReflectMut::TupleStruct(tst_reflect) = resource.reflect_mut() {
541                // tuple structs with length > 1 are always serialized as arrays
542                if tst_reflect.field_len() > 1
543                    && let Some(array) = value.as_array()
544                {
545                    for (idx, toml_field_value) in array.iter().enumerate() {
546                        if let Some(field_info) = tstinfo.field_at(idx)
547                            && let Some(field_type) = types.get(field_info.type_id())
548                        {
549                            let deserializer = TypedReflectDeserializer::new(field_type, types);
550                            if let Ok(field_value) =
551                                deserializer.deserialize(toml_field_value.clone())
552                            {
553                                // Should be safe to unwrap here since we know the field exists (above).
554                                tst_reflect.field_mut(idx).unwrap().apply(&*field_value);
555                            }
556                        }
557                    }
558                } else if tst_reflect.field_len() == 1
559                    && let Some(field_info) = tstinfo.field_at(0)
560                    && let Some(field_type) = types.get(field_info.type_id())
561                {
562                    let deserializer = TypedReflectDeserializer::new(field_type, types);
563                    if let Ok(field_value) = deserializer.deserialize(value.clone()) {
564                        // Should be safe to unwrap here since we know the field exists (above).
565                        tst_reflect.field_mut(0).unwrap().apply(&*field_value);
566                    }
567                }
568            }
569        }
570        TypeInfo::Enum(einfo) => {
571            if let ReflectMut::Enum(en_reflect) = resource.reflect_mut()
572                && let Some(variant_type) = types.get(einfo.type_id())
573            {
574                let deserializer = TypedReflectDeserializer::new(variant_type, types);
575
576                if let Ok(variant_value) = deserializer.deserialize(value.clone()) {
577                    en_reflect.apply(&*variant_value);
578                }
579            }
580        }
581        _ => {}
582    }
583}
584
585fn handle_delayed_save(
586    mut settings: ResMut<SettingsFileRegistry>,
587    time: Res<Time>,
588    mut commands: Commands,
589) {
590    settings.save_timer.tick(time.delta());
591    if settings.save_timer.just_finished() {
592        commands.queue(SaveSettings::IfChanged);
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use bevy_ecs::change_detection::Tick;
600    use bevy_reflect::Reflect;
601    // Required to make proc macros work in bevy itself.
602    extern crate self as bevy_settings;
603
604    /// Test resource that uses default settings group name (derived from type name)
605    #[derive(Resource, SettingsGroup, Reflect, Default)]
606    #[reflect(Resource, SettingsGroup, Default)]
607    struct CounterSettings {
608        count: i32,
609    }
610
611    /// Test resource that shares the same settings group name as another resource
612    #[derive(Resource, SettingsGroup, Reflect, Default)]
613    #[reflect(Resource, SettingsGroup, Default)]
614    #[settings_group(group = "counter_settings")]
615    struct ExtraCounterSettings {
616        enabled: bool,
617    }
618
619    #[derive(Resource, SettingsGroup, Reflect, Debug, Default, PartialEq)]
620    #[reflect(Resource, SettingsGroup, Default)]
621    #[settings_group(group = "counter_settings", key = "refresh_rate")]
622    enum CounterRefreshRateSettings {
623        #[default]
624        Slow,
625        Fast,
626    }
627
628    /// Test resource that uses a different settings file
629    #[derive(Resource, SettingsGroup, Reflect, Default)]
630    #[reflect(Resource, SettingsGroup, Default)]
631    #[settings_group(file = "audio")]
632    struct AudioSettings {
633        volume: f32,
634    }
635
636    #[test]
637    fn test_build_registry_single_struct_resource() {
638        let mut types = TypeRegistry::default();
639        types.register::<CounterSettings>();
640
641        let registry = build_settings_registry("test_app", &types, Tick::new(0));
642
643        assert_eq!(registry.app_name, "test_app");
644        assert_eq!(registry.files.len(), 1);
645        assert!(registry.files.contains_key("settings"));
646
647        let manifest = registry.files.get("settings").unwrap();
648        assert_eq!(manifest.resource_types.len(), 1);
649    }
650
651    #[test]
652    fn test_build_registry_single_enum_resource() {
653        let mut types = TypeRegistry::default();
654        types.register::<CounterRefreshRateSettings>();
655
656        let registry = build_settings_registry("test_app", &types, Tick::new(0));
657
658        assert_eq!(registry.app_name, "test_app");
659        assert_eq!(registry.files.len(), 1);
660        assert!(registry.files.contains_key("settings"));
661
662        let manifest = registry.files.get("settings").unwrap();
663        assert_eq!(manifest.resource_types.len(), 1);
664    }
665
666    #[test]
667    fn test_build_registry_merged_groups() {
668        let mut types = TypeRegistry::default();
669        types.register::<CounterSettings>();
670        types.register::<ExtraCounterSettings>();
671
672        let registry = build_settings_registry("test_app", &types, Tick::new(0));
673
674        // Both resources should be in the same file
675        assert_eq!(registry.files.len(), 1);
676        assert!(registry.files.contains_key("settings"));
677
678        let manifest = registry.files.get("settings").unwrap();
679        // Both resources should be tracked
680        assert_eq!(manifest.resource_types.len(), 2);
681    }
682
683    #[test]
684    fn test_build_registry_separate_files() {
685        let mut types = TypeRegistry::default();
686        types.register::<CounterSettings>();
687        types.register::<AudioSettings>();
688
689        let registry = build_settings_registry("test_app", &types, Tick::new(0));
690
691        // Resources should be in different files
692        assert_eq!(registry.files.len(), 2);
693        assert!(registry.files.contains_key("settings"));
694        assert!(registry.files.contains_key("audio"));
695
696        let settings_manifest = registry.files.get("settings").unwrap();
697        assert_eq!(settings_manifest.resource_types.len(), 1);
698
699        let audio_manifest = registry.files.get("audio").unwrap();
700        assert_eq!(audio_manifest.resource_types.len(), 1);
701    }
702
703    #[test]
704    fn test_resources_to_toml_merges_same_group() {
705        let mut world = World::new();
706        let mut types = TypeRegistry::default();
707        types.register::<CounterSettings>();
708        types.register::<ExtraCounterSettings>();
709        types.register::<CounterRefreshRateSettings>();
710
711        // Insert both resources
712        world.insert_resource(CounterSettings { count: 42 });
713        world.insert_resource(ExtraCounterSettings { enabled: true });
714        world.insert_resource(CounterRefreshRateSettings::Fast);
715
716        // Build a manifest with both resource types
717        let manifest = SettingsFileManifest {
718            last_save: Tick::new(0),
719            resource_types: vec![
720                TypeId::of::<CounterSettings>(),
721                TypeId::of::<ExtraCounterSettings>(),
722                TypeId::of::<CounterRefreshRateSettings>(),
723            ],
724        };
725
726        let table = resources_to_toml(&world, &types, &manifest);
727
728        // Both resources should be merged into the same "counter_settings" section
729        assert!(table.contains_key("counter_settings"));
730        let counter_section = table.get("counter_settings").unwrap().as_table().unwrap();
731
732        // Check that fields are present in the merged section
733        assert_eq!(
734            counter_section.get("count").unwrap().as_integer().unwrap(),
735            42
736        );
737        assert!(counter_section.get("enabled").unwrap().as_bool().unwrap());
738        assert_eq!(
739            counter_section
740                .get("refresh_rate")
741                .unwrap()
742                .as_str()
743                .unwrap(),
744            "Fast"
745        );
746    }
747
748    #[test]
749    fn test_round_trip_serialization() {
750        #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug, Default)]
751        #[reflect(Resource, SettingsGroup, Default)]
752        struct SingleFieldTupleStruct(u8);
753
754        #[derive(Reflect, PartialEq, Debug, Default)]
755        #[reflect(Default)]
756        struct NestedStruct {
757            a: u8,
758            b: u16,
759        }
760
761        #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug, Default)]
762        #[reflect(Resource, SettingsGroup, Default)]
763        struct MultiFieldTupleStruct(u8, NestedStruct);
764
765        #[derive(Resource, SettingsGroup, Reflect, Default)]
766        #[reflect(Resource, SettingsGroup, Default)]
767        struct NewTypeSingleTupleStruct(SingleFieldTupleStruct);
768
769        #[derive(Resource, SettingsGroup, Reflect, Default)]
770        #[reflect(Resource, SettingsGroup, Default)]
771        struct NewTypeMultiTupleStruct(SingleFieldTupleStruct, MultiFieldTupleStruct);
772
773        #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug, Default)]
774        #[reflect(Resource, SettingsGroup, Default)]
775        enum EnumUnitVariant {
776            #[default]
777            A,
778        }
779
780        #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
781        #[reflect(Resource, SettingsGroup, Default)]
782        enum EnumSingleTupleVariant {
783            A(u8),
784        }
785
786        impl Default for EnumSingleTupleVariant {
787            fn default() -> Self {
788                EnumSingleTupleVariant::A(0)
789            }
790        }
791
792        #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
793        #[reflect(Resource, SettingsGroup, Default)]
794        enum EnumMultiTupleVariant {
795            A(u16, u32),
796        }
797
798        impl Default for EnumMultiTupleVariant {
799            fn default() -> Self {
800                EnumMultiTupleVariant::A(0, 0)
801            }
802        }
803
804        #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
805        #[reflect(Resource, SettingsGroup, Default)]
806        enum EnumStructVariant {
807            A { x: u8, y: u16 },
808        }
809
810        impl Default for EnumStructVariant {
811            fn default() -> Self {
812                EnumStructVariant::A { x: 0, y: 0 }
813            }
814        }
815
816        #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
817        #[reflect(Resource, SettingsGroup, Default)]
818        enum EnumSingleNewTypeVariant {
819            A(SingleFieldTupleStruct),
820        }
821
822        impl Default for EnumSingleNewTypeVariant {
823            fn default() -> Self {
824                EnumSingleNewTypeVariant::A(SingleFieldTupleStruct(0))
825            }
826        }
827
828        #[derive(Resource, SettingsGroup, Reflect, PartialEq, Debug)]
829        #[reflect(Resource, SettingsGroup, Default)]
830        enum EnumMultiNewTypeVariant {
831            A(SingleFieldTupleStruct, MultiFieldTupleStruct),
832        }
833
834        impl Default for EnumMultiNewTypeVariant {
835            fn default() -> Self {
836                EnumMultiNewTypeVariant::A(
837                    SingleFieldTupleStruct(0),
838                    MultiFieldTupleStruct(0, NestedStruct { a: 0, b: 0 }),
839                )
840            }
841        }
842
843        let mut world = World::new();
844        let mut types = TypeRegistry::default();
845
846        types.register::<CounterSettings>();
847        types.register::<ExtraCounterSettings>();
848        types.register::<CounterRefreshRateSettings>();
849        types.register::<SingleFieldTupleStruct>();
850        types.register::<MultiFieldTupleStruct>();
851        types.register::<NewTypeSingleTupleStruct>();
852        types.register::<NewTypeMultiTupleStruct>();
853        types.register::<EnumUnitVariant>();
854        types.register::<EnumSingleTupleVariant>();
855        types.register::<EnumMultiTupleVariant>();
856        types.register::<EnumStructVariant>();
857        types.register::<EnumSingleNewTypeVariant>();
858        types.register::<EnumMultiNewTypeVariant>();
859
860        // Insert resources with specific values
861        world.insert_resource(CounterSettings { count: 123 });
862        world.insert_resource(ExtraCounterSettings { enabled: false });
863        world.insert_resource(CounterRefreshRateSettings::Fast);
864        world.insert_resource(SingleFieldTupleStruct(1));
865        world.insert_resource(MultiFieldTupleStruct(2, NestedStruct { a: 1, b: 2 }));
866        world.insert_resource(NewTypeSingleTupleStruct(SingleFieldTupleStruct(1)));
867        world.insert_resource(NewTypeMultiTupleStruct(
868            SingleFieldTupleStruct(1),
869            MultiFieldTupleStruct(2, NestedStruct { a: 1, b: 2 }),
870        ));
871        world.insert_resource(EnumUnitVariant::A);
872        world.insert_resource(EnumSingleTupleVariant::A(1));
873        world.insert_resource(EnumMultiTupleVariant::A(1, 2));
874        world.insert_resource(EnumStructVariant::A { x: 1, y: 2 });
875        world.insert_resource(EnumSingleNewTypeVariant::A(SingleFieldTupleStruct(1)));
876        world.insert_resource(EnumMultiNewTypeVariant::A(
877            SingleFieldTupleStruct(1),
878            MultiFieldTupleStruct(2, NestedStruct { a: 1, b: 2 }),
879        ));
880
881        // Build a manifest with both resource types
882        let manifest = SettingsFileManifest {
883            last_save: Tick::new(0),
884            resource_types: vec![
885                TypeId::of::<CounterSettings>(),
886                TypeId::of::<ExtraCounterSettings>(),
887                TypeId::of::<CounterRefreshRateSettings>(),
888                TypeId::of::<SingleFieldTupleStruct>(),
889                TypeId::of::<MultiFieldTupleStruct>(),
890                TypeId::of::<NewTypeSingleTupleStruct>(),
891                TypeId::of::<NewTypeMultiTupleStruct>(),
892                TypeId::of::<EnumUnitVariant>(),
893                TypeId::of::<EnumSingleTupleVariant>(),
894                TypeId::of::<EnumMultiTupleVariant>(),
895                TypeId::of::<EnumStructVariant>(),
896                TypeId::of::<EnumSingleNewTypeVariant>(),
897                TypeId::of::<EnumMultiNewTypeVariant>(),
898            ],
899        };
900
901        // Serialize to TOML
902        let table = resources_to_toml(&world, &types, &manifest);
903
904        // Create a new world and apply the TOML
905        let mut new_world = World::new();
906        apply_settings_to_world(&mut new_world, Some(&table), &manifest, &types);
907
908        // Verify resources were created with correct values
909        let counter = new_world.get_resource::<CounterSettings>().unwrap();
910        assert_eq!(counter.count, 123);
911
912        let extra = new_world.get_resource::<ExtraCounterSettings>().unwrap();
913        assert!(!extra.enabled);
914
915        let refresh_rate = new_world
916            .get_resource::<CounterRefreshRateSettings>()
917            .unwrap();
918        assert_eq!(*refresh_rate, CounterRefreshRateSettings::Fast);
919
920        let single_field_tuple_struct = new_world.get_resource::<SingleFieldTupleStruct>().unwrap();
921        assert_eq!(single_field_tuple_struct.0, 1);
922
923        let multi_field_tuple_struct = new_world.get_resource::<MultiFieldTupleStruct>().unwrap();
924        assert_eq!(multi_field_tuple_struct.0, 2);
925        assert_eq!(multi_field_tuple_struct.1.a, 1);
926        assert_eq!(multi_field_tuple_struct.1.b, 2);
927
928        let new_type_single_tuple_struct = new_world
929            .get_resource::<NewTypeSingleTupleStruct>()
930            .unwrap();
931        assert_eq!(new_type_single_tuple_struct.0 .0, 1);
932
933        let new_type_multi_tuple_struct =
934            new_world.get_resource::<NewTypeMultiTupleStruct>().unwrap();
935        assert_eq!(new_type_multi_tuple_struct.0 .0, 1);
936        assert_eq!(new_type_multi_tuple_struct.1 .0, 2);
937        assert_eq!(new_type_multi_tuple_struct.1 .1.a, 1);
938        assert_eq!(new_type_multi_tuple_struct.1 .1.b, 2);
939
940        let enum_unit_variant = new_world.get_resource::<EnumUnitVariant>().unwrap();
941        assert_eq!(*enum_unit_variant, EnumUnitVariant::A);
942
943        let enum_single_tuple_variant = new_world.get_resource::<EnumSingleTupleVariant>().unwrap();
944        assert_eq!(*enum_single_tuple_variant, EnumSingleTupleVariant::A(1));
945
946        let enum_multi_tuple_variant = new_world.get_resource::<EnumMultiTupleVariant>().unwrap();
947        assert_eq!(*enum_multi_tuple_variant, EnumMultiTupleVariant::A(1, 2));
948
949        let enum_struct_variant = new_world.get_resource::<EnumStructVariant>().unwrap();
950        assert_eq!(*enum_struct_variant, EnumStructVariant::A { x: 1, y: 2 });
951
952        let enum_single_new_type_variant = new_world
953            .get_resource::<EnumSingleNewTypeVariant>()
954            .unwrap();
955        assert_eq!(
956            *enum_single_new_type_variant,
957            EnumSingleNewTypeVariant::A(SingleFieldTupleStruct(1))
958        );
959
960        let enum_multi_new_type_variant =
961            new_world.get_resource::<EnumMultiNewTypeVariant>().unwrap();
962        assert_eq!(
963            *enum_multi_new_type_variant,
964            EnumMultiNewTypeVariant::A(
965                SingleFieldTupleStruct(1),
966                MultiFieldTupleStruct(2, NestedStruct { a: 1, b: 2 })
967            )
968        );
969    }
970
971    #[test]
972    fn test_round_trip_with_existing_resources() {
973        let mut world = World::new();
974        let mut types = TypeRegistry::default();
975        types.register::<CounterSettings>();
976        types.register::<CounterRefreshRateSettings>();
977
978        // Insert resource with initial values
979        world.insert_resource(CounterSettings { count: 100 });
980        world.insert_resource(CounterRefreshRateSettings::Fast);
981
982        let manifest = SettingsFileManifest {
983            last_save: Tick::new(0),
984            resource_types: vec![
985                TypeId::of::<CounterSettings>(),
986                TypeId::of::<CounterRefreshRateSettings>(),
987            ],
988        };
989
990        // Serialize
991        let table = resources_to_toml(&world, &types, &manifest);
992
993        // Modify the resource
994        world.resource_mut::<CounterSettings>().count = 999;
995        *world.resource_mut::<CounterRefreshRateSettings>() = CounterRefreshRateSettings::Slow;
996
997        // Apply TOML (should restore the original value)
998        apply_settings_to_world(&mut world, Some(&table), &manifest, &types);
999
1000        let counter = world.get_resource::<CounterSettings>().unwrap();
1001        assert_eq!(counter.count, 100);
1002        let refresh_rate = world.get_resource::<CounterRefreshRateSettings>().unwrap();
1003        assert_eq!(*refresh_rate, CounterRefreshRateSettings::Fast);
1004    }
1005
1006    #[test]
1007    fn test_partial_toml_preserves_missing_fields() {
1008        let mut world = World::new();
1009        let mut types = TypeRegistry::default();
1010        types.register::<CounterSettings>();
1011        types.register::<ExtraCounterSettings>();
1012        types.register::<CounterRefreshRateSettings>();
1013
1014        // Insert resources with specific values
1015        world.insert_resource(CounterSettings { count: 50 });
1016        world.insert_resource(ExtraCounterSettings { enabled: true });
1017        world.insert_resource(CounterRefreshRateSettings::Fast);
1018
1019        // Create a TOML table that only contains one field from one resource
1020        let mut table = toml::Table::new();
1021        let mut counter_section = toml::Table::new();
1022        counter_section.insert("count".to_string(), toml::Value::Integer(999));
1023        table.insert(
1024            "counter_settings".to_string(),
1025            toml::Value::Table(counter_section),
1026        );
1027        // Note: "enabled" field is missing from the TOML
1028
1029        let manifest = SettingsFileManifest {
1030            last_save: Tick::new(0),
1031            resource_types: vec![
1032                TypeId::of::<CounterSettings>(),
1033                TypeId::of::<ExtraCounterSettings>(),
1034                TypeId::of::<CounterRefreshRateSettings>(),
1035            ],
1036        };
1037
1038        // Apply the partial TOML
1039        apply_settings_to_world(&mut world, Some(&table), &manifest, &types);
1040
1041        // Verify count was updated
1042        let counter = world.get_resource::<CounterSettings>().unwrap();
1043        assert_eq!(counter.count, 999);
1044
1045        // Verify enabled was preserved (not overwritten with default false)
1046        let extra = world.get_resource::<ExtraCounterSettings>().unwrap();
1047        assert!(extra.enabled);
1048
1049        // Verify refresh_rate was preserved
1050        let refresh_rate = world.get_resource::<CounterRefreshRateSettings>().unwrap();
1051        assert_eq!(*refresh_rate, CounterRefreshRateSettings::Fast);
1052    }
1053}