Skip to main content

cloudiful_bevy_settings/
systems.rs

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}