1use std::{any::TypeId, marker::PhantomData, path::PathBuf};
6
7use bevy::{
8 app::{App, Plugin, PostUpdate, Startup},
9 ecs::{
10 component::Component,
11 schedule::{IntoScheduleConfigs, SystemSet},
12 system::{Commands, Query},
13 world::{CommandQueue, World},
14 },
15 log::warn,
16 prelude::Resource,
17 reflect::{
18 GetTypeRegistration, Reflect, TypePath, TypeRegistry,
19 serde::{TypedReflectDeserializer, TypedReflectSerializer},
20 },
21 tasks::{Task, block_on, futures_lite::future},
22};
23pub use bevy_simple_prefs_derive::*;
24use ron::ser::{PrettyConfig, to_string_pretty};
25use serde::de::DeserializeSeed;
26
27pub trait Prefs {
29 fn init(app: &mut App);
31 fn save(world: &mut World);
33 fn load(world: &mut World);
35}
36
37pub struct PrefsPlugin<T: Reflect + TypePath> {
59 pub path: PathBuf,
65 pub local_storage_key: String,
74 pub _phantom: PhantomData<T>,
76}
77impl<T: Reflect + TypePath> Default for PrefsPlugin<T> {
78 fn default() -> Self {
79 let package_name = T::crate_name().unwrap_or("bevy_simple");
80 let file_name = format!("{}_prefs.ron", package_name);
81
82 Self {
83 path: file_name.into(),
84 local_storage_key: format!("{package_name}::{}.ron", T::short_type_path()),
88 _phantom: Default::default(),
89 }
90 }
91}
92
93#[derive(Resource)]
95pub struct PrefsSettings<T> {
96 pub local_storage_key: String,
98 pub path: PathBuf,
100 pub _phantom: PhantomData<T>,
102}
103
104#[derive(Resource)]
106pub struct PrefsStatus<T> {
107 pub loaded: bool,
109 _phantom: PhantomData<T>,
110}
111
112impl<T> Default for PrefsStatus<T> {
113 fn default() -> Self {
114 Self {
115 loaded: false,
116 _phantom: Default::default(),
117 }
118 }
119}
120
121#[derive(Component)]
123pub struct LoadPrefsTask(pub Task<CommandQueue>);
124
125#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
127pub struct PrefsSaveSystems;
128
129#[derive(Resource, Default)]
130struct HandleTasksSystemAdded;
131
132impl<T: Prefs + Reflect + TypePath> Plugin for PrefsPlugin<T> {
133 fn build(&self, app: &mut bevy::prelude::App) {
134 app.insert_resource::<PrefsSettings<T>>(PrefsSettings {
135 path: self.path.clone(),
136 local_storage_key: self.local_storage_key.clone(),
137 _phantom: Default::default(),
138 });
139 app.init_resource::<PrefsStatus<T>>();
140
141 <T>::init(app);
142
143 if app
144 .world()
145 .get_resource::<HandleTasksSystemAdded>()
146 .is_none()
147 {
148 app.add_systems(PostUpdate, handle_tasks.before(PrefsSaveSystems));
149 app.init_resource::<HandleTasksSystemAdded>();
150 }
151
152 app.add_systems(PostUpdate, <T>::save.in_set(PrefsSaveSystems));
154 app.add_systems(Startup, <T>::load);
155 }
156}
157
158fn handle_tasks(mut commands: Commands, mut transform_tasks: Query<&mut LoadPrefsTask>) {
159 for mut task in &mut transform_tasks {
160 if let Some(mut commands_queue) = block_on(future::poll_once(&mut task.0)) {
161 bevy::log::debug!("Adding Resource update commands to queue");
162 commands.append(&mut commands_queue);
163 }
164 }
165}
166
167#[cfg(not(target_arch = "wasm32"))]
169pub fn load_str(path: &std::path::Path) -> Option<String> {
170 std::fs::read_to_string(path).ok()
171}
172
173#[cfg(target_arch = "wasm32")]
175pub fn load_str(local_storage_key: &str) -> Option<String> {
176 let Some(window) = web_sys::window() else {
177 warn!("Failed to load save file: no window.");
178 return None;
179 };
180
181 let Ok(Some(storage)) = window.local_storage() else {
182 warn!("Failed to load save file: no storage.");
183 return None;
184 };
185
186 let Ok(maybe_item) = storage.get_item(local_storage_key) else {
187 warn!("Failed to load save file: failed to get item.");
188 return None;
189 };
190
191 maybe_item
192}
193
194#[cfg(not(target_arch = "wasm32"))]
196pub fn save_str(path: &std::path::Path, data: &str) {
197 if let Err(e) = std::fs::write(path, data) {
198 warn!("Failed to store save file: {:?}", e);
199 }
200}
201
202#[cfg(target_arch = "wasm32")]
204pub fn save_str(local_storage_key: &str, data: &str) {
205 let Some(window) = web_sys::window() else {
206 warn!("Failed to store save file: no window.");
207 return;
208 };
209
210 let Ok(Some(storage)) = window.local_storage() else {
211 warn!("Failed to store save file: no storage.");
212 return;
213 };
214
215 if let Err(e) = storage.set_item(local_storage_key, data) {
216 warn!("Failed to store save file: {:?}", e);
217 }
218}
219
220pub fn deserialize<T: Reflect + GetTypeRegistration + Default>(
222 serialized: &str,
223) -> Result<T, ron::de::Error> {
224 let mut registry = TypeRegistry::new();
225 registry.register::<T>();
226 let registration = registry.get(TypeId::of::<T>()).unwrap();
227
228 let mut deserializer = ron::Deserializer::from_str(serialized).unwrap();
229
230 let de = TypedReflectDeserializer::new(registration, ®istry);
231 let dynamic_struct = de.deserialize(&mut deserializer)?;
232
233 let mut val = T::default();
234 val.apply(&*dynamic_struct);
235 Ok(val)
236}
237
238pub fn serialize<T: Reflect + GetTypeRegistration>(to_save: &T) -> Result<String, ron::Error> {
240 let mut registry = TypeRegistry::new();
241 registry.register::<T>();
242
243 let config = PrettyConfig::default();
244 let reflect_serializer = TypedReflectSerializer::new(to_save, ®istry);
245 to_string_pretty(&reflect_serializer, config)
246}