paiagram/rw_data/
saveload.rs1use bevy::prelude::*;
2#[cfg(target_arch = "wasm32")]
3use moonshine_core::load::LoadInput;
4use moonshine_core::save::prelude::*;
5#[cfg(not(target_arch = "wasm32"))]
6use std::path::PathBuf;
7
8#[cfg(not(target_arch = "wasm32"))]
10pub fn storage_file_location(app_id: &str, name: &str) -> Option<PathBuf> {
11 eframe::storage_dir(app_id)?.join(name).into()
12}
13
14const APP_ID: &str = "paiagramdrawer";
15const AUTOSAVE_FILE_NAME: &str = "autosave.paiagram";
16#[cfg(not(target_arch = "wasm32"))]
17fn autosave_file_location() -> Result<PathBuf, std::io::Error> {
18 storage_file_location(APP_ID, AUTOSAVE_FILE_NAME).ok_or_else(|| {
19 std::io::Error::new(
20 std::io::ErrorKind::NotFound,
21 "Autosave file location not found or could not be created",
22 )
23 })
24}
25
26fn trigger_save(commands: &mut Commands, loader: SaveWorld) {
28 let event = loader
29 .include_resource::<crate::settings::ApplicationSettings>()
30 .include_resource::<crate::interface::UiState>()
31 .include_resource::<crate::graph::Graph>();
32 commands.trigger_save(event)
33}
34
35#[cfg(not(target_arch = "wasm32"))]
36pub fn autosave(mut commands: Commands) {
37 let path = match autosave_file_location() {
38 Ok(path) => path,
39 Err(e) => {
40 error!("Could not determine autosave file location, {:?}", e);
41 return;
42 }
43 };
44 trigger_save(&mut commands, SaveWorld::default_into_file(path.clone()));
45 info!("Triggered autosave to {:?}", path);
46}
47
48#[cfg(target_arch = "wasm32")]
49pub fn autosave(mut commands: Commands) {
50 use moonshine_core::save::DefaultSaveFilter;
51 trigger_save(
52 &mut commands,
53 SaveWorld::<DefaultSaveFilter>::new(SaveOutput::Drop),
54 );
55 info!("Triggered autosave.");
56}
57
58#[cfg(target_arch = "wasm32")]
61pub fn observe_autosave(save_reader: On<Saved>, registry: Res<AppTypeRegistry>) {
62 let registry = registry.read();
64 let result = match save_reader.scene.serialize(®istry) {
65 Ok(data) => data,
66 Err(e) => {
67 error!("Could not serialize scene for autosave: {:?}", e);
68 return;
69 }
70 };
71 let size = result.len();
72 let compressed: Vec<u8> = lz4_flex::block::compress_prepend_size(result.as_bytes());
73 info!(
74 "Autosave serialized ({} bytes) and compressed to {} bytes ({:2}% of original size)",
75 size,
76 compressed.len(),
77 (compressed.len() as f32 / size as f32) * 100.0
78 );
79 let task = async move {
82 use idb::DatabaseEvent;
83 use js_sys::Uint8Array;
84 let factory = idb::Factory::new()?;
85 let mut open_request = factory.open(APP_ID, Some(1))?;
86
87 open_request.on_upgrade_needed(|event| {
88 let Ok(db) = event.database() else {
89 error!("Failed to get IndexedDB database during upgrade");
90 return;
91 };
92 if !db.store_names().contains(&AUTOSAVE_FILE_NAME.to_string()) {
93 if let Err(e) =
94 db.create_object_store(AUTOSAVE_FILE_NAME, idb::ObjectStoreParams::new())
95 {
96 error!("Failed to create object store for autosave: {:?}", e);
97 }
98 }
99 });
100
101 let db = open_request.await?;
102
103 let tx = db.transaction(&[AUTOSAVE_FILE_NAME], idb::TransactionMode::ReadWrite)?;
104 let store = tx.object_store(AUTOSAVE_FILE_NAME)?;
105 store.put(
106 &Uint8Array::from(compressed.as_slice()).into(),
107 Some(&wasm_bindgen::JsValue::from_str("latest")),
108 )?;
109 tx.commit()?.await?;
110 info!("Autosave successfully stored in IndexedDB");
111 Ok::<(), idb::Error>(())
112 };
113 bevy::tasks::IoTaskPool::get().spawn(task).detach();
114}
115
116#[cfg(not(target_arch = "wasm32"))]
117pub fn load_autosave(mut commands: Commands) {
118 match autosave_file_location() {
119 Ok(path) => load_save(&mut commands, path),
120 Err(e) => {
121 error!("Could not determine autosave file location: {:?}", e);
122 return;
123 }
124 };
125 info!("Triggered loading of autosave file");
126}
127
128#[cfg(not(target_arch = "wasm32"))]
129pub fn load_save(commands: &mut Commands, path: PathBuf) {
130 commands.trigger_load(LoadWorld::default_from_file(path.clone()));
131 info!("Triggered loading from {:?}", path);
132}
133
134#[cfg(target_arch = "wasm32")]
135#[derive(Resource)]
136pub struct AutosaveLoadTask {
137 task: bevy::tasks::Task<Option<Vec<u8>>>,
138}
139
140#[cfg(target_arch = "wasm32")]
141pub fn load_autosave(mut commands: Commands, task: Option<Res<AutosaveLoadTask>>) {
142 if task.is_some() {
143 info!("Autosave load already in progress");
144 return;
145 }
146
147 let task = async move {
148 use idb::DatabaseEvent;
149 let factory = idb::Factory::new()?;
150 let mut open_request = factory.open(APP_ID, Some(1))?;
151
152 open_request.on_upgrade_needed(|event| {
153 let Ok(db) = event.database() else {
154 error!("Failed to get IndexedDB database during upgrade");
155 return;
156 };
157 if !db.store_names().contains(&AUTOSAVE_FILE_NAME.to_string()) {
158 if let Err(e) =
159 db.create_object_store(AUTOSAVE_FILE_NAME, idb::ObjectStoreParams::new())
160 {
161 error!("Failed to create object store for autosave: {:?}", e);
162 }
163 }
164 });
165
166 let db = open_request.await?;
167 let tx = db.transaction(&[AUTOSAVE_FILE_NAME], idb::TransactionMode::ReadOnly)?;
168 let store = tx.object_store(AUTOSAVE_FILE_NAME)?;
169 let value = store
170 .get(wasm_bindgen::JsValue::from_str("latest"))?
171 .await?;
172 tx.await?;
173
174 let bytes = value.map(|value| js_sys::Uint8Array::new(&value).to_vec());
175 Ok::<Option<Vec<u8>>, idb::Error>(bytes)
176 };
177
178 let task = bevy::tasks::IoTaskPool::get().spawn(async move {
179 match task.await {
180 Ok(data) => data,
181 Err(e) => {
182 error!("Failed to load autosave data from IndexedDB: {:?}", e);
183 None
184 }
185 }
186 });
187 commands.insert_resource(AutosaveLoadTask { task });
188 info!("Triggered loading of autosave from IndexedDB");
189}
190
191#[cfg(target_arch = "wasm32")]
192pub fn consume_autosave_load_task(
193 mut commands: Commands,
194 registry: Res<AppTypeRegistry>,
195 mut task: ResMut<AutosaveLoadTask>,
196) {
197 let Some(result) =
198 bevy::tasks::block_on(bevy::tasks::futures_lite::future::poll_once(&mut task.task))
199 else {
200 return;
201 };
202
203 commands.remove_resource::<AutosaveLoadTask>();
204
205 let Some(bytes) = result else {
206 info!("No autosave data found in IndexedDB");
207 return;
208 };
209
210 let decompressed = match lz4_flex::block::decompress_size_prepended(&bytes) {
211 Ok(data) => data,
212 Err(err) => {
213 error!("Could not decompress autosave data: {:?}", err);
214 return;
215 }
216 };
217 let scene_str = match String::from_utf8(decompressed) {
218 Ok(s) => s,
219 Err(err) => {
220 error!("Could not convert autosave data to UTF-8 string: {:?}", err);
221 return;
222 }
223 };
224 let registry = registry.read();
225 use bevy::scene::serde::SceneDeserializer;
226 use ron::de::Deserializer;
227 use serde::de::DeserializeSeed;
228
229 let mut de = match Deserializer::from_str(&scene_str) {
230 Ok(de) => de,
231 Err(err) => {
232 error!(
233 "Could not create RON deserializer for autosave data: {:?}",
234 err
235 );
236 return;
237 }
238 };
239
240 let scene = match (SceneDeserializer {
241 type_registry: ®istry,
242 }
243 .deserialize(&mut de))
244 {
245 Ok(scene) => scene,
246 Err(err) => {
247 error!("Could not deserialize autosave scene: {:?}", err);
248 return;
249 }
250 };
251
252 let mut loader = LoadWorld::default_from_file("");
253 loader.input = LoadInput::Scene(scene);
254 commands.trigger_load(loader);
255 info!("Triggered loading autosave from DynamicScene");
256}