Skip to main content

proof_engine/save/
snapshot.rs

1//! World snapshots — serializable representations of the entire game state.
2//!
3//! A `WorldSnapshot` captures all entity components and named resources at a
4//! single point in time. Snapshots are used by the save/load system, the
5//! checkpoint system, and the network replication layer.
6//!
7//! ## Diff / incremental saves
8//!
9//! `WorldSnapshot::diff` produces a `SnapshotDiff` describing what changed
10//! between two snapshots. This is used for incremental save files and for
11//! network state synchronisation.
12
13use std::collections::HashMap;
14
15use crate::save::serializer::{DeserializeError, Serialize, SerializedValue};
16
17// ─────────────────────────────────────────────
18//  EntitySnapshot
19// ─────────────────────────────────────────────
20
21/// Serialized state of a single entity (all its components).
22#[derive(Debug, Clone, PartialEq)]
23pub struct EntitySnapshot {
24    /// Stable entity id (assigned by the ECS / entity manager).
25    pub entity_id: u64,
26    /// Component type name → serialized value.
27    pub components: HashMap<String, SerializedValue>,
28}
29
30impl EntitySnapshot {
31    /// Create an empty snapshot for `entity_id`.
32    pub fn new(entity_id: u64) -> Self {
33        Self { entity_id, components: HashMap::new() }
34    }
35
36    /// Add or replace a component value.
37    pub fn insert_component(&mut self, name: impl Into<String>, value: SerializedValue) {
38        self.components.insert(name.into(), value);
39    }
40
41    /// Remove a component. Returns the old value if present.
42    pub fn remove_component(&mut self, name: &str) -> Option<SerializedValue> {
43        self.components.remove(name)
44    }
45
46    /// Get a component value by name.
47    pub fn get_component(&self, name: &str) -> Option<&SerializedValue> {
48        self.components.get(name)
49    }
50
51    /// Returns `true` if the entity has a component with this name.
52    pub fn has_component(&self, name: &str) -> bool {
53        self.components.contains_key(name)
54    }
55
56    /// Number of components stored.
57    pub fn component_count(&self) -> usize {
58        self.components.len()
59    }
60
61    /// Merge components from `other` into `self` (other wins on conflict).
62    pub fn merge_from(&mut self, other: &EntitySnapshot) {
63        for (k, v) in &other.components {
64            self.components.insert(k.clone(), v.clone());
65        }
66    }
67
68    /// Serialize to a `SerializedValue::Map`.
69    pub fn to_serialized(&self) -> SerializedValue {
70        let mut map = HashMap::new();
71        map.insert("entity_id".into(), SerializedValue::Int(self.entity_id as i64));
72        let comp_map: HashMap<String, SerializedValue> = self.components.clone();
73        map.insert("components".into(), SerializedValue::Map(comp_map));
74        SerializedValue::Map(map)
75    }
76
77    /// Deserialize from a `SerializedValue::Map`.
78    pub fn from_serialized(v: &SerializedValue) -> Result<Self, DeserializeError> {
79        let id = v.get("entity_id")
80            .and_then(|v| v.as_int())
81            .ok_or_else(|| DeserializeError::MissingKey("entity_id".into()))? as u64;
82        let components = v.get("components")
83            .and_then(|v| v.as_map())
84            .cloned()
85            .unwrap_or_default();
86        Ok(Self { entity_id: id, components })
87    }
88}
89
90// ─────────────────────────────────────────────
91//  ResourceSnapshot
92// ─────────────────────────────────────────────
93
94/// Serialized state of a named global resource (e.g. "PlayerStats", "GameClock").
95#[derive(Debug, Clone, PartialEq)]
96pub struct ResourceSnapshot {
97    /// The resource's type name (used as a key during deserialization).
98    pub type_name: String,
99    /// The serialized value.
100    pub value: SerializedValue,
101}
102
103impl ResourceSnapshot {
104    pub fn new(type_name: impl Into<String>, value: SerializedValue) -> Self {
105        Self { type_name: type_name.into(), value }
106    }
107
108    pub fn to_serialized(&self) -> SerializedValue {
109        let mut map = HashMap::new();
110        map.insert("type_name".into(), SerializedValue::Str(self.type_name.clone()));
111        map.insert("value".into(), self.value.clone());
112        SerializedValue::Map(map)
113    }
114
115    pub fn from_serialized(v: &SerializedValue) -> Result<Self, DeserializeError> {
116        let type_name = v.get("type_name")
117            .and_then(|v| v.as_str())
118            .ok_or_else(|| DeserializeError::MissingKey("type_name".into()))?
119            .to_string();
120        let value = v.get("value").cloned().unwrap_or(SerializedValue::Null);
121        Ok(Self { type_name, value })
122    }
123}
124
125// ─────────────────────────────────────────────
126//  SnapshotDiff
127// ─────────────────────────────────────────────
128
129/// The delta between two `WorldSnapshot`s. Used for incremental saves and
130/// for detecting what changed between frames.
131#[derive(Debug, Clone, Default)]
132pub struct SnapshotDiff {
133    /// Entities present in `new` but not in `old`.
134    pub added_entities: Vec<EntitySnapshot>,
135    /// Entity ids present in `old` but removed in `new`.
136    pub removed_entities: Vec<u64>,
137    /// Entities present in both but with different component data.
138    pub changed_entities: Vec<(u64, EntitySnapshot)>,
139    /// Resources added or changed.
140    pub changed_resources: Vec<ResourceSnapshot>,
141    /// Resource type names that were removed.
142    pub removed_resources: Vec<String>,
143}
144
145impl SnapshotDiff {
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Returns `true` if no changes were detected.
151    pub fn is_empty(&self) -> bool {
152        self.added_entities.is_empty()
153            && self.removed_entities.is_empty()
154            && self.changed_entities.is_empty()
155            && self.changed_resources.is_empty()
156            && self.removed_resources.is_empty()
157    }
158
159    /// Total number of entity-level changes.
160    pub fn entity_change_count(&self) -> usize {
161        self.added_entities.len() + self.removed_entities.len() + self.changed_entities.len()
162    }
163
164    /// Total number of resource-level changes.
165    pub fn resource_change_count(&self) -> usize {
166        self.changed_resources.len() + self.removed_resources.len()
167    }
168}
169
170// ─────────────────────────────────────────────
171//  WorldSnapshot
172// ─────────────────────────────────────────────
173
174/// A complete serialized snapshot of the world state at a point in time.
175///
176/// This is the primary unit passed to the save/load pipeline and the checkpoint
177/// manager.
178#[derive(Debug, Clone)]
179pub struct WorldSnapshot {
180    /// All entity snapshots.
181    pub entities: Vec<EntitySnapshot>,
182    /// All resource snapshots.
183    pub resources: Vec<ResourceSnapshot>,
184    /// When this snapshot was taken, in seconds since some epoch (game time).
185    pub timestamp: f64,
186    /// Schema version — used to detect incompatible save files.
187    pub version: u32,
188    /// Free-form metadata (level name, player name, etc.).
189    pub metadata: HashMap<String, String>,
190}
191
192impl WorldSnapshot {
193    /// Create an empty snapshot.
194    pub fn new() -> Self {
195        Self {
196            entities: Vec::new(),
197            resources: Vec::new(),
198            timestamp: 0.0,
199            version: 1,
200            metadata: HashMap::new(),
201        }
202    }
203
204    /// Create a snapshot with a specific timestamp.
205    pub fn with_timestamp(mut self, ts: f64) -> Self {
206        self.timestamp = ts;
207        self
208    }
209
210    // ── Building ───────────────────────────────────────────────────────────
211
212    /// Add or replace an entity snapshot.
213    pub fn add_entity(&mut self, entity_id: u64, components: HashMap<String, SerializedValue>) {
214        if let Some(existing) = self.entities.iter_mut().find(|e| e.entity_id == entity_id) {
215            existing.components = components;
216        } else {
217            self.entities.push(EntitySnapshot { entity_id, components });
218        }
219    }
220
221    /// Add an entity snapshot directly.
222    pub fn add_entity_snapshot(&mut self, snapshot: EntitySnapshot) {
223        self.add_entity(snapshot.entity_id, snapshot.components);
224    }
225
226    /// Add or replace a resource snapshot.
227    pub fn add_resource(&mut self, type_name: impl Into<String>, value: SerializedValue) {
228        let type_name = type_name.into();
229        if let Some(existing) = self.resources.iter_mut().find(|r| r.type_name == type_name) {
230            existing.value = value;
231        } else {
232            self.resources.push(ResourceSnapshot::new(type_name, value));
233        }
234    }
235
236    /// Set a metadata key.
237    pub fn set_meta(&mut self, key: impl Into<String>, value: impl Into<String>) {
238        self.metadata.insert(key.into(), value.into());
239    }
240
241    /// Get a metadata value.
242    pub fn get_meta(&self, key: &str) -> Option<&str> {
243        self.metadata.get(key).map(String::as_str)
244    }
245
246    // ── Queries ────────────────────────────────────────────────────────────
247
248    /// Number of entity snapshots.
249    pub fn entity_count(&self) -> usize {
250        self.entities.len()
251    }
252
253    /// Number of resource snapshots.
254    pub fn resource_count(&self) -> usize {
255        self.resources.len()
256    }
257
258    /// Find an entity snapshot by id.
259    pub fn get_entity(&self, id: u64) -> Option<&EntitySnapshot> {
260        self.entities.iter().find(|e| e.entity_id == id)
261    }
262
263    /// Find a mutable entity snapshot by id.
264    pub fn get_entity_mut(&mut self, id: u64) -> Option<&mut EntitySnapshot> {
265        self.entities.iter_mut().find(|e| e.entity_id == id)
266    }
267
268    /// Find a resource snapshot by type name.
269    pub fn get_resource(&self, type_name: &str) -> Option<&ResourceSnapshot> {
270        self.resources.iter().find(|r| r.type_name == type_name)
271    }
272
273    /// Remove an entity by id. Returns `true` if it was found.
274    pub fn remove_entity(&mut self, id: u64) -> bool {
275        let before = self.entities.len();
276        self.entities.retain(|e| e.entity_id != id);
277        self.entities.len() < before
278    }
279
280    // ── Merge ──────────────────────────────────────────────────────────────
281
282    /// Merge `other` into `self`. Entities and resources in `other` override `self`.
283    pub fn merge(&mut self, other: &WorldSnapshot) {
284        for entity in &other.entities {
285            self.add_entity(entity.entity_id, entity.components.clone());
286        }
287        for resource in &other.resources {
288            self.add_resource(resource.type_name.clone(), resource.value.clone());
289        }
290        for (k, v) in &other.metadata {
291            self.metadata.insert(k.clone(), v.clone());
292        }
293        // Use the newer timestamp
294        if other.timestamp > self.timestamp {
295            self.timestamp = other.timestamp;
296        }
297    }
298
299    // ── Diff ───────────────────────────────────────────────────────────────
300
301    /// Compute the changes from `self` (old) to `other` (new).
302    pub fn diff(&self, other: &WorldSnapshot) -> SnapshotDiff {
303        let mut diff = SnapshotDiff::new();
304
305        // Build entity lookup
306        let old_map: HashMap<u64, &EntitySnapshot> =
307            self.entities.iter().map(|e| (e.entity_id, e)).collect();
308        let new_map: HashMap<u64, &EntitySnapshot> =
309            other.entities.iter().map(|e| (e.entity_id, e)).collect();
310
311        // Added and changed
312        for (&id, &new_e) in &new_map {
313            match old_map.get(&id) {
314                None => diff.added_entities.push(new_e.clone()),
315                Some(&old_e) => {
316                    if old_e != new_e {
317                        diff.changed_entities.push((id, new_e.clone()));
318                    }
319                }
320            }
321        }
322
323        // Removed entities
324        for &id in old_map.keys() {
325            if !new_map.contains_key(&id) {
326                diff.removed_entities.push(id);
327            }
328        }
329
330        // Resources
331        let old_res: HashMap<&str, &ResourceSnapshot> =
332            self.resources.iter().map(|r| (r.type_name.as_str(), r)).collect();
333        let new_res: HashMap<&str, &ResourceSnapshot> =
334            other.resources.iter().map(|r| (r.type_name.as_str(), r)).collect();
335
336        for (&name, &new_r) in &new_res {
337            match old_res.get(name) {
338                None => diff.changed_resources.push(new_r.clone()),
339                Some(&old_r) => {
340                    if old_r != new_r {
341                        diff.changed_resources.push(new_r.clone());
342                    }
343                }
344            }
345        }
346
347        for &name in old_res.keys() {
348            if !new_res.contains_key(name) {
349                diff.removed_resources.push(name.to_string());
350            }
351        }
352
353        diff
354    }
355
356    // ── Apply diff ─────────────────────────────────────────────────────────
357
358    /// Apply a `SnapshotDiff` to produce an updated snapshot.
359    pub fn apply_diff(&mut self, diff: &SnapshotDiff) {
360        // Remove entities
361        for &id in &diff.removed_entities {
362            self.remove_entity(id);
363        }
364        // Add new entities
365        for entity in &diff.added_entities {
366            self.add_entity_snapshot(entity.clone());
367        }
368        // Apply changes
369        for (_, entity) in &diff.changed_entities {
370            self.add_entity_snapshot(entity.clone());
371        }
372        // Resources
373        for &ref name in &diff.removed_resources {
374            self.resources.retain(|r| &r.type_name != name);
375        }
376        for resource in &diff.changed_resources {
377            self.add_resource(resource.type_name.clone(), resource.value.clone());
378        }
379    }
380}
381
382impl Default for WorldSnapshot {
383    fn default() -> Self {
384        Self::new()
385    }
386}
387
388// ─────────────────────────────────────────────
389//  SnapshotSerializer
390// ─────────────────────────────────────────────
391
392/// Converts `WorldSnapshot` to/from raw bytes using JSON encoding.
393pub struct SnapshotSerializer;
394
395impl SnapshotSerializer {
396    /// Serialize a `WorldSnapshot` to a JSON byte vector.
397    pub fn to_bytes(snapshot: &WorldSnapshot) -> Vec<u8> {
398        let sv = Self::snapshot_to_sv(snapshot);
399        sv.to_json_string().into_bytes()
400    }
401
402    /// Deserialize a `WorldSnapshot` from JSON bytes.
403    pub fn from_bytes(bytes: &[u8]) -> Result<WorldSnapshot, DeserializeError> {
404        let s = std::str::from_utf8(bytes)
405            .map_err(|e| DeserializeError::ParseError(e.to_string()))?;
406        let sv = SerializedValue::from_json_str(s)?;
407        Self::snapshot_from_sv(&sv)
408    }
409
410    fn snapshot_to_sv(snapshot: &WorldSnapshot) -> SerializedValue {
411        let mut map = HashMap::new();
412        map.insert("version".into(), SerializedValue::Int(snapshot.version as i64));
413        map.insert("timestamp".into(), SerializedValue::Float(snapshot.timestamp));
414
415        // Entities
416        let entities: Vec<SerializedValue> =
417            snapshot.entities.iter().map(|e| e.to_serialized()).collect();
418        map.insert("entities".into(), SerializedValue::List(entities));
419
420        // Resources
421        let resources: Vec<SerializedValue> =
422            snapshot.resources.iter().map(|r| r.to_serialized()).collect();
423        map.insert("resources".into(), SerializedValue::List(resources));
424
425        // Metadata
426        let meta: HashMap<String, SerializedValue> = snapshot
427            .metadata
428            .iter()
429            .map(|(k, v)| (k.clone(), SerializedValue::Str(v.clone())))
430            .collect();
431        map.insert("metadata".into(), SerializedValue::Map(meta));
432
433        SerializedValue::Map(map)
434    }
435
436    fn snapshot_from_sv(sv: &SerializedValue) -> Result<WorldSnapshot, DeserializeError> {
437        let version = sv.get("version")
438            .and_then(|v| v.as_int())
439            .unwrap_or(1) as u32;
440        let timestamp = sv.get("timestamp")
441            .and_then(|v| v.as_float())
442            .unwrap_or(0.0);
443
444        let entities = sv.get("entities")
445            .and_then(|v| v.as_list())
446            .unwrap_or(&[])
447            .iter()
448            .map(EntitySnapshot::from_serialized)
449            .collect::<Result<Vec<_>, _>>()?;
450
451        let resources = sv.get("resources")
452            .and_then(|v| v.as_list())
453            .unwrap_or(&[])
454            .iter()
455            .map(ResourceSnapshot::from_serialized)
456            .collect::<Result<Vec<_>, _>>()?;
457
458        let metadata = sv.get("metadata")
459            .and_then(|v| v.as_map())
460            .map(|m| {
461                m.iter()
462                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
463                    .collect()
464            })
465            .unwrap_or_default();
466
467        Ok(WorldSnapshot { entities, resources, timestamp, version, metadata })
468    }
469}
470
471// ─────────────────────────────────────────────
472//  Tests
473// ─────────────────────────────────────────────
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    fn make_entity(id: u64, hp: i64) -> EntitySnapshot {
480        let mut e = EntitySnapshot::new(id);
481        e.insert_component("health", SerializedValue::Int(hp));
482        e
483    }
484
485    #[test]
486    fn snapshot_add_entity() {
487        let mut snap = WorldSnapshot::new();
488        snap.add_entity(1, {
489            let mut m = HashMap::new();
490            m.insert("x".into(), SerializedValue::Float(10.0));
491            m
492        });
493        assert_eq!(snap.entity_count(), 1);
494        assert!(snap.get_entity(1).is_some());
495    }
496
497    #[test]
498    fn snapshot_replace_entity() {
499        let mut snap = WorldSnapshot::new();
500        snap.add_entity(1, {
501            let mut m = HashMap::new(); m.insert("hp".into(), SerializedValue::Int(100)); m
502        });
503        snap.add_entity(1, {
504            let mut m = HashMap::new(); m.insert("hp".into(), SerializedValue::Int(50)); m
505        });
506        assert_eq!(snap.entity_count(), 1);
507        assert_eq!(
508            snap.get_entity(1).unwrap().get_component("hp"),
509            Some(&SerializedValue::Int(50))
510        );
511    }
512
513    #[test]
514    fn snapshot_remove_entity() {
515        let mut snap = WorldSnapshot::new();
516        snap.add_entity(42, HashMap::new());
517        assert!(snap.remove_entity(42));
518        assert!(!snap.remove_entity(42));
519    }
520
521    #[test]
522    fn snapshot_diff_added_removed() {
523        let mut old = WorldSnapshot::new();
524        old.add_entity_snapshot(make_entity(1, 100));
525        old.add_entity_snapshot(make_entity(2, 50));
526
527        let mut new = WorldSnapshot::new();
528        new.add_entity_snapshot(make_entity(1, 80)); // changed
529        new.add_entity_snapshot(make_entity(3, 60)); // added
530
531        let diff = old.diff(&new);
532        assert_eq!(diff.added_entities.len(), 1);
533        assert_eq!(diff.added_entities[0].entity_id, 3);
534        assert_eq!(diff.removed_entities, vec![2]);
535        assert_eq!(diff.changed_entities.len(), 1);
536        assert_eq!(diff.changed_entities[0].0, 1);
537    }
538
539    #[test]
540    fn snapshot_diff_no_changes() {
541        let mut snap = WorldSnapshot::new();
542        snap.add_entity_snapshot(make_entity(1, 100));
543        let diff = snap.diff(&snap.clone());
544        assert!(diff.is_empty());
545    }
546
547    #[test]
548    fn snapshot_merge() {
549        let mut base = WorldSnapshot::new();
550        base.add_entity_snapshot(make_entity(1, 100));
551
552        let mut patch = WorldSnapshot::new();
553        patch.add_entity_snapshot(make_entity(2, 200));
554        patch.timestamp = 99.0;
555
556        base.merge(&patch);
557        assert_eq!(base.entity_count(), 2);
558        assert_eq!(base.timestamp, 99.0);
559    }
560
561    #[test]
562    fn snapshot_serializer_roundtrip() {
563        let mut snap = WorldSnapshot::new();
564        snap.timestamp = 42.0;
565        snap.add_entity_snapshot(make_entity(7, 123));
566        snap.add_resource("score", SerializedValue::Int(9999));
567        snap.set_meta("level", "dungeon_1");
568
569        let bytes = SnapshotSerializer::to_bytes(&snap);
570        let restored = SnapshotSerializer::from_bytes(&bytes).unwrap();
571
572        assert_eq!(restored.timestamp, 42.0);
573        assert_eq!(restored.entity_count(), 1);
574        assert_eq!(restored.resource_count(), 1);
575        assert_eq!(restored.get_meta("level"), Some("dungeon_1"));
576        assert_eq!(
577            restored.get_entity(7).unwrap().get_component("health"),
578            Some(&SerializedValue::Int(123))
579        );
580    }
581
582    #[test]
583    fn resource_snapshot_roundtrip() {
584        let r = ResourceSnapshot::new("timer", SerializedValue::Float(3.14));
585        let sv = r.to_serialized();
586        let r2 = ResourceSnapshot::from_serialized(&sv).unwrap();
587        assert_eq!(r.type_name, r2.type_name);
588    }
589
590    #[test]
591    fn entity_snapshot_merge_from() {
592        let mut a = make_entity(1, 100);
593        let mut b = EntitySnapshot::new(1);
594        b.insert_component("mana", SerializedValue::Int(50));
595        a.merge_from(&b);
596        assert!(a.has_component("health"));
597        assert!(a.has_component("mana"));
598        assert_eq!(a.component_count(), 2);
599    }
600}