1#![warn(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use std::fs;
5use std::io::{self, BufRead};
6use std::path::Path;
7use std::{fs::File, io::BufWriter};
8
9use bevy::prelude::*;
10use bevy::window::WindowClosing;
11use serialization::{deserialize_transform, serialize_transform};
12
13mod serialization;
14
15#[derive(Component)]
26#[require(Transform)]
27pub struct WhereWasI {
28 name: String,
29}
30
31impl WhereWasI {
32 pub fn from_name(name: &str) -> Self {
34 Self { name: name.into() }
35 }
36
37 pub fn camera() -> Self {
46 WhereWasI::from_name("camera")
47 }
48}
49
50#[derive(Resource)]
52struct WhereWasIConfig {
53 directory: String,
54}
55
56pub struct WhereWasIPlugin {
59 pub directory: String,
61}
62
63impl Default for WhereWasIPlugin {
64 fn default() -> Self {
65 Self {
66 directory: "./assets/saves".into(),
67 }
68 }
69}
70
71impl Plugin for WhereWasIPlugin {
72 fn build(&self, app: &mut App) {
73 app.insert_resource(WhereWasIConfig {
74 directory: self.directory.clone(),
75 })
76 .add_systems(Update, save_state)
77 .add_systems(PostStartup, load_state);
78 }
79}
80
81fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
83where
84 P: AsRef<Path>,
85{
86 let file = File::open(filename)?;
87 Ok(io::BufReader::new(file).lines())
88}
89
90fn load_state(mut to_save: Query<(&WhereWasI, &mut Transform)>, config: Res<WhereWasIConfig>) {
92 let mut initialized = 0;
93
94 for (where_was_i, mut transform) in to_save.iter_mut() {
95 let (directory, filename) = (&config.directory, &where_was_i.name);
96 let filepath = format!("{directory}/{filename}.state");
97
98 if let Ok(contents) = read_lines(filepath) {
99 match deserialize_transform(contents) {
100 Ok(new) => {
101 *transform = new;
102 initialized += 1;
103 }
104 Err(err) => {
105 error!("Could not deserialize transform: {}", err.message);
106 }
107 }
108 }
109 }
110
111 info!("Initialized {} transform(s)", initialized);
112}
113
114fn save_state(
119 mut events: MessageReader<WindowClosing>,
120 to_save: Query<(&WhereWasI, &Transform)>,
121 config: Res<WhereWasIConfig>,
122) {
123 let directory = &config.directory;
124 let mut saved_files = 0;
125
126 if events.read().next().is_some() {
127 for (where_was_i, transform) in to_save.iter() {
128 let filename = where_was_i.name.clone();
129
130 if let Ok(false) = fs::exists(directory) {
131 fs::create_dir_all(directory).expect("Could not create directory");
132 }
133
134 let mut writer = BufWriter::new(
135 File::create(format!("{directory}/{filename}.state"))
136 .expect("Error occurred while opening file"),
137 );
138
139 #[cfg(not(target_arch = "wasm32"))]
140 serialize_transform(&mut writer, transform)
141 .expect("Error occurred while writing to disk");
142
143 saved_files += 1;
144 }
145 info!("Saved {} transforms to: {}", saved_files, directory);
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 const TRANSFORM: Transform = Transform {
154 translation: Vec3::new(4.0, 3.5, -2.0),
155 rotation: Quat::from_xyzw(-0.1, 0.7, 0.4, 0.6),
156 scale: Vec3::new(12.6, -1.0, 2.4),
157 };
158 const SAVE_STATE_FILE: &str = "assets/tests/system_save_test.state";
159
160 fn setup_camera_with_transform(mut commands: Commands<'_, '_>) {
161 commands.spawn((WhereWasI::from_name("system_save_test"), TRANSFORM));
162 }
163
164 fn setup_camera_without_transform(mut commands: Commands<'_, '_>) {
165 commands.spawn((Camera::default(), WhereWasI::camera()));
166 }
167
168 #[test]
169 fn test_save() {
170 let mut app = App::new();
171
172 if let Ok(true) = fs::exists(SAVE_STATE_FILE) {
173 fs::remove_file("assets/tests/system_save_test.state").unwrap();
174 }
175 assert!(!fs::exists(SAVE_STATE_FILE).unwrap());
176
177 app.insert_resource(WhereWasIConfig {
178 directory: "assets/tests".into(),
179 });
180 app.add_message::<WindowClosing>();
181 app.add_systems(Startup, setup_camera_with_transform);
182 app.add_systems(Update, save_state);
183
184 app.world_mut()
186 .resource_mut::<Messages<WindowClosing>>()
187 .write(WindowClosing {
188 window: Entity::from_raw_u32(322).unwrap(),
189 });
190
191 app.update();
192
193 let lines = read_lines("assets/tests/system_save_test.state").unwrap();
194 assert_eq!(deserialize_transform(lines).unwrap(), TRANSFORM);
195
196 fs::remove_file("assets/tests/system_save_test.state").unwrap();
197 }
198
199 #[test]
200 fn test_load() {
201 let mut app = App::new();
202
203 app.insert_resource(WhereWasIConfig {
204 directory: "assets/tests".into(),
205 });
206 app.add_systems(Startup, setup_camera_without_transform);
207 app.add_systems(Update, load_state);
208
209 app.update();
210
211 let result = app
212 .world_mut()
213 .query::<&Transform>()
214 .single(app.world())
215 .expect("`load_state` should have added a Transform to the world");
216
217 const TRANSFORM: Transform = Transform {
218 translation: Vec3::new(10.000002, 10.0, 10.0),
219 rotation: Quat::from_xyzw(-0.27984813, 0.36470526, 0.11591691, 0.88047624),
220 scale: Vec3::new(1.0, 1.0, 1.0),
221 };
222 assert_eq!(*result, TRANSFORM);
223 }
224}