Skip to main content

nms_core/
delta.rs

1//! Delta types describing changes between two save file snapshots.
2//!
3//! These types live in nms-core so that both nms-graph (apply) and nms-watch
4//! (produce) can use them without creating a dependency cycle.
5
6use crate::address::GalacticAddress;
7use crate::player::PlayerBase;
8use crate::system::{Planet, System, SystemId};
9
10/// A typed description of what changed between two save file snapshots.
11#[derive(Debug, Clone)]
12pub struct SaveDelta {
13    /// Newly discovered systems (not in previous snapshot).
14    pub new_systems: Vec<System>,
15    /// Newly discovered planets (not in previous snapshot).
16    pub new_planets: Vec<(SystemId, Planet)>,
17    /// Player moved to a new position.
18    pub player_moved: Option<PlayerMoved>,
19    /// Newly placed bases.
20    pub new_bases: Vec<PlayerBase>,
21    /// Modified bases (name exists but content differs).
22    pub modified_bases: Vec<PlayerBase>,
23}
24
25/// Player position change.
26#[derive(Debug, Clone)]
27pub struct PlayerMoved {
28    pub from: GalacticAddress,
29    pub to: GalacticAddress,
30}
31
32impl SaveDelta {
33    /// Create an empty delta (no changes).
34    pub fn empty() -> Self {
35        Self {
36            new_systems: Vec::new(),
37            new_planets: Vec::new(),
38            player_moved: None,
39            new_bases: Vec::new(),
40            modified_bases: Vec::new(),
41        }
42    }
43
44    /// Returns true if no changes were detected.
45    pub fn is_empty(&self) -> bool {
46        self.new_systems.is_empty()
47            && self.new_planets.is_empty()
48            && self.player_moved.is_none()
49            && self.new_bases.is_empty()
50            && self.modified_bases.is_empty()
51    }
52
53    /// Total number of individual changes.
54    pub fn change_count(&self) -> usize {
55        self.new_systems.len()
56            + self.new_planets.len()
57            + self.player_moved.as_ref().map_or(0, |_| 1)
58            + self.new_bases.len()
59            + self.modified_bases.len()
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn test_empty_delta_is_empty() {
69        let delta = SaveDelta::empty();
70        assert!(delta.is_empty());
71        assert_eq!(delta.change_count(), 0);
72    }
73
74    #[test]
75    fn test_delta_with_system_is_not_empty() {
76        let addr = GalacticAddress::new(100, 50, -200, 0x42, 0, 0);
77        let system = System::new(addr, Some("Test".into()), None, None, vec![]);
78        let delta = SaveDelta {
79            new_systems: vec![system],
80            ..SaveDelta::empty()
81        };
82        assert!(!delta.is_empty());
83        assert_eq!(delta.change_count(), 1);
84    }
85
86    #[test]
87    fn test_delta_change_count_sums_all() {
88        let addr = GalacticAddress::new(100, 50, -200, 0x42, 0, 0);
89        let system = System::new(addr, None, None, None, vec![]);
90        let sys_id = SystemId::from_address(&addr);
91        let planet = Planet::new(0, None, None, false, None, None);
92        let base = PlayerBase::new(
93            "Base".into(),
94            crate::player::BaseType::HomePlanetBase,
95            addr,
96            [0.0, 0.0, 0.0],
97            None,
98        );
99
100        let delta = SaveDelta {
101            new_systems: vec![system],
102            new_planets: vec![(sys_id, planet)],
103            player_moved: Some(PlayerMoved {
104                from: addr,
105                to: addr,
106            }),
107            new_bases: vec![base.clone()],
108            modified_bases: vec![base],
109        };
110        assert_eq!(delta.change_count(), 5);
111    }
112}