bevy_simple_prefs/
lib.rs

1//! Bevy Simple Prefs
2//!
3//! A small Bevy plugin for persisting multiple `Resource`s to a single file.
4
5use 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
27/// A trait to be implemented by `bevy_simple_prefs_derive`.
28pub trait Prefs {
29    /// Runs when `PrefsPlugin` is built and initializes individual preference `Resource`s with default values.
30    fn init(app: &mut App);
31    /// Runs when individual preferences `Resources` are changed and persists preferences.
32    fn save(world: &mut World);
33    /// Loads preferences and updates individual preference `Resources`.
34    fn load(world: &mut World);
35}
36
37/// The Bevy plugin responsible for persisting `T`.
38///
39/// ```rust
40/// use bevy::prelude::*;
41/// use bevy_simple_prefs::{Prefs, PrefsPlugin};
42///
43/// #[derive(Prefs, Reflect, Default)]
44/// struct ExamplePrefs {
45///     difficulty: Difficulty,
46/// }
47///
48/// #[derive(Resource, Reflect, Clone, Eq, PartialEq, Debug, Default)]
49/// enum Difficulty {
50///     Easy,
51///     #[default]
52///     Normal,
53///     Hard,
54/// }
55///
56/// App::new().add_plugins(PrefsPlugin::<ExamplePrefs>::default());
57/// ```
58pub struct PrefsPlugin<T: Reflect + TypePath> {
59    /// Path to the file where the preferences will be stored.
60    ///
61    /// This value is not used in Wasm builds.
62    ///
63    /// Defaults to `(crate name of T)_prefs.ron` in the current working directory.
64    pub path: PathBuf,
65    /// String to use for the key when storing preferences in localStorage on
66    /// Wasm builds.
67    ///
68    /// This value should be unique to your app to avoid collisions with other
69    /// apps on the same web server. On itch.io, for example, many other games
70    /// will be using the same storage area.
71    ///
72    /// Defaults to `(crate name of T)::(type name of T).ron`.
73    pub local_storage_key: String,
74    /// PhantomData
75    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            // For Wasm, we want to provide a unique name for a project by default
85            // to avoid collisions when doing local development or deploying multiple
86            // apps to the same web server.
87            local_storage_key: format!("{package_name}::{}.ron", T::short_type_path()),
88            _phantom: Default::default(),
89        }
90    }
91}
92
93/// Settings for [`PrefsPlugin`].
94#[derive(Resource)]
95pub struct PrefsSettings<T> {
96    /// See [`PrefsPlugin::local_storage_key`].
97    pub local_storage_key: String,
98    /// See [`PrefsPlugin::path`].
99    pub path: PathBuf,
100    /// PhantomData
101    pub _phantom: PhantomData<T>,
102}
103
104/// Current status of the [`PrefsPlugin`].
105#[derive(Resource)]
106pub struct PrefsStatus<T> {
107    /// `true` if the preferences have been loaded
108    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/// A component that holds the task responsible for updating individual preference `Resource`s after they have been loaded.
122#[derive(Component)]
123pub struct LoadPrefsTask(pub Task<CommandQueue>);
124
125/// A system set containing the system that initializes a save task.
126#[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        // `save` checks load status and needs to run in the same frame after `handle_tasks`.
153        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/// Loads preferences from persisted data.
168#[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/// Loads preferences from persisted data.
174#[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/// Persists preferences.
195#[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/// Persists preferences.
203#[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
220/// Deserializes preferences
221pub 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, &registry);
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
238/// Serialize preferences
239pub 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, &registry);
245    to_string_pretty(&reflect_serializer, config)
246}