Skip to main content

proof_engine/save/
checkpoint.rs

1//! Checkpoint system — spatial save points and respawn management.
2//!
3//! A `Checkpoint` is a named point in the world that stores a full
4//! `WorldSnapshot` and a 2D position. The `CheckpointManager` maintains an
5//! ordered list of checkpoints, evicting the oldest ones when the cap is
6//! exceeded.
7//!
8//! `RespawnSystem` sits on top of the manager and tracks the "last activated"
9//! checkpoint so the game can quickly restore state on player death.
10
11use std::collections::HashMap;
12
13use glam::Vec2;
14
15use crate::save::serializer::{DeserializeError, Serialize, Deserialize, SerializedValue};
16use crate::save::snapshot::{SnapshotSerializer, WorldSnapshot};
17
18// ─────────────────────────────────────────────
19//  Checkpoint
20// ─────────────────────────────────────────────
21
22/// A single checkpoint — a world position plus a saved snapshot.
23#[derive(Debug, Clone)]
24pub struct Checkpoint {
25    /// Unique id assigned by `CheckpointManager`.
26    pub id: u64,
27    /// Human-readable label (e.g. "dungeon_entrance", "boss_arena_pre").
28    pub name: String,
29    /// The world-space position of this checkpoint.
30    pub position: Vec2,
31    /// The world state at the moment this checkpoint was created.
32    pub snapshot: WorldSnapshot,
33    /// When this checkpoint was created (game time in seconds).
34    pub created_at: f64,
35    /// Optional metadata tags.
36    pub tags: HashMap<String, String>,
37}
38
39impl Checkpoint {
40    pub fn new(
41        id: u64,
42        name: impl Into<String>,
43        position: Vec2,
44        snapshot: WorldSnapshot,
45        created_at: f64,
46    ) -> Self {
47        Self {
48            id,
49            name: name.into(),
50            position,
51            snapshot,
52            created_at,
53            tags: HashMap::new(),
54        }
55    }
56
57    /// Set a metadata tag on this checkpoint.
58    pub fn set_tag(&mut self, key: impl Into<String>, value: impl Into<String>) {
59        self.tags.insert(key.into(), value.into());
60    }
61
62    /// Get a metadata tag.
63    pub fn get_tag(&self, key: &str) -> Option<&str> {
64        self.tags.get(key).map(String::as_str)
65    }
66
67    /// Euclidean distance from this checkpoint to `pos`.
68    pub fn distance_to(&self, pos: Vec2) -> f32 {
69        (self.position - pos).length()
70    }
71
72    // ── Serialization ──────────────────────────────────────────────────────
73
74    pub fn to_serialized(&self) -> SerializedValue {
75        let mut map = HashMap::new();
76        map.insert("id".into(), SerializedValue::Int(self.id as i64));
77        map.insert("name".into(), SerializedValue::Str(self.name.clone()));
78        map.insert("position".into(), self.position.serialize());
79        map.insert("created_at".into(), SerializedValue::Float(self.created_at));
80
81        // Embed snapshot as a nested JSON string to avoid encoding issues
82        let snap_bytes = SnapshotSerializer::to_bytes(&self.snapshot);
83        let snap_str = String::from_utf8(snap_bytes).unwrap_or_default();
84        map.insert("snapshot".into(), SerializedValue::Str(snap_str));
85
86        let tags: HashMap<String, SerializedValue> = self.tags.iter()
87            .map(|(k, v)| (k.clone(), SerializedValue::Str(v.clone())))
88            .collect();
89        map.insert("tags".into(), SerializedValue::Map(tags));
90
91        SerializedValue::Map(map)
92    }
93
94    pub fn from_serialized(sv: &SerializedValue) -> Result<Self, DeserializeError> {
95        let id = sv.get("id")
96            .and_then(|v| v.as_int())
97            .ok_or_else(|| DeserializeError::MissingKey("id".into()))? as u64;
98        let name = sv.get("name")
99            .and_then(|v| v.as_str())
100            .unwrap_or("unnamed")
101            .to_string();
102        let position = sv.get("position")
103            .map(Vec2::deserialize)
104            .transpose()?
105            .unwrap_or(Vec2::ZERO);
106        let created_at = sv.get("created_at")
107            .and_then(|v| v.as_float())
108            .unwrap_or(0.0);
109
110        let snapshot_str = sv.get("snapshot")
111            .and_then(|v| v.as_str())
112            .unwrap_or("{}");
113        let snapshot = SnapshotSerializer::from_bytes(snapshot_str.as_bytes())
114            .unwrap_or_else(|_| WorldSnapshot::new());
115
116        let tags: HashMap<String, String> = sv.get("tags")
117            .and_then(|v| v.as_map())
118            .map(|m| {
119                m.iter()
120                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
121                    .collect()
122            })
123            .unwrap_or_default();
124
125        Ok(Checkpoint { id, name, position, snapshot, created_at, tags })
126    }
127}
128
129// ─────────────────────────────────────────────
130//  CheckpointManager
131// ─────────────────────────────────────────────
132
133/// Manages a bounded list of checkpoints.
134///
135/// When the checkpoint count exceeds `max_checkpoints`, the oldest (by
136/// `created_at`) is evicted automatically.
137pub struct CheckpointManager {
138    checkpoints: Vec<Checkpoint>,
139    pub max_checkpoints: usize,
140    id_counter: u64,
141}
142
143impl CheckpointManager {
144    /// Create a new manager with a capacity cap.
145    pub fn new(max: usize) -> Self {
146        Self {
147            checkpoints: Vec::new(),
148            max_checkpoints: max.max(1),
149            id_counter: 0,
150        }
151    }
152
153    // ── Creation ───────────────────────────────────────────────────────────
154
155    /// Create a new checkpoint and return its id.
156    ///
157    /// If the checkpoint list is at capacity, the oldest checkpoint is removed.
158    pub fn create(
159        &mut self,
160        name: impl Into<String>,
161        pos: Vec2,
162        snapshot: WorldSnapshot,
163    ) -> u64 {
164        self.create_with_time(name, pos, snapshot, 0.0)
165    }
166
167    /// Like `create` but with an explicit game-time timestamp.
168    pub fn create_with_time(
169        &mut self,
170        name: impl Into<String>,
171        pos: Vec2,
172        snapshot: WorldSnapshot,
173        created_at: f64,
174    ) -> u64 {
175        let id = self.next_id();
176        let checkpoint = Checkpoint::new(id, name, pos, snapshot, created_at);
177        self.checkpoints.push(checkpoint);
178        self.evict_if_over_cap();
179        id
180    }
181
182    fn next_id(&mut self) -> u64 {
183        let id = self.id_counter;
184        self.id_counter += 1;
185        id
186    }
187
188    fn evict_if_over_cap(&mut self) {
189        while self.checkpoints.len() > self.max_checkpoints {
190            // Remove the checkpoint with the smallest created_at
191            let oldest_idx = self
192                .checkpoints
193                .iter()
194                .enumerate()
195                .min_by(|(_, a), (_, b)| a.created_at.partial_cmp(&b.created_at).unwrap())
196                .map(|(i, _)| i)
197                .unwrap_or(0);
198            self.checkpoints.remove(oldest_idx);
199        }
200    }
201
202    // ── Retrieval ──────────────────────────────────────────────────────────
203
204    /// Get a checkpoint by id.
205    pub fn get(&self, id: u64) -> Option<&Checkpoint> {
206        self.checkpoints.iter().find(|c| c.id == id)
207    }
208
209    /// Get a mutable checkpoint by id.
210    pub fn get_mut(&mut self, id: u64) -> Option<&mut Checkpoint> {
211        self.checkpoints.iter_mut().find(|c| c.id == id)
212    }
213
214    /// Get the checkpoint closest to `pos` (Euclidean distance).
215    pub fn get_nearest(&self, pos: Vec2) -> Option<&Checkpoint> {
216        self.checkpoints
217            .iter()
218            .min_by(|a, b| {
219                a.distance_to(pos)
220                    .partial_cmp(&b.distance_to(pos))
221                    .unwrap_or(std::cmp::Ordering::Equal)
222            })
223    }
224
225    /// Get the most recently created checkpoint (highest `created_at`).
226    pub fn get_most_recent(&self) -> Option<&Checkpoint> {
227        self.checkpoints
228            .iter()
229            .max_by(|a, b| a.created_at.partial_cmp(&b.created_at).unwrap_or(std::cmp::Ordering::Equal))
230    }
231
232    /// Get all checkpoints within `radius` of `pos`, sorted by distance.
233    pub fn get_within_radius(&self, pos: Vec2, radius: f32) -> Vec<&Checkpoint> {
234        let mut nearby: Vec<&Checkpoint> = self
235            .checkpoints
236            .iter()
237            .filter(|c| c.distance_to(pos) <= radius)
238            .collect();
239        nearby.sort_by(|a, b| {
240            a.distance_to(pos)
241                .partial_cmp(&b.distance_to(pos))
242                .unwrap_or(std::cmp::Ordering::Equal)
243        });
244        nearby
245    }
246
247    /// Iterate over all checkpoints.
248    pub fn list(&self) -> &[Checkpoint] {
249        &self.checkpoints
250    }
251
252    // ── Removal ────────────────────────────────────────────────────────────
253
254    /// Remove the checkpoint with the given id. Returns `true` if found.
255    pub fn remove(&mut self, id: u64) -> bool {
256        let before = self.checkpoints.len();
257        self.checkpoints.retain(|c| c.id != id);
258        self.checkpoints.len() < before
259    }
260
261    /// Remove all checkpoints.
262    pub fn clear(&mut self) {
263        self.checkpoints.clear();
264    }
265
266    // ── Info ───────────────────────────────────────────────────────────────
267
268    /// Number of checkpoints.
269    pub fn len(&self) -> usize {
270        self.checkpoints.len()
271    }
272
273    pub fn is_empty(&self) -> bool {
274        self.checkpoints.is_empty()
275    }
276
277    pub fn is_at_cap(&self) -> bool {
278        self.checkpoints.len() >= self.max_checkpoints
279    }
280
281    // ── Serialization ──────────────────────────────────────────────────────
282
283    /// Serialize all checkpoints to a JSON byte vector.
284    pub fn serialize_all(&self) -> Vec<u8> {
285        let list: Vec<SerializedValue> = self.checkpoints.iter()
286            .map(|c| c.to_serialized())
287            .collect();
288        let sv = SerializedValue::List(list);
289        sv.to_json_string().into_bytes()
290    }
291
292    /// Deserialize checkpoints from a JSON byte vector (as produced by `serialize_all`).
293    pub fn deserialize_all(bytes: &[u8]) -> Result<Vec<Checkpoint>, DeserializeError> {
294        let s = std::str::from_utf8(bytes)
295            .map_err(|e| DeserializeError::ParseError(e.to_string()))?;
296        let sv = SerializedValue::from_json_str(s)?;
297        sv.as_list()
298            .ok_or(DeserializeError::Custom("expected list of checkpoints".into()))?
299            .iter()
300            .map(Checkpoint::from_serialized)
301            .collect()
302    }
303
304    /// Restore checkpoints from bytes, replacing any existing ones.
305    pub fn load_from_bytes(&mut self, bytes: &[u8]) -> Result<(), DeserializeError> {
306        let checkpoints = Self::deserialize_all(bytes)?;
307        // Update id counter to avoid collisions
308        if let Some(max_id) = checkpoints.iter().map(|c| c.id).max() {
309            self.id_counter = max_id + 1;
310        }
311        self.checkpoints = checkpoints;
312        Ok(())
313    }
314}
315
316// ─────────────────────────────────────────────
317//  RespawnSystem
318// ─────────────────────────────────────────────
319
320/// Tracks the active checkpoint and handles player respawning.
321///
322/// Call `update_checkpoint` periodically (e.g. when the player enters a
323/// checkpoint trigger zone) to register the nearest checkpoint as active.
324/// Call `respawn` on player death to retrieve the saved state.
325pub struct RespawnSystem {
326    /// The id of the last activated checkpoint, if any.
327    pub last_checkpoint: Option<u64>,
328    /// How many times the player has respawned.
329    pub respawn_count: u32,
330    /// The minimum distance from a checkpoint to auto-activate it.
331    pub activation_radius: f32,
332    /// History of respawn events (checkpoint id + game time).
333    respawn_history: Vec<RespawnEvent>,
334}
335
336/// A single respawn event recorded by `RespawnSystem`.
337#[derive(Debug, Clone)]
338pub struct RespawnEvent {
339    pub checkpoint_id: u64,
340    pub game_time: f64,
341    pub respawn_index: u32,
342}
343
344impl RespawnSystem {
345    /// Create a new respawn system with no active checkpoint.
346    pub fn new() -> Self {
347        Self {
348            last_checkpoint: None,
349            respawn_count: 0,
350            activation_radius: 2.0,
351            respawn_history: Vec::new(),
352        }
353    }
354
355    pub fn with_activation_radius(mut self, r: f32) -> Self {
356        self.activation_radius = r;
357        self
358    }
359
360    // ── Activation ─────────────────────────────────────────────────────────
361
362    /// Update the active checkpoint based on the player's current position.
363    ///
364    /// If the player is within `activation_radius` of any checkpoint, the nearest
365    /// one becomes the active checkpoint. Returns the id of the newly activated
366    /// checkpoint (if one was activated this call) or `None`.
367    pub fn update_checkpoint(
368        &mut self,
369        manager: &CheckpointManager,
370        player_pos: Vec2,
371    ) -> Option<u64> {
372        let nearest = manager.get_nearest(player_pos)?;
373        if nearest.distance_to(player_pos) <= self.activation_radius {
374            let activated = self.last_checkpoint != Some(nearest.id);
375            self.last_checkpoint = Some(nearest.id);
376            if activated { Some(nearest.id) } else { None }
377        } else {
378            None
379        }
380    }
381
382    /// Manually activate a checkpoint by id.
383    pub fn activate(&mut self, checkpoint_id: u64) {
384        self.last_checkpoint = Some(checkpoint_id);
385    }
386
387    /// Clear the active checkpoint (e.g. at the start of a level).
388    pub fn deactivate(&mut self) {
389        self.last_checkpoint = None;
390    }
391
392    // ── Respawn ────────────────────────────────────────────────────────────
393
394    /// Get the snapshot to restore on respawn.
395    ///
396    /// Returns `None` if no checkpoint has been activated.
397    pub fn respawn<'a>(
398        &mut self,
399        manager: &'a CheckpointManager,
400        game_time: f64,
401    ) -> Option<&'a WorldSnapshot> {
402        let id = self.last_checkpoint?;
403        let checkpoint = manager.get(id)?;
404        self.respawn_count += 1;
405        self.respawn_history.push(RespawnEvent {
406            checkpoint_id: id,
407            game_time,
408            respawn_index: self.respawn_count,
409        });
410        Some(&checkpoint.snapshot)
411    }
412
413    /// Get the snapshot without incrementing the respawn counter.
414    pub fn peek_snapshot<'a>(&self, manager: &'a CheckpointManager) -> Option<&'a WorldSnapshot> {
415        let id = self.last_checkpoint?;
416        manager.get(id).map(|c| &c.snapshot)
417    }
418
419    // ── Info ───────────────────────────────────────────────────────────────
420
421    pub fn has_checkpoint(&self) -> bool {
422        self.last_checkpoint.is_some()
423    }
424
425    pub fn respawn_history(&self) -> &[RespawnEvent] {
426        &self.respawn_history
427    }
428
429    pub fn clear_history(&mut self) {
430        self.respawn_history.clear();
431    }
432
433    /// The position of the active checkpoint, if any.
434    pub fn checkpoint_position(&self, manager: &CheckpointManager) -> Option<Vec2> {
435        let id = self.last_checkpoint?;
436        manager.get(id).map(|c| c.position)
437    }
438}
439
440impl Default for RespawnSystem {
441    fn default() -> Self {
442        Self::new()
443    }
444}
445
446// ─────────────────────────────────────────────
447//  Tests
448// ─────────────────────────────────────────────
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use crate::save::serializer::SerializedValue;
454
455    fn make_snap(id: u64) -> WorldSnapshot {
456        let mut s = WorldSnapshot::new();
457        s.set_meta("source_entity", &id.to_string());
458        s
459    }
460
461    #[test]
462    fn create_and_get() {
463        let mut mgr = CheckpointManager::new(10);
464        let id = mgr.create("start", Vec2::ZERO, make_snap(1));
465        assert!(mgr.get(id).is_some());
466        assert_eq!(mgr.get(id).unwrap().name, "start");
467    }
468
469    #[test]
470    fn eviction_at_cap() {
471        let mut mgr = CheckpointManager::new(3);
472        let mut ids = vec![];
473        for i in 0..5u64 {
474            ids.push(mgr.create_with_time(format!("cp{i}"), Vec2::ZERO, make_snap(i), i as f64));
475        }
476        assert_eq!(mgr.len(), 3);
477        // Oldest (id 0, 1) should be gone; newest 3 remain
478        assert!(mgr.get(ids[0]).is_none());
479        assert!(mgr.get(ids[1]).is_none());
480        assert!(mgr.get(ids[4]).is_some());
481    }
482
483    #[test]
484    fn nearest_checkpoint() {
485        let mut mgr = CheckpointManager::new(10);
486        mgr.create("a", Vec2::new(0.0, 0.0), make_snap(1));
487        mgr.create("b", Vec2::new(100.0, 0.0), make_snap(2));
488        let near = mgr.get_nearest(Vec2::new(5.0, 0.0)).unwrap();
489        assert_eq!(near.name, "a");
490    }
491
492    #[test]
493    fn most_recent_checkpoint() {
494        let mut mgr = CheckpointManager::new(10);
495        mgr.create_with_time("first", Vec2::ZERO, make_snap(1), 1.0);
496        mgr.create_with_time("second", Vec2::ZERO, make_snap(2), 5.0);
497        mgr.create_with_time("third", Vec2::ZERO, make_snap(3), 3.0);
498        assert_eq!(mgr.get_most_recent().unwrap().name, "second");
499    }
500
501    #[test]
502    fn remove_checkpoint() {
503        let mut mgr = CheckpointManager::new(10);
504        let id = mgr.create("cp", Vec2::ZERO, make_snap(1));
505        assert!(mgr.remove(id));
506        assert!(!mgr.remove(id));
507        assert!(mgr.is_empty());
508    }
509
510    #[test]
511    fn serialize_deserialize_all() {
512        let mut mgr = CheckpointManager::new(10);
513        let mut s = make_snap(1);
514        s.timestamp = 42.0;
515        mgr.create("alpha", Vec2::new(1.0, 2.0), s);
516        mgr.create("beta", Vec2::new(3.0, 4.0), make_snap(2));
517
518        let bytes = mgr.serialize_all();
519        let restored = CheckpointManager::deserialize_all(&bytes).unwrap();
520        assert_eq!(restored.len(), 2);
521        assert_eq!(restored[0].name, "alpha");
522        assert!((restored[0].position.x - 1.0_f32).abs() < 1e-5);
523    }
524
525    #[test]
526    fn checkpoint_manager_load_from_bytes() {
527        let mut mgr = CheckpointManager::new(10);
528        mgr.create("cp1", Vec2::new(10.0, 20.0), make_snap(1));
529        let bytes = mgr.serialize_all();
530
531        let mut mgr2 = CheckpointManager::new(10);
532        mgr2.load_from_bytes(&bytes).unwrap();
533        assert_eq!(mgr2.len(), 1);
534        assert_eq!(mgr2.list()[0].name, "cp1");
535    }
536
537    #[test]
538    fn respawn_system_activate_and_respawn() {
539        let mut mgr = CheckpointManager::new(10);
540        let id = mgr.create("spawn", Vec2::ZERO, make_snap(99));
541
542        let mut respawn = RespawnSystem::new();
543        respawn.activate(id);
544        assert!(respawn.has_checkpoint());
545
546        let snap = respawn.respawn(&mgr, 0.0).unwrap();
547        assert_eq!(snap.get_meta("source_entity"), Some("99"));
548        assert_eq!(respawn.respawn_count, 1);
549    }
550
551    #[test]
552    fn respawn_system_update_by_proximity() {
553        let mut mgr = CheckpointManager::new(10);
554        mgr.create("near", Vec2::new(1.0, 0.0), make_snap(1));
555
556        let mut respawn = RespawnSystem::new().with_activation_radius(5.0);
557        let activated = respawn.update_checkpoint(&mgr, Vec2::new(0.5, 0.0));
558        assert!(activated.is_some());
559        assert!(respawn.has_checkpoint());
560    }
561
562    #[test]
563    fn respawn_system_no_checkpoint() {
564        let mgr = CheckpointManager::new(10);
565        let mut respawn = RespawnSystem::new();
566        assert!(respawn.respawn(&mgr, 0.0).is_none());
567    }
568
569    #[test]
570    fn respawn_history_tracks_events() {
571        let mut mgr = CheckpointManager::new(10);
572        let id = mgr.create("cp", Vec2::ZERO, make_snap(1));
573        let mut respawn = RespawnSystem::new();
574        respawn.activate(id);
575        respawn.respawn(&mgr, 10.0);
576        respawn.respawn(&mgr, 20.0);
577        assert_eq!(respawn.respawn_history().len(), 2);
578        assert_eq!(respawn.respawn_history()[1].game_time, 20.0);
579    }
580
581    #[test]
582    fn within_radius() {
583        let mut mgr = CheckpointManager::new(10);
584        mgr.create("close", Vec2::new(1.0, 0.0), make_snap(1));
585        mgr.create("far", Vec2::new(100.0, 0.0), make_snap(2));
586        let nearby = mgr.get_within_radius(Vec2::ZERO, 5.0);
587        assert_eq!(nearby.len(), 1);
588        assert_eq!(nearby[0].name, "close");
589    }
590
591    #[test]
592    fn checkpoint_tags() {
593        let mut mgr = CheckpointManager::new(10);
594        let id = mgr.create("tagged", Vec2::ZERO, make_snap(1));
595        mgr.get_mut(id).unwrap().set_tag("type", "boss_entrance");
596        assert_eq!(mgr.get(id).unwrap().get_tag("type"), Some("boss_entrance"));
597    }
598}