tx2-pack 0.1.1

Binary world snapshot format for ECS persistence, checkpointing, and time-travel
Documentation
use crate::error::{PackError, Result};
use crate::format::PackedSnapshot;
use crate::checkpoint::{Checkpoint, CheckpointManager};
use std::collections::VecDeque;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplayDirection {
    Forward,
    Backward,
}

pub struct ReplayEngine {
    checkpoints: VecDeque<Checkpoint>,
    current_index: usize,
    loop_replay: bool,
}

impl ReplayEngine {
    pub fn new() -> Self {
        Self {
            checkpoints: VecDeque::new(),
            current_index: 0,
            loop_replay: false,
        }
    }

    pub fn with_loop(mut self, enabled: bool) -> Self {
        self.loop_replay = enabled;
        self
    }

    pub fn add_checkpoint(&mut self, checkpoint: Checkpoint) {
        self.checkpoints.push_back(checkpoint);
    }

    pub fn load_from_manager(&mut self, manager: &mut CheckpointManager) -> Result<()> {
        self.checkpoints.clear();

        let chain = manager.get_checkpoint_chain().to_vec();
        for id in chain {
            let checkpoint = manager.load_checkpoint(&id)?;
            self.checkpoints.push_back(checkpoint);
        }

        self.current_index = 0;

        Ok(())
    }

    pub fn current(&self) -> Option<&Checkpoint> {
        self.checkpoints.get(self.current_index)
    }

    pub fn next(&mut self) -> Option<&Checkpoint> {
        if self.current_index + 1 < self.checkpoints.len() {
            self.current_index += 1;
            self.current()
        } else if self.loop_replay && !self.checkpoints.is_empty() {
            self.current_index = 0;
            self.current()
        } else {
            None
        }
    }

    pub fn previous(&mut self) -> Option<&Checkpoint> {
        if self.current_index > 0 {
            self.current_index -= 1;
            self.current()
        } else if self.loop_replay && !self.checkpoints.is_empty() {
            self.current_index = self.checkpoints.len() - 1;
            self.current()
        } else {
            None
        }
    }

    pub fn seek(&mut self, index: usize) -> Result<&Checkpoint> {
        if index >= self.checkpoints.len() {
            return Err(PackError::InvalidCheckpoint(
                format!("Index {} out of bounds", index)
            ));
        }

        self.current_index = index;
        self.current()
            .ok_or_else(|| PackError::InvalidCheckpoint("No checkpoint at index".to_string()))
    }

    pub fn seek_to_start(&mut self) -> Option<&Checkpoint> {
        self.current_index = 0;
        self.current()
    }

    pub fn seek_to_end(&mut self) -> Option<&Checkpoint> {
        if !self.checkpoints.is_empty() {
            self.current_index = self.checkpoints.len() - 1;
        }
        self.current()
    }

    pub fn get_index(&self) -> usize {
        self.current_index
    }

    pub fn len(&self) -> usize {
        self.checkpoints.len()
    }

    pub fn is_empty(&self) -> bool {
        self.checkpoints.is_empty()
    }

    pub fn is_at_start(&self) -> bool {
        self.current_index == 0
    }

    pub fn is_at_end(&self) -> bool {
        self.current_index == self.checkpoints.len().saturating_sub(1)
    }

    pub fn clear(&mut self) {
        self.checkpoints.clear();
        self.current_index = 0;
    }
}

impl Default for ReplayEngine {
    fn default() -> Self {
        Self::new()
    }
}

pub struct TimeTravel {
    snapshots: Vec<(f64, PackedSnapshot)>,
    current_time: f64,
}

impl TimeTravel {
    pub fn new() -> Self {
        Self {
            snapshots: Vec::new(),
            current_time: 0.0,
        }
    }

    pub fn record(&mut self, time: f64, snapshot: PackedSnapshot) {
        self.snapshots.push((time, snapshot));
        self.snapshots.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
        self.current_time = time;
    }

    pub fn seek_to_time(&mut self, target_time: f64) -> Option<&PackedSnapshot> {
        let index = self.find_snapshot_at_time(target_time)?;
        self.current_time = self.snapshots[index].0;
        Some(&self.snapshots[index].1)
    }

    pub fn get_snapshot_at_time(&self, time: f64) -> Option<&PackedSnapshot> {
        let index = self.find_snapshot_at_time(time)?;
        Some(&self.snapshots[index].1)
    }

    pub fn get_current_snapshot(&self) -> Option<&PackedSnapshot> {
        self.get_snapshot_at_time(self.current_time)
    }

    pub fn get_earliest_time(&self) -> Option<f64> {
        self.snapshots.first().map(|(t, _)| *t)
    }

    pub fn get_latest_time(&self) -> Option<f64> {
        self.snapshots.last().map(|(t, _)| *t)
    }

    pub fn get_current_time(&self) -> f64 {
        self.current_time
    }

    pub fn fork_at_time(&self, time: f64) -> Option<PackedSnapshot> {
        self.get_snapshot_at_time(time).cloned()
    }

    pub fn prune_before(&mut self, time: f64) {
        self.snapshots.retain(|(t, _)| *t >= time);
    }

    pub fn prune_after(&mut self, time: f64) {
        self.snapshots.retain(|(t, _)| *t <= time);
    }

    pub fn clear(&mut self) {
        self.snapshots.clear();
        self.current_time = 0.0;
    }

    pub fn len(&self) -> usize {
        self.snapshots.len()
    }

    pub fn is_empty(&self) -> bool {
        self.snapshots.is_empty()
    }

    fn find_snapshot_at_time(&self, target_time: f64) -> Option<usize> {
        if self.snapshots.is_empty() {
            return None;
        }

        let mut left = 0;
        let mut right = self.snapshots.len();

        while left < right {
            let mid = (left + right) / 2;
            if self.snapshots[mid].0 < target_time {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        if left > 0 && (left >= self.snapshots.len() ||
           (self.snapshots[left].0 - target_time).abs() >
           (target_time - self.snapshots[left - 1].0).abs()) {
            Some(left - 1)
        } else if left < self.snapshots.len() {
            Some(left)
        } else if left > 0 {
            Some(left - 1)
        } else {
            None
        }
    }
}

impl Default for TimeTravel {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_replay_engine() {
        let mut engine = ReplayEngine::new();

        for i in 0..5 {
            let checkpoint = Checkpoint::new(format!("cp{}", i), PackedSnapshot::new());
            engine.add_checkpoint(checkpoint);
        }

        assert_eq!(engine.len(), 5);
        assert_eq!(engine.get_index(), 0);

        engine.next();
        assert_eq!(engine.get_index(), 1);

        engine.previous();
        assert_eq!(engine.get_index(), 0);

        engine.seek_to_end();
        assert_eq!(engine.get_index(), 4);
        assert!(engine.is_at_end());

        engine.seek_to_start();
        assert_eq!(engine.get_index(), 0);
        assert!(engine.is_at_start());
    }

    #[test]
    fn test_replay_loop() {
        let mut engine = ReplayEngine::new().with_loop(true);

        for i in 0..3 {
            let checkpoint = Checkpoint::new(format!("cp{}", i), PackedSnapshot::new());
            engine.add_checkpoint(checkpoint);
        }

        engine.seek_to_end();
        assert_eq!(engine.get_index(), 2);

        engine.next();
        assert_eq!(engine.get_index(), 0);

        engine.seek_to_start();
        assert_eq!(engine.get_index(), 0);

        engine.previous();
        assert_eq!(engine.get_index(), 2);
    }

    #[test]
    fn test_time_travel() {
        let mut tt = TimeTravel::new();

        for i in 0..10 {
            let snapshot = PackedSnapshot::new();
            tt.record(i as f64 * 10.0, snapshot);
        }

        assert_eq!(tt.len(), 10);
        assert_eq!(tt.get_earliest_time(), Some(0.0));
        assert_eq!(tt.get_latest_time(), Some(90.0));

        let snapshot = tt.seek_to_time(45.0);
        assert!(snapshot.is_some());
        assert_eq!(tt.get_current_time(), 50.0);

        tt.prune_before(30.0);
        assert_eq!(tt.len(), 7);
        assert_eq!(tt.get_earliest_time(), Some(30.0));

        tt.prune_after(70.0);
        assert_eq!(tt.len(), 5);
        assert_eq!(tt.get_latest_time(), Some(70.0));
    }

    #[test]
    fn test_time_travel_fork() {
        let mut tt = TimeTravel::new();

        for i in 0..5 {
            let snapshot = PackedSnapshot::new();
            tt.record(i as f64 * 10.0, snapshot);
        }

        let forked = tt.fork_at_time(20.0);
        assert!(forked.is_some());
    }
}