1extern crate directories;
2
3use std::{fmt::Debug, marker::PhantomData, path::PathBuf};
4
5use directories::ProjectDirs;
6
7use bevy_app::{App, Plugin, Update};
8use bevy_ecs::{
9 prelude::{Event, EventReader, Resource},
10 system::Res,
11};
12use bevy_log::prelude::debug;
13
14pub extern crate serde;
15pub use serde::{Deserialize, Serialize};
16
17pub trait Settingable: Resource + Clone + Serialize + Default + for<'a> Deserialize<'a> {}
19
20impl<S> Settingable for S where S: Resource + Clone + Serialize + Default + for<'a> Deserialize<'a> {}
22
23#[derive(Event)]
26pub struct PersistSettings;
27
28#[derive(Event, Default)]
31pub struct PersistSetting<S: Settingable>(PhantomData<S>);
32
33pub struct SettingsPlugin<S: Settingable> {
34 domain: String,
35 company: String,
36 project: String,
37 settings: PhantomData<S>,
38}
39
40#[derive(Resource, Debug)]
41pub struct SettingsConfig<S: Settingable> {
42 directory: PathBuf,
43 path: PathBuf,
44 settings: PhantomData<S>,
45}
46
47impl<S: Settingable> SettingsPlugin<S> {
48 pub fn new(company: impl Into<String>, project: impl Into<String>) -> Self {
49 Self {
50 domain: "com".into(),
51 company: company.into(),
52 project: project.into(),
53 settings: PhantomData::<S>,
54 }
55 }
56
57 pub fn resource(&self) -> S {
58 self.load().unwrap_or_default()
59 }
60
61 fn load(&self) -> Option<S> {
62 let path = self.path();
63 if !path.exists() {
64 return None;
65 }
66 let settings_string = std::fs::read_to_string(path).ok()?;
67 toml::from_str(&settings_string).ok()
68 }
69
70 fn path(&self) -> PathBuf {
71 ProjectDirs::from(&self.domain, &self.company, &self.project)
72 .expect("Couldn't build settings path")
73 .config_dir()
74 .join(format!("{}.toml", self.project))
75 }
76
77 fn settings_directory(&self) -> PathBuf {
78 ProjectDirs::from(&self.domain, &self.company, &self.project)
79 .expect("Couldn't find a folder to store the settings")
80 .config_dir()
81 .to_path_buf()
82 }
83
84 fn persist(
85 settings: Res<S>,
86 config: Res<SettingsConfig<S>>,
87 reader_single: EventReader<PersistSetting<S>>,
88 reader_all: EventReader<PersistSettings>,
89 ) {
90 if !reader_single.is_empty() || !reader_all.is_empty() {
91 match (!reader_single.is_empty(), !reader_all.is_empty()) {
92 (true, true) => debug!(
93 "Persist called on {} to path {:?} and also through the global event",
94 std::any::type_name::<S>(),
95 config.path
96 ),
97 (true, false) => debug!(
98 "Persist called through global event to path {:?}",
99 config.path
100 ),
101 (false, true) => debug!(
102 "Persist called on {} to path {:?}",
103 std::any::type_name::<S>(),
104 config.path
105 ),
106 (false, false) => {}
107 }
108
109 std::fs::create_dir_all(config.directory.clone())
110 .expect("Couldn't create the folders for the settings file");
111 std::fs::write(
112 config.path.clone(),
113 toml::to_string(&*settings).expect("Couldn't serialize the settings to toml"),
114 )
115 .expect("couldn't persist the settings while trying to write the string to disk");
116 }
117 }
118}
119
120impl<S: Settingable> Plugin for SettingsPlugin<S> {
121 fn build(&self, app: &mut App) {
122 app.insert_resource(self.resource())
123 .insert_resource(SettingsConfig {
124 directory: self.settings_directory(),
125 path: self.path(),
126 settings: PhantomData::<S>,
127 })
128 .add_event::<PersistSettings>()
129 .add_event::<PersistSetting<S>>()
130 .add_systems(Update, SettingsPlugin::<S>::persist);
131 }
132}
133
134#[cfg(test)]
138mod tests {
139 use super::{PersistSettings, SettingsPlugin};
140 use bevy::prelude::*;
141 use pretty_assertions::{assert_eq, assert_ne};
142
143 use crate::PersistSetting;
144 pub use crate::{Deserialize, Serialize};
145
146 #[derive(Resource, Default, Serialize, Deserialize, Clone)]
147 struct TestSetting1 {
148 test: u32,
149 }
150
151 #[derive(Resource, Default, Serialize, Deserialize, Clone)]
152 struct TestSetting2 {
153 test: u32,
154 }
155
156 #[test]
157 #[serial_test::serial]
158 fn it_should_store_multiple_settings() {
159 let mut app1 = App::new();
160 let u32_1: u32 = rand::random::<u32>();
161 let u32_2: u32 = rand::random::<u32>();
162 app1.add_plugins(SettingsPlugin::<TestSetting1>::new(
163 "Bevy Settings Test Corp",
164 "Some Game File 1",
165 ));
166 app1.add_plugins(SettingsPlugin::<TestSetting2>::new(
167 "Bevy Settings Test Corp",
168 "Some Game File 2",
169 ));
170 app1.add_systems(
171 Update,
172 move |mut writer: EventWriter<PersistSettings>,
173 mut test_setting_1: ResMut<TestSetting1>,
174 mut test_setting_2: ResMut<TestSetting2>| {
175 *test_setting_1 = TestSetting1 { test: u32_1 };
176 *test_setting_2 = TestSetting2 { test: u32_2 };
177 writer.write(PersistSettings);
178 },
179 );
180 app1.update(); app1.update(); let mut app2 = App::new();
184 app2.add_plugins(SettingsPlugin::<TestSetting1>::new(
185 "Bevy Settings Test Corp",
186 "Some Game File 1",
187 ));
188 app2.add_plugins(SettingsPlugin::<TestSetting2>::new(
189 "Bevy Settings Test Corp",
190 "Some Game File 2",
191 ));
192 app2.update();
193 let world2 = app2.world();
194 let test_setting_1 = world2.resource::<TestSetting1>();
195 assert_eq!(test_setting_1.test, u32_1, "Failed to verify TestSetting1");
196 let test_setting_2 = world2.resource::<TestSetting2>();
197 assert_eq!(test_setting_2.test, u32_2, "Failed to verify TestSetting2");
198 }
199
200 #[test]
201 #[serial_test::serial]
202 fn it_should_store_singular_settings() {
203 let mut app1 = App::new();
204 let u32_1: u32 = rand::random::<u32>();
205 let u32_2: u32 = rand::random::<u32>();
206 app1.add_plugins(SettingsPlugin::<TestSetting1>::new(
207 "Bevy Settings Test Corp",
208 "Some Game File 1",
209 ));
210 app1.add_plugins(SettingsPlugin::<TestSetting2>::new(
211 "Bevy Settings Test Corp",
212 "Some Game File 2",
213 ));
214 app1.add_systems(
215 Update,
216 move |mut writer: EventWriter<PersistSetting<TestSetting1>>,
217 mut test_setting_1: ResMut<TestSetting1>,
218 mut test_setting_2: ResMut<TestSetting2>| {
219 *test_setting_1 = TestSetting1 { test: u32_1 };
220 *test_setting_2 = TestSetting2 { test: u32_2 };
221 writer.write(PersistSetting::default());
222 },
223 );
224 app1.update(); app1.update(); let mut app2 = App::new();
228 app2.add_plugins(SettingsPlugin::<TestSetting1>::new(
229 "Bevy Settings Test Corp",
230 "Some Game File 1",
231 ));
232 app2.add_plugins(SettingsPlugin::<TestSetting2>::new(
233 "Bevy Settings Test Corp",
234 "Some Game File 2",
235 ));
236 app2.update();
237 let world2 = app2.world();
238 let test_setting_1 = world2.resource::<TestSetting1>();
239 assert_eq!(test_setting_1.test, u32_1, "Failed to verify TestSetting1");
240 let test_setting_2 = world2.resource::<TestSetting2>();
241 assert_ne!(test_setting_2.test, u32_2, "Failed to verify TestSetting2");
242 }
243}