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