Skip to main content

nms_save/
convert.rs

1//! Conversion from raw save model types to `nms-core` domain types.
2
3use chrono::DateTime;
4
5use crate::model::*;
6
7impl RawDiscoveryRecord {
8    /// Convert to an `nms_core::DiscoveryRecord`, or `None` if the discovery
9    /// type is unrecognized.
10    pub fn to_core_record(&self) -> Option<nms_core::DiscoveryRecord> {
11        let discovery_type = match self.dd.dt.as_str() {
12            "Planet" => nms_core::Discovery::Planet,
13            "SolarSystem" => nms_core::Discovery::SolarSystem,
14            "Sector" => nms_core::Discovery::Sector,
15            "Animal" => nms_core::Discovery::Animal,
16            "Flora" => nms_core::Discovery::Flora,
17            "Mineral" => nms_core::Discovery::Mineral,
18            _ => return None,
19        };
20
21        let timestamp = if self.ows.ts > 0 {
22            DateTime::from_timestamp(self.ows.ts as i64, 0)
23        } else {
24            None
25        };
26
27        let discoverer = if self.ows.usn.is_empty() {
28            None
29        } else {
30            Some(self.ows.usn.clone())
31        };
32
33        let is_uploaded = self.fl.uploaded.unwrap_or(0) > 0;
34
35        Some(nms_core::DiscoveryRecord::new(
36            discovery_type,
37            self.dd.ua.to_galactic_address(0),
38            timestamp,
39            None, // Discovery records in the save don't carry display names
40            discoverer,
41            is_uploaded,
42        ))
43    }
44}
45
46impl PersistentPlayerBase {
47    /// Convert to an `nms_core::PlayerBase`.
48    pub fn to_core_base(&self) -> nms_core::PlayerBase {
49        let base_type = match self.base_type.persistent_base_types.as_str() {
50            "HomePlanetBase" => nms_core::BaseType::HomePlanetBase,
51            "FreighterBase" => nms_core::BaseType::FreighterBase,
52            _ => nms_core::BaseType::ExternalPlanetBase,
53        };
54
55        nms_core::PlayerBase::new(
56            self.name.clone(),
57            base_type,
58            self.galactic_address.to_galactic_address(0),
59            self.position,
60            if self.owner.uid.is_empty() {
61                None
62            } else {
63                Some(self.owner.uid.clone())
64            },
65        )
66    }
67}
68
69impl SaveRoot {
70    /// Get `PlayerStateData` for the active context.
71    pub fn active_player_state(&self) -> &PlayerStateData {
72        match self.active_context.as_str() {
73            "Expedition" => &self.expedition_context.player_state_data,
74            _ => &self.base_context.player_state_data,
75        }
76    }
77
78    /// Convert active player state to `nms_core::PlayerState`.
79    pub fn to_core_player_state(&self) -> nms_core::PlayerState {
80        let ps = self.active_player_state();
81        let ua = &ps.universe_address;
82        let current_address = ua.galactic_address.to_galactic_address(ua.reality_index);
83
84        let prev_ua = &ps.previous_universe_address;
85        let previous_address = if prev_ua.galactic_address.voxel_x == 0
86            && prev_ua.galactic_address.voxel_y == 0
87            && prev_ua.galactic_address.voxel_z == 0
88            && prev_ua.galactic_address.solar_system_index == 0
89        {
90            None
91        } else {
92            Some(
93                prev_ua
94                    .galactic_address
95                    .to_galactic_address(prev_ua.reality_index),
96            )
97        };
98
99        nms_core::PlayerState::new(
100            current_address,
101            ua.reality_index,
102            previous_address,
103            None, // freighter_address not yet extracted
104            ps.units as u64,
105            ps.nanites as u64,
106            ps.specials as u64,
107        )
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn discovery_record_to_core() {
117        let raw = RawDiscoveryRecord {
118            dd: DiscoveryData {
119                ua: PackedGalacticAddress(0x513300F79B1D82),
120                dt: "Flora".into(),
121                vp: vec![],
122            },
123            dm: serde_json::Value::Object(serde_json::Map::new()),
124            ows: OwnershipData {
125                lid: String::new(),
126                uid: "12345".into(),
127                usn: "TestUser".into(),
128                ptk: "ST".into(),
129                ts: 1700000000,
130            },
131            fl: DiscoveryFlags {
132                created: Some(1),
133                uploaded: Some(1),
134            },
135            rid: Some("abc".into()),
136        };
137
138        let core = raw.to_core_record().unwrap();
139        assert_eq!(core.discovery_type, nms_core::Discovery::Flora);
140        assert_eq!(core.discoverer.as_deref(), Some("TestUser"));
141        assert!(core.is_uploaded);
142        assert!(core.timestamp.is_some());
143    }
144
145    #[test]
146    fn unknown_discovery_type_returns_none() {
147        let raw = RawDiscoveryRecord {
148            dd: DiscoveryData {
149                ua: PackedGalacticAddress(0),
150                dt: "UnknownType".into(),
151                vp: vec![],
152            },
153            dm: serde_json::Value::Null,
154            ows: OwnershipData::default(),
155            fl: DiscoveryFlags::default(),
156            rid: None,
157        };
158        assert!(raw.to_core_record().is_none());
159    }
160
161    #[test]
162    fn base_to_core() {
163        let base = PersistentPlayerBase {
164            base_version: 8,
165            galactic_address: PackedGalacticAddress(0x40050003AB8C07),
166            position: [100.0, 200.0, 300.0],
167            forward: [1.0, 0.0, 0.0],
168            last_update_timestamp: 1700000000,
169            objects: vec![],
170            rid: String::new(),
171            owner: OwnershipData {
172                lid: String::new(),
173                uid: "76561198025707979".into(),
174                usn: String::new(),
175                ptk: "ST".into(),
176                ts: 0,
177            },
178            name: "My Base".into(),
179            base_type: BaseTypeWrapper {
180                persistent_base_types: "HomePlanetBase".into(),
181            },
182            last_edited_by_id: String::new(),
183            last_edited_by_username: String::new(),
184            game_mode: None,
185        };
186
187        let core = base.to_core_base();
188        assert_eq!(core.name, "My Base");
189        assert_eq!(core.base_type, nms_core::BaseType::HomePlanetBase);
190        assert_eq!(core.owner_uid.as_deref(), Some("76561198025707979"));
191    }
192
193    #[test]
194    fn galactic_address_object_to_core() {
195        let obj = GalacticAddressObject {
196            voxel_x: 1699,
197            voxel_y: -2,
198            voxel_z: 165,
199            solar_system_index: 369,
200            planet_index: 0,
201        };
202        let addr = obj.to_galactic_address(0);
203        assert_eq!(addr.voxel_x(), 1699);
204        assert_eq!(addr.voxel_y(), -2);
205        assert_eq!(addr.voxel_z(), 165);
206        assert_eq!(addr.solar_system_index(), 369);
207        assert_eq!(addr.planet_index(), 0);
208        assert_eq!(addr.reality_index, 0);
209    }
210
211    #[test]
212    fn active_context_main() {
213        let json = r#"{
214            "Version": 4720,
215            "Platform": "Mac|Final",
216            "ActiveContext": "Main",
217            "CommonStateData": {"SaveName": "test"},
218            "BaseContext": {
219                "GameMode": 1,
220                "PlayerStateData": {"Units": 999}
221            },
222            "ExpeditionContext": {
223                "GameMode": 6,
224                "PlayerStateData": {"Units": 111}
225            },
226            "DiscoveryManagerData": {"DiscoveryData-v1": {"Store": {"Record": []}}}
227        }"#;
228        let save: SaveRoot = serde_json::from_str(json).unwrap();
229        assert_eq!(save.active_player_state().units, 999);
230    }
231
232    #[test]
233    fn active_context_expedition() {
234        let json = r#"{
235            "Version": 4720,
236            "Platform": "Mac|Final",
237            "ActiveContext": "Expedition",
238            "CommonStateData": {"SaveName": "test"},
239            "BaseContext": {
240                "GameMode": 1,
241                "PlayerStateData": {"Units": 999}
242            },
243            "ExpeditionContext": {
244                "GameMode": 6,
245                "PlayerStateData": {"Units": 111}
246            },
247            "DiscoveryManagerData": {"DiscoveryData-v1": {"Store": {"Record": []}}}
248        }"#;
249        let save: SaveRoot = serde_json::from_str(json).unwrap();
250        assert_eq!(save.active_player_state().units, 111);
251    }
252
253    #[test]
254    fn to_core_player_state_with_previous() {
255        let json = r#"{
256            "Version": 4720,
257            "Platform": "Mac|Final",
258            "ActiveContext": "Main",
259            "CommonStateData": {"SaveName": "test"},
260            "BaseContext": {
261                "GameMode": 1,
262                "PlayerStateData": {
263                    "UniverseAddress": {
264                        "RealityIndex": 0,
265                        "GalacticAddress": {"VoxelX": 100, "VoxelY": 10, "VoxelZ": 200, "SolarSystemIndex": 369, "PlanetIndex": 2}
266                    },
267                    "PreviousUniverseAddress": {
268                        "RealityIndex": 0,
269                        "GalacticAddress": {"VoxelX": 50, "VoxelY": 5, "VoxelZ": 100, "SolarSystemIndex": 505, "PlanetIndex": 0}
270                    },
271                    "Units": 1000000,
272                    "Nanites": 5000,
273                    "Specials": 200
274                }
275            },
276            "ExpeditionContext": {"GameMode": 6, "PlayerStateData": {}},
277            "DiscoveryManagerData": {"DiscoveryData-v1": {"Store": {"Record": []}}}
278        }"#;
279        let save: SaveRoot = serde_json::from_str(json).unwrap();
280        let state = save.to_core_player_state();
281        assert_eq!(state.units, 1000000);
282        assert_eq!(state.nanites, 5000);
283        assert_eq!(state.quicksilver, 200);
284        assert_eq!(state.current_address.voxel_x(), 100);
285        assert!(state.previous_address.is_some());
286        assert_eq!(state.previous_address.unwrap().voxel_x(), 50);
287    }
288
289    #[test]
290    fn to_core_player_state_zero_previous_is_none() {
291        let json = r#"{
292            "Version": 4720,
293            "Platform": "Mac|Final",
294            "ActiveContext": "Main",
295            "CommonStateData": {},
296            "BaseContext": {
297                "GameMode": 1,
298                "PlayerStateData": {
299                    "UniverseAddress": {
300                        "RealityIndex": 0,
301                        "GalacticAddress": {"VoxelX": 100, "VoxelY": 10, "VoxelZ": 200, "SolarSystemIndex": 369, "PlanetIndex": 0}
302                    },
303                    "PreviousUniverseAddress": {
304                        "RealityIndex": 0,
305                        "GalacticAddress": {"VoxelX": 0, "VoxelY": 0, "VoxelZ": 0, "SolarSystemIndex": 0, "PlanetIndex": 0}
306                    },
307                    "Units": 500
308                }
309            },
310            "ExpeditionContext": {"GameMode": 6, "PlayerStateData": {}},
311            "DiscoveryManagerData": {"DiscoveryData-v1": {"Store": {"Record": []}}}
312        }"#;
313        let save: SaveRoot = serde_json::from_str(json).unwrap();
314        let state = save.to_core_player_state();
315        assert!(state.previous_address.is_none());
316    }
317}