paiagram/rw_data/
saveload.rs

1use 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// output a fixed autosave location, platform dependent
9#[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
26/// The function to trigger the save, with filters built in.
27fn 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// TODO: distinguish between different autosave slots
59// TODO: old autosave cleanup and manual saves
60#[cfg(target_arch = "wasm32")]
61pub fn observe_autosave(save_reader: On<Saved>, registry: Res<AppTypeRegistry>) {
62    // serialize the scene to a string
63    let registry = registry.read();
64    let result = match save_reader.scene.serialize(&registry) {
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    // store in IndexedDB as blob
80    // TODO: handle errors properly
81    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: &registry,
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}