1use crate::{RequestedSettingAction, SettingActionHandler};
2use bevy::{
3 app::{App, Update},
4 ecs::{
5 message::{Message, MessageReader},
6 schedule::{IntoScheduleConfigs, SystemSet},
7 system::{ResMut, ScheduleSystem},
8 },
9 prelude::Resource,
10};
11use bevy_persistent::Persistent;
12use serde::{Serialize, de::DeserializeOwned};
13
14#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum SettingSystemSet {
16 ApplyActions,
17 SyncUi,
18}
19
20pub fn apply_setting_action<T, Action, AppAction>(settings: &mut Persistent<T>, action: AppAction)
21where
22 T: Resource + Serialize + DeserializeOwned + SettingActionHandler<Action>,
23 Action: Copy + TryFrom<AppAction>,
24 AppAction: Copy,
25{
26 let Ok(action) = Action::try_from(action) else {
27 return;
28 };
29
30 if !SettingActionHandler::can_apply(settings.get(), action) {
31 return;
32 }
33
34 settings
35 .update(|setting: &mut T| {
36 SettingActionHandler::apply(setting, action);
37 })
38 .unwrap();
39}
40
41pub fn change_setting<T, Action, Requested, AppAction>(
42 mut settings: ResMut<Persistent<T>>,
43 mut action_messages: MessageReader<Requested>,
44) where
45 T: Resource + Serialize + DeserializeOwned + SettingActionHandler<Action>,
46 Action: Copy + TryFrom<AppAction>,
47 Requested: Message + RequestedSettingAction<AppAction>,
48 AppAction: Copy,
49{
50 for message in action_messages.read() {
51 apply_setting_action(&mut settings, message.action());
52 }
53}
54
55pub fn register_setting_systems<ChangeMarker, SyncMarker>(
56 app: &mut App,
57 change_systems: impl IntoScheduleConfigs<ScheduleSystem, ChangeMarker>,
58 sync_systems: impl IntoScheduleConfigs<ScheduleSystem, SyncMarker>,
59) -> &mut App {
60 app.configure_sets(
61 Update,
62 (
63 SettingSystemSet::ApplyActions,
64 SettingSystemSet::SyncUi.after(SettingSystemSet::ApplyActions),
65 ),
66 )
67 .add_systems(
68 Update,
69 change_systems.in_set(SettingSystemSet::ApplyActions),
70 )
71 .add_systems(Update, sync_systems.in_set(SettingSystemSet::SyncUi))
72}
73
74#[cfg(test)]
75mod tests {
76 use super::{apply_setting_action, change_setting};
77 use crate::{RequestedSettingAction, SettingActionHandler};
78 use bevy::{app::App, ecs::message::Message, prelude::*};
79 use bevy_persistent::{Persistent, StorageFormat};
80 use serde::{Deserialize, Serialize};
81 use std::{
82 path::PathBuf,
83 sync::atomic::{AtomicUsize, Ordering},
84 };
85
86 static TEST_FILE_COUNTER: AtomicUsize = AtomicUsize::new(0);
87
88 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
89 enum TestAction {
90 Increase,
91 Disabled,
92 }
93
94 #[derive(Resource, Debug, Default, Serialize, Deserialize)]
95 struct TestSettings {
96 value: u32,
97 applied: u32,
98 }
99
100 impl SettingActionHandler<TestAction> for TestSettings {
101 fn can_apply(&self, action: TestAction) -> bool {
102 match action {
103 TestAction::Increase => self.value == 0,
104 TestAction::Disabled => false,
105 }
106 }
107
108 fn apply(&mut self, action: TestAction) {
109 if let TestAction::Increase = action {
110 self.value += 1;
111 self.applied += 1;
112 }
113 }
114 }
115
116 #[derive(Message, Debug, Clone, Copy)]
117 struct TestRequested {
118 action: TestAction,
119 }
120
121 impl RequestedSettingAction<TestAction> for TestRequested {
122 fn action(&self) -> TestAction {
123 self.action
124 }
125 }
126
127 #[test]
128 fn apply_setting_action_respects_can_apply_and_updates_once() {
129 let mut settings = test_persistent(TestSettings::default());
130
131 apply_setting_action::<_, TestAction, TestAction>(&mut settings, TestAction::Disabled);
132 assert_eq!(settings.get().value, 0);
133 assert_eq!(settings.get().applied, 0);
134
135 apply_setting_action::<_, TestAction, TestAction>(&mut settings, TestAction::Increase);
136 assert_eq!(settings.get().value, 1);
137 assert_eq!(settings.get().applied, 1);
138
139 apply_setting_action::<_, TestAction, TestAction>(&mut settings, TestAction::Increase);
140 assert_eq!(settings.get().value, 1);
141 assert_eq!(settings.get().applied, 1);
142 }
143
144 #[test]
145 fn change_setting_reads_action_from_requested_message() {
146 let mut app = App::new();
147 app.add_message::<TestRequested>();
148 app.insert_resource(test_persistent(TestSettings::default()));
149 app.add_systems(
150 Update,
151 change_setting::<TestSettings, TestAction, TestRequested, TestAction>,
152 );
153
154 app.world_mut().write_message(TestRequested {
155 action: TestAction::Increase,
156 });
157 app.update();
158
159 let settings = app.world().resource::<Persistent<TestSettings>>();
160 assert_eq!(settings.get().value, 1);
161 assert_eq!(settings.get().applied, 1);
162 }
163
164 fn test_persistent(default: TestSettings) -> Persistent<TestSettings> {
165 let path = test_file_path("settings");
166
167 Persistent::<TestSettings>::builder()
168 .name("test settings")
169 .format(StorageFormat::Toml)
170 .path(&path)
171 .default(default)
172 .build()
173 .unwrap_or_else(|err| panic!("failed to build persistent settings at {path:?}: {err}"))
174 }
175
176 fn test_file_path(name: &str) -> PathBuf {
177 let next_id = TEST_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
178 std::env::temp_dir().join(format!(
179 "cloudiful-bevy-settings-{name}-{}-{next_id}.toml",
180 std::process::id()
181 ))
182 }
183}