1use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19
20use crate::save::serializer::{DeserializeError, SerializedValue};
21use crate::save::snapshot::{SnapshotSerializer, WorldSnapshot};
22
23#[derive(Debug, Clone)]
29pub enum SaveError {
30 Io(String),
32 Corrupt,
34 VersionMismatch { expected: u32, found: u32 },
36 ChecksumMismatch,
38 SlotEmpty(u8),
40 Serialize(String),
42}
43
44impl std::fmt::Display for SaveError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 SaveError::Io(s) => write!(f, "I/O error: {s}"),
48 SaveError::Corrupt => write!(f, "save file is corrupt"),
49 SaveError::VersionMismatch { expected, found } => {
50 write!(f, "version mismatch: expected {expected}, found {found}")
51 }
52 SaveError::ChecksumMismatch => write!(f, "checksum mismatch — file may be corrupt"),
53 SaveError::SlotEmpty(n) => write!(f, "save slot {n} is empty"),
54 SaveError::Serialize(s) => write!(f, "serialization error: {s}"),
55 }
56 }
57}
58
59impl std::error::Error for SaveError {}
60
61impl From<DeserializeError> for SaveError {
62 fn from(e: DeserializeError) -> Self {
63 SaveError::Serialize(e.to_string())
64 }
65}
66
67pub const SAVE_MAGIC: [u8; 4] = *b"SAVE";
73
74pub const CURRENT_FORMAT_VERSION: u32 = 1;
76
77pub fn compute_checksum(data: &[u8]) -> u32 {
86 let mut crc: u32 = 0xFFFF_FFFF;
87 for &byte in data {
88 crc ^= byte as u32;
89 for _ in 0..8 {
90 if crc & 1 != 0 {
91 crc = (crc >> 1) ^ 0xEDB8_8320;
92 } else {
93 crc >>= 1;
94 }
95 }
96 }
97 !crc
98}
99
100#[derive(Debug, Clone)]
109pub struct SaveHeader {
110 pub magic: [u8; 4],
112 pub version: u32,
114 pub game_version: String,
116 pub timestamp: u64,
118 pub player_name: String,
120 pub play_time_seconds: f64,
122 pub metadata: HashMap<String, String>,
124}
125
126impl SaveHeader {
127 pub fn new() -> Self {
128 Self {
129 magic: SAVE_MAGIC,
130 version: CURRENT_FORMAT_VERSION,
131 game_version: "0.1.0".to_string(),
132 timestamp: current_unix_ts(),
133 player_name: "Player".to_string(),
134 play_time_seconds: 0.0,
135 metadata: HashMap::new(),
136 }
137 }
138
139 pub fn with_player(mut self, name: impl Into<String>) -> Self {
140 self.player_name = name.into();
141 self
142 }
143
144 pub fn with_play_time(mut self, secs: f64) -> Self {
145 self.play_time_seconds = secs;
146 self
147 }
148
149 pub fn with_game_version(mut self, ver: impl Into<String>) -> Self {
150 self.game_version = ver.into();
151 self
152 }
153
154 pub fn set_meta(&mut self, key: impl Into<String>, value: impl Into<String>) {
155 self.metadata.insert(key.into(), value.into());
156 }
157
158 pub fn get_meta(&self, key: &str) -> Option<&str> {
159 self.metadata.get(key).map(String::as_str)
160 }
161
162 fn to_serialized(&self) -> SerializedValue {
163 let mut map = HashMap::new();
164 map.insert("magic".into(), SerializedValue::Str(
165 std::str::from_utf8(&self.magic).unwrap_or("SAVE").to_string()
166 ));
167 map.insert("version".into(), SerializedValue::Int(self.version as i64));
168 map.insert("game_version".into(), SerializedValue::Str(self.game_version.clone()));
169 map.insert("timestamp".into(), SerializedValue::Int(self.timestamp as i64));
170 map.insert("player_name".into(), SerializedValue::Str(self.player_name.clone()));
171 map.insert("play_time_seconds".into(), SerializedValue::Float(self.play_time_seconds));
172 let meta: HashMap<String, SerializedValue> = self.metadata.iter()
173 .map(|(k, v)| (k.clone(), SerializedValue::Str(v.clone())))
174 .collect();
175 map.insert("metadata".into(), SerializedValue::Map(meta));
176 SerializedValue::Map(map)
177 }
178
179 fn from_serialized(sv: &SerializedValue) -> Result<Self, SaveError> {
180 let version = sv.get("version")
181 .and_then(|v| v.as_int())
182 .unwrap_or(CURRENT_FORMAT_VERSION as i64) as u32;
183 let game_version = sv.get("game_version")
184 .and_then(|v| v.as_str())
185 .unwrap_or("unknown")
186 .to_string();
187 let timestamp = sv.get("timestamp")
188 .and_then(|v| v.as_int())
189 .unwrap_or(0) as u64;
190 let player_name = sv.get("player_name")
191 .and_then(|v| v.as_str())
192 .unwrap_or("Player")
193 .to_string();
194 let play_time_seconds = sv.get("play_time_seconds")
195 .and_then(|v| v.as_float())
196 .unwrap_or(0.0);
197 let metadata: HashMap<String, String> = sv.get("metadata")
198 .and_then(|v| v.as_map())
199 .map(|m| {
200 m.iter()
201 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
202 .collect()
203 })
204 .unwrap_or_default();
205
206 Ok(SaveHeader {
207 magic: SAVE_MAGIC,
208 version,
209 game_version,
210 timestamp,
211 player_name,
212 play_time_seconds,
213 metadata,
214 })
215 }
216}
217
218impl Default for SaveHeader {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224pub struct SaveFile {
232 pub header: SaveHeader,
233 pub snapshot: WorldSnapshot,
234 pub checksum: u32,
236}
237
238impl SaveFile {
239 pub fn new(snapshot: WorldSnapshot) -> Self {
242 Self {
243 header: SaveHeader::new(),
244 snapshot,
245 checksum: 0,
246 }
247 }
248
249 pub fn with_header(mut self, header: SaveHeader) -> Self {
250 self.header = header;
251 self
252 }
253
254 pub fn write_to_bytes(&mut self) -> Vec<u8> {
258 let header_bytes = self.header.to_serialized().to_json_string().into_bytes();
259 let snapshot_bytes = SnapshotSerializer::to_bytes(&self.snapshot);
260
261 let mut checksum_input = Vec::with_capacity(header_bytes.len() + snapshot_bytes.len());
263 checksum_input.extend_from_slice(&header_bytes);
264 checksum_input.extend_from_slice(&snapshot_bytes);
265 self.checksum = compute_checksum(&checksum_input);
266
267 let mut out = Vec::with_capacity(
268 4 + 4 + header_bytes.len() + 4 + snapshot_bytes.len() + 4,
269 );
270
271 out.extend_from_slice(&SAVE_MAGIC);
273 out.extend_from_slice(&(header_bytes.len() as u32).to_le_bytes());
275 out.extend_from_slice(&header_bytes);
276 out.extend_from_slice(&(snapshot_bytes.len() as u32).to_le_bytes());
278 out.extend_from_slice(&snapshot_bytes);
279 out.extend_from_slice(&self.checksum.to_le_bytes());
281
282 out
283 }
284
285 pub fn write_to_file(&mut self, path: &str) -> Result<(), SaveError> {
287 let bytes = self.write_to_bytes();
288 if let Some(parent) = Path::new(path).parent() {
290 if !parent.as_os_str().is_empty() {
291 std::fs::create_dir_all(parent)
292 .map_err(|e| SaveError::Io(format!("create_dir_all: {e}")))?;
293 }
294 }
295 std::fs::write(path, &bytes)
296 .map_err(|e| SaveError::Io(format!("write '{path}': {e}")))?;
297 Ok(())
298 }
299
300 pub fn read_from_bytes(bytes: &[u8]) -> Result<SaveFile, SaveError> {
304 if bytes.len() < 4 {
305 return Err(SaveError::Corrupt);
306 }
307
308 if &bytes[0..4] != b"SAVE" {
310 return Err(SaveError::Corrupt);
311 }
312 let mut cursor = 4usize;
313
314 let header_len = read_u32(bytes, cursor)? as usize;
316 cursor += 4;
317 if cursor + header_len > bytes.len() {
318 return Err(SaveError::Corrupt);
319 }
320 let header_bytes = &bytes[cursor..cursor + header_len];
321 cursor += header_len;
322
323 let snap_len = read_u32(bytes, cursor)? as usize;
325 cursor += 4;
326 if cursor + snap_len > bytes.len() {
327 return Err(SaveError::Corrupt);
328 }
329 let snap_bytes = &bytes[cursor..cursor + snap_len];
330 cursor += snap_len;
331
332 if cursor + 4 > bytes.len() {
334 return Err(SaveError::Corrupt);
335 }
336 let stored_checksum = read_u32(bytes, cursor)?;
337
338 let mut checksum_input = Vec::with_capacity(header_len + snap_len);
340 checksum_input.extend_from_slice(header_bytes);
341 checksum_input.extend_from_slice(snap_bytes);
342 let computed = compute_checksum(&checksum_input);
343 if computed != stored_checksum {
344 return Err(SaveError::ChecksumMismatch);
345 }
346
347 let header_str = std::str::from_utf8(header_bytes).map_err(|_| SaveError::Corrupt)?;
349 let header_sv = SerializedValue::from_json_str(header_str)
350 .map_err(|e| SaveError::Serialize(e.to_string()))?;
351 let header = SaveHeader::from_serialized(&header_sv)?;
352
353 if header.version != CURRENT_FORMAT_VERSION {
355 return Err(SaveError::VersionMismatch {
356 expected: CURRENT_FORMAT_VERSION,
357 found: header.version,
358 });
359 }
360
361 let snapshot = SnapshotSerializer::from_bytes(snap_bytes)
363 .map_err(|e| SaveError::Serialize(e.to_string()))?;
364
365 Ok(SaveFile { header, snapshot, checksum: stored_checksum })
366 }
367
368 pub fn read_from_file(path: &str) -> Result<SaveFile, SaveError> {
370 let bytes = std::fs::read(path)
371 .map_err(|e| SaveError::Io(format!("read '{path}': {e}")))?;
372 SaveFile::read_from_bytes(&bytes)
373 }
374
375 pub fn verify_checksum(&self) -> bool {
379 let header_bytes = self.header.to_serialized().to_json_string().into_bytes();
380 let snapshot_bytes = SnapshotSerializer::to_bytes(&self.snapshot);
381 let mut input = Vec::with_capacity(header_bytes.len() + snapshot_bytes.len());
382 input.extend_from_slice(&header_bytes);
383 input.extend_from_slice(&snapshot_bytes);
384 compute_checksum(&input) == self.checksum
385 }
386}
387
388fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, SaveError> {
389 if offset + 4 > bytes.len() {
390 return Err(SaveError::Corrupt);
391 }
392 Ok(u32::from_le_bytes([
393 bytes[offset],
394 bytes[offset + 1],
395 bytes[offset + 2],
396 bytes[offset + 3],
397 ]))
398}
399
400#[derive(Debug, Clone)]
406pub struct SaveSlot {
407 pub slot_number: u8,
409 pub path: String,
411 pub header: Option<SaveHeader>,
413}
414
415impl SaveSlot {
416 pub fn new(slot_number: u8, path: String) -> Self {
417 Self { slot_number, path, header: None }
418 }
419
420 pub fn is_empty(&self) -> bool {
421 self.header.is_none()
422 }
423
424 pub fn player_name(&self) -> Option<&str> {
425 self.header.as_ref().map(|h| h.player_name.as_str())
426 }
427
428 pub fn play_time(&self) -> Option<f64> {
429 self.header.as_ref().map(|h| h.play_time_seconds)
430 }
431
432 pub fn timestamp(&self) -> Option<u64> {
433 self.header.as_ref().map(|h| h.timestamp)
434 }
435}
436
437pub struct SaveManager {
445 pub save_dir: String,
447 slots: Vec<SaveSlot>,
449}
450
451const AUTO_SAVE_SLOT: u8 = 255;
452const AUTO_SAVE_FILENAME: &str = "autosave.sav";
453
454impl SaveManager {
455 pub fn new(save_dir: impl Into<String>) -> Self {
459 Self {
460 save_dir: save_dir.into(),
461 slots: Vec::new(),
462 }
463 }
464
465 fn slot_path(&self, slot: u8) -> String {
468 if slot == AUTO_SAVE_SLOT {
469 format!("{}/{}", self.save_dir, AUTO_SAVE_FILENAME)
470 } else {
471 format!("{}/save_{:02}.sav", self.save_dir, slot)
472 }
473 }
474
475 pub fn list_slots(&self, max: usize) -> Vec<SaveSlot> {
480 let mut result = Vec::with_capacity(max);
481 for i in 0..max.min(255) {
482 let slot_num = i as u8;
483 let path = self.slot_path(slot_num);
484 let header = Self::try_read_header(&path);
485 result.push(SaveSlot { slot_number: slot_num, path, header });
486 }
487 result
488 }
489
490 fn try_read_header(path: &str) -> Option<SaveHeader> {
491 let bytes = std::fs::read(path).ok()?;
492 let file = SaveFile::read_from_bytes(&bytes).ok()?;
493 Some(file.header)
494 }
495
496 pub fn save_to_slot(
500 &self,
501 slot: u8,
502 snapshot: WorldSnapshot,
503 header: SaveHeader,
504 ) -> Result<(), SaveError> {
505 let path = self.slot_path(slot);
506 let mut file = SaveFile::new(snapshot).with_header(header);
507 file.write_to_file(&path)
508 }
509
510 pub fn load_slot(&self, slot: u8) -> Result<SaveFile, SaveError> {
514 let path = self.slot_path(slot);
515 if !Path::new(&path).exists() {
516 return Err(SaveError::SlotEmpty(slot));
517 }
518 SaveFile::read_from_file(&path)
519 }
520
521 pub fn delete_slot(&self, slot: u8) -> Result<(), SaveError> {
525 let path = self.slot_path(slot);
526 if !Path::new(&path).exists() {
527 return Ok(()); }
529 std::fs::remove_file(&path)
530 .map_err(|e| SaveError::Io(format!("delete slot {slot}: {e}")))
531 }
532
533 pub fn auto_save(&self, snapshot: WorldSnapshot) -> Result<(), SaveError> {
537 let mut header = SaveHeader::new();
538 header.set_meta("slot_type", "autosave");
539 self.save_to_slot(AUTO_SAVE_SLOT, snapshot, header)
540 }
541
542 pub fn has_auto_save(&self) -> bool {
544 Path::new(&self.slot_path(AUTO_SAVE_SLOT)).exists()
545 }
546
547 pub fn load_auto_save(&self) -> Result<SaveFile, SaveError> {
549 self.load_slot(AUTO_SAVE_SLOT)
550 }
551
552 pub fn most_recent_slot(&self) -> Option<u8> {
558 let mut best: Option<(u8, u64)> = None;
559
560 for slot_num in 0u8..32 {
561 let path = self.slot_path(slot_num);
562 if let Some(header) = Self::try_read_header(&path) {
563 if best.map_or(true, |(_, ts)| header.timestamp > ts) {
564 best = Some((slot_num, header.timestamp));
565 }
566 }
567 }
568
569 let auto_path = self.slot_path(AUTO_SAVE_SLOT);
571 if let Some(header) = Self::try_read_header(&auto_path) {
572 if best.map_or(true, |(_, ts)| header.timestamp > ts) {
573 best = Some((AUTO_SAVE_SLOT, header.timestamp));
574 }
575 }
576
577 best.map(|(slot, _)| slot)
578 }
579
580 pub fn ensure_dir(&self) -> Result<(), SaveError> {
584 std::fs::create_dir_all(&self.save_dir)
585 .map_err(|e| SaveError::Io(format!("create save dir '{}': {e}", self.save_dir)))
586 }
587
588 pub fn used_slot_count(&self) -> usize {
590 (0u8..32)
591 .filter(|&slot| Path::new(&self.slot_path(slot)).exists())
592 .count()
593 }
594}
595
596fn current_unix_ts() -> u64 {
601 use std::time::{SystemTime, UNIX_EPOCH};
602 SystemTime::now()
603 .duration_since(UNIX_EPOCH)
604 .map(|d| d.as_secs())
605 .unwrap_or(0)
606}
607
608#[cfg(test)]
613mod tests {
614 use super::*;
615 use crate::save::serializer::SerializedValue;
616
617 fn tmp_dir() -> String {
618 use std::time::{SystemTime, UNIX_EPOCH};
619 let t = SystemTime::now()
620 .duration_since(UNIX_EPOCH)
621 .unwrap()
622 .subsec_nanos();
623 let p = std::env::temp_dir().join(format!("proof_save_{t}"));
624 std::fs::create_dir_all(&p).unwrap();
625 p.to_string_lossy().to_string()
626 }
627
628 fn cleanup(dir: &str) {
629 let _ = std::fs::remove_dir_all(dir);
630 }
631
632 #[test]
633 fn checksum_deterministic() {
634 let data = b"hello world";
635 assert_eq!(compute_checksum(data), compute_checksum(data));
636 assert_ne!(compute_checksum(data), compute_checksum(b"hello worlD"));
637 }
638
639 #[test]
640 fn save_header_defaults() {
641 let h = SaveHeader::new();
642 assert_eq!(h.magic, SAVE_MAGIC);
643 assert_eq!(h.version, CURRENT_FORMAT_VERSION);
644 }
645
646 #[test]
647 fn save_file_roundtrip_bytes() {
648 let mut snap = WorldSnapshot::new();
649 snap.timestamp = 77.0;
650 snap.add_entity(1, {
651 let mut m = std::collections::HashMap::new();
652 m.insert("hp".into(), SerializedValue::Int(100));
653 m
654 });
655
656 let mut file = SaveFile::new(snap);
657 file.header.player_name = "Tester".into();
658
659 let bytes = file.write_to_bytes();
660 let restored = SaveFile::read_from_bytes(&bytes).unwrap();
661 assert_eq!(restored.header.player_name, "Tester");
662 assert_eq!(restored.snapshot.timestamp, 77.0);
663 assert_eq!(restored.snapshot.entity_count(), 1);
664 }
665
666 #[test]
667 fn save_file_verify_checksum() {
668 let mut file = SaveFile::new(WorldSnapshot::new());
669 file.write_to_bytes();
670 assert!(file.verify_checksum());
671 }
672
673 #[test]
674 fn save_file_checksum_mismatch() {
675 let mut file = SaveFile::new(WorldSnapshot::new());
676 let mut bytes = file.write_to_bytes();
677 let len = bytes.len();
679 bytes[len - 5] ^= 0xFF;
680 let result = SaveFile::read_from_bytes(&bytes);
681 assert!(matches!(result, Err(SaveError::ChecksumMismatch)));
682 }
683
684 #[test]
685 fn save_file_bad_magic() {
686 let bytes = b"NOPE...";
687 let result = SaveFile::read_from_bytes(bytes);
688 assert!(matches!(result, Err(SaveError::Corrupt)));
689 }
690
691 #[test]
692 fn save_manager_write_and_load() {
693 let dir = tmp_dir();
694 let mgr = SaveManager::new(&dir);
695
696 let mut snap = WorldSnapshot::new();
697 snap.set_meta("level", "forest");
698 let header = SaveHeader::new().with_player("Alice");
699 mgr.save_to_slot(0, snap, header).unwrap();
700
701 let file = mgr.load_slot(0).unwrap();
702 assert_eq!(file.header.player_name, "Alice");
703 assert_eq!(file.snapshot.get_meta("level"), Some("forest"));
704
705 cleanup(&dir);
706 }
707
708 #[test]
709 fn save_manager_empty_slot_error() {
710 let dir = tmp_dir();
711 let mgr = SaveManager::new(&dir);
712 let result = mgr.load_slot(7);
713 assert!(matches!(result, Err(SaveError::SlotEmpty(7))));
714 cleanup(&dir);
715 }
716
717 #[test]
718 fn save_manager_auto_save() {
719 let dir = tmp_dir();
720 let mgr = SaveManager::new(&dir);
721 assert!(!mgr.has_auto_save());
722 mgr.auto_save(WorldSnapshot::new()).unwrap();
723 assert!(mgr.has_auto_save());
724 cleanup(&dir);
725 }
726
727 #[test]
728 fn save_manager_delete_slot() {
729 let dir = tmp_dir();
730 let mgr = SaveManager::new(&dir);
731 mgr.save_to_slot(1, WorldSnapshot::new(), SaveHeader::new()).unwrap();
732 assert!(mgr.load_slot(1).is_ok());
733 mgr.delete_slot(1).unwrap();
734 assert!(matches!(mgr.load_slot(1), Err(SaveError::SlotEmpty(1))));
735 cleanup(&dir);
736 }
737
738 #[test]
739 fn save_manager_most_recent() {
740 let dir = tmp_dir();
741 let mgr = SaveManager::new(&dir);
742 mgr.save_to_slot(0, WorldSnapshot::new(), SaveHeader::new()).unwrap();
743 std::thread::sleep(std::time::Duration::from_millis(1100));
745 mgr.save_to_slot(1, WorldSnapshot::new(), SaveHeader::new()).unwrap();
746 let recent = mgr.most_recent_slot();
747 assert!(recent.is_some());
749 cleanup(&dir);
750 }
751
752 #[test]
753 fn save_slot_is_empty() {
754 let slot = SaveSlot::new(0, "path/save_00.sav".into());
755 assert!(slot.is_empty());
756 }
757
758 #[test]
759 fn save_header_metadata() {
760 let mut h = SaveHeader::new();
761 h.set_meta("difficulty", "hard");
762 assert_eq!(h.get_meta("difficulty"), Some("hard"));
763 }
764}