// Savegames: JSON with format tag and version, containing the game state, the camera // and the player's settings (settings are always applied when a savegame is loaded). use crate::game::Game; use crate::render3d::Cam; use crate::settings::Settings; use serde::{Deserialize, Serialize}; pub const FORMAT: &str = "dungeoncraft-save"; pub const VERSION: u32 = 2; // 2: variable map size, several portals, packed tiles pub const AUTOSAVE_KEY: &str = "dc.autosave"; #[derive(Serialize)] struct SaveOut<'a> { format: &'a str, version: u32, game: &'a Game, camera: &'a Cam, settings: &'a Settings, } #[derive(Deserialize)] struct SaveIn { format: String, version: u32, game: Game, camera: Option, settings: Option, } pub struct Loaded { pub game: Game, pub camera: Option, pub settings: Option, } pub fn to_json(game: &Game, camera: &Cam, settings: &Settings) -> String { serde_json::to_string(&SaveOut { format: FORMAT, version: VERSION, game, camera, settings }).unwrap_or_default() } // Err contains a language key describing the problem pub fn from_bytes(data: &[u8]) -> Result { let s: SaveIn = serde_json::from_slice(data).map_err(|_| "save.err_invalid")?; if s.format != FORMAT { return Err("save.err_invalid"); } if s.version > VERSION { return Err("save.err_version"); } let mut game = s.game; game.map.activate(); game.recount(); Ok(Loaded { game, camera: s.camera, settings: s.settings }) } #[cfg(test)] mod tests { use super::*; #[test] fn save_roundtrip() { let mut g = Game::new(42, 96, 128); let (cx, cy) = g.map.core; g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true); for _ in 0..3000 { g.update(0.05); } let cam = Cam::new(10.0, 20.0); let mut settings = Settings::default(); settings.language = "de".into(); let json = to_json(&g, &cam, &settings); println!("savegame size: {} KB", json.len() / 1024); let l = from_bytes(json.as_bytes()).expect("load"); assert_eq!(l.game.time, g.time); assert_eq!(l.game.ents.len(), g.ents.len()); assert_eq!(l.game.gold, g.gold); assert!(l.game.map.t.iter().zip(g.map.t.iter()).all(|(a, b)| a.kind == b.kind && a.owner == b.owner && a.marked == b.marked)); assert_eq!(l.settings.unwrap().language, "de"); assert_eq!(l.camera.unwrap().tx, 10.0); assert_eq!((l.game.map.w, l.game.map.h), (96, 128)); assert_eq!(l.game.map.portals.len(), g.map.portals.len()); assert!(l.game.map.t.iter().zip(g.map.t.iter()).all(|(a, b)| a.var == b.var && a.hp == b.hp && a.food == b.food && a.gold == b.gold)); // the loaded game keeps running identically let mut a = l.game; let mut b = g; for _ in 0..2000 { a.update(0.05); b.update(0.05); } assert_eq!(a.gold, b.gold); assert_eq!(a.ents.len(), b.ents.len()); assert!(from_bytes(b"{\"format\":\"x\"}").is_err()); } #[test] fn huge_map() { let t0 = std::time::Instant::now(); let mut g = Game::new(7, 256, 256); let gen = t0.elapsed(); assert_eq!(g.map.portals.len(), 16); assert_eq!(g.map.treasures.len(), 0); // knight caves are replaced by dwarf caves assert!((1..=4).contains(&g.dwarf_caves.len())); let (cx, cy) = g.map.core; g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true); let t1 = std::time::Instant::now(); for _ in 0..(120 * 20) { g.update(0.05); } let sim = t1.elapsed(); let json = to_json(&g, &Cam::new(0.0, 0.0), &Settings::default()); println!("256x256: generate {:?}, 2 min simulated in {:?}, savegame {} KB", gen, sim, json.len() / 1024); let l = from_bytes(json.as_bytes()).expect("load"); assert_eq!(l.game.map.t.len(), 256 * 256); } // writes a savegame shortly before a wave (with planned entry points) when DC_SAVE_OUT is set #[test] fn dump_entry_warning_save() { let Ok(path) = std::env::var("DC_SAVE_OUT") else { return }; let mut g = Game::new(11, 64, 64); for _ in 0..(60 * 20) { g.update(0.05); } g.wave_timer = 31.0; for _ in 0..30 { g.update(0.05); // the scouts warn 30 s before the wave } assert!(!g.planned_entries.is_empty()); let (cx, cy) = g.map.core; g.spawn(crate::game::EK::Chainbreaker, false, (cx - 1) as f32 + 0.5, (cy - 1) as f32 + 0.5, 1); g.spawn(crate::game::EK::Ogre, false, (cx + 4) as f32 + 0.5, (cy - 1) as f32 + 0.5, 1); let (ex, ey) = (cx + 1, cy + 1); let mut cam = Cam::new(ex as f32 + 0.5, ey as f32 + 0.5); cam.zoom = 12.0; std::fs::write(path, to_json(&g, &cam, &Settings { language: "en".into(), ..Settings::default() })).unwrap(); } }