1use std::fs;
4use std::path::Path;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use chrono::TimeZone;
8use rkyv::rancor::Error as RkyvError;
9
10use nms_core::system::System;
11use nms_graph::{EdgeStrategy, GalaxyModel};
12
13use crate::data::{CacheData, CachedSystem};
14use crate::error::CacheError;
15
16pub fn extract_cache_data(model: &GalaxyModel, save_version: u32) -> CacheData {
18 let systems: Vec<CachedSystem> = model
19 .systems
20 .values()
21 .map(|system| CachedSystem {
22 address: system.address,
23 name: system.name.clone(),
24 discoverer: system.discoverer.clone(),
25 timestamp_secs: system.timestamp.map(|ts| ts.timestamp()),
26 planets: system.planets.clone(),
27 })
28 .collect();
29
30 let bases = model.bases.values().cloned().collect();
31
32 let cached_at = SystemTime::now()
33 .duration_since(UNIX_EPOCH)
34 .unwrap_or_default()
35 .as_secs();
36
37 CacheData {
38 systems,
39 bases,
40 player_state: model.player_state.clone(),
41 save_version,
42 cached_at,
43 }
44}
45
46pub fn serialize(data: &CacheData) -> Result<Vec<u8>, CacheError> {
48 rkyv::to_bytes::<RkyvError>(data)
49 .map(|v| v.to_vec())
50 .map_err(|e| CacheError::Serialize(e.to_string()))
51}
52
53pub fn deserialize(bytes: &[u8]) -> Result<CacheData, CacheError> {
55 rkyv::from_bytes::<CacheData, RkyvError>(bytes)
56 .map_err(|e| CacheError::Deserialize(e.to_string()))
57}
58
59pub fn write_cache(data: &CacheData, path: &Path) -> Result<(), CacheError> {
61 let bytes = serialize(data)?;
62
63 let tmp_path = path.with_extension("rkyv.tmp");
65 fs::write(&tmp_path, &bytes).map_err(CacheError::Io)?;
66 fs::rename(&tmp_path, path).map_err(CacheError::Io)?;
67
68 Ok(())
69}
70
71pub fn read_cache(path: &Path) -> Result<CacheData, CacheError> {
73 let bytes = fs::read(path).map_err(CacheError::Io)?;
74 deserialize(&bytes)
75}
76
77pub fn rebuild_model(data: &CacheData) -> GalaxyModel {
81 let mut model = GalaxyModel::new();
82
83 for cached in &data.systems {
84 let timestamp = cached
85 .timestamp_secs
86 .and_then(|secs| chrono::Utc.timestamp_opt(secs, 0).single());
87
88 let system = System::new(
89 cached.address,
90 cached.name.clone(),
91 cached.discoverer.clone(),
92 timestamp,
93 cached.planets.clone(),
94 );
95 model.insert_system(system);
96 }
97
98 for base in &data.bases {
99 model.insert_base(base.clone());
100 }
101
102 model.player_state = data.player_state.clone();
103 model.build_edges(EdgeStrategy::default());
104
105 model
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 fn test_model() -> GalaxyModel {
113 let json = r#"{
114 "Version": 4720, "Platform": "Mac|Final", "ActiveContext": "Main",
115 "CommonStateData": {"SaveName": "Test", "TotalPlayTime": 100},
116 "BaseContext": {
117 "GameMode": 1,
118 "PlayerStateData": {
119 "UniverseAddress": {"RealityIndex": 0, "GalacticAddress": {"VoxelX": 100, "VoxelY": 50, "VoxelZ": -200, "SolarSystemIndex": 42, "PlanetIndex": 0}},
120 "Units": 5000000, "Nanites": 10000, "Specials": 500,
121 "PersistentPlayerBases": [
122 {"BaseVersion": 8, "GalacticAddress": "0x050003AB8C07", "Position": [0.0,0.0,0.0], "Forward": [1.0,0.0,0.0], "LastUpdateTimestamp": 0, "Objects": [], "RID": "", "Owner": {"LID":"","UID":"1","USN":"","PTK":"ST","TS":0}, "Name": "Test Base", "BaseType": {"PersistentBaseTypes": "HomePlanetBase"}, "LastEditedById": "", "LastEditedByUsername": ""}
123 ]
124 }
125 },
126 "ExpeditionContext": {"GameMode": 6, "PlayerStateData": {"UniverseAddress": {"RealityIndex": 0, "GalacticAddress": {"VoxelX": 0, "VoxelY": 0, "VoxelZ": 0, "SolarSystemIndex": 0, "PlanetIndex": 0}}, "Units": 0, "Nanites": 0, "Specials": 0, "PersistentPlayerBases": []}},
127 "DiscoveryManagerData": {"DiscoveryData-v1": {"ReserveStore": 0, "ReserveManaged": 0, "Store": {"Record": [
128 {"DD": {"UA": "0x050003AB8C07", "DT": "SolarSystem", "VP": []}, "DM": {}, "OWS": {"LID":"","UID":"1","USN":"Explorer","PTK":"ST","TS":0}, "FL": {"U": 1}},
129 {"DD": {"UA": "0x150003AB8C07", "DT": "Planet", "VP": ["0xAB", 0]}, "DM": {}, "OWS": {"LID":"","UID":"1","USN":"Explorer","PTK":"ST","TS":0}, "FL": {"U": 1}},
130 {"DD": {"UA": "0x250003AB8C07", "DT": "Planet", "VP": ["0xCD", 1]}, "DM": {}, "OWS": {"LID":"","UID":"1","USN":"Explorer","PTK":"ST","TS":0}, "FL": {"U": 1}}
131 ]}}}
132 }"#;
133 let save = nms_save::parse_save(json.as_bytes()).unwrap();
134 GalaxyModel::from_save(&save)
135 }
136
137 #[test]
138 fn test_extract_cache_data() {
139 let model = test_model();
140 let data = extract_cache_data(&model, 4720);
141 assert_eq!(data.systems.len(), model.systems.len());
142 assert_eq!(data.bases.len(), model.bases.len());
143 assert_eq!(data.save_version, 4720);
144 assert!(data.cached_at > 0);
145 }
146
147 #[test]
148 fn test_serialize_deserialize_roundtrip() {
149 let model = test_model();
150 let data = extract_cache_data(&model, 4720);
151 let bytes = serialize(&data).unwrap();
152 let restored = deserialize(&bytes).unwrap();
153 assert_eq!(restored.systems.len(), data.systems.len());
154 assert_eq!(restored.bases.len(), data.bases.len());
155 }
156
157 #[test]
158 fn test_rebuild_model_preserves_counts() {
159 let model = test_model();
160 let data = extract_cache_data(&model, 4720);
161 let bytes = serialize(&data).unwrap();
162 let restored_data = deserialize(&bytes).unwrap();
163 let rebuilt = rebuild_model(&restored_data);
164
165 assert_eq!(rebuilt.systems.len(), model.systems.len());
166 assert_eq!(rebuilt.planets.len(), model.planets.len());
167 assert_eq!(rebuilt.bases.len(), model.bases.len());
168 }
169
170 #[test]
171 fn test_write_and_read_cache_file() {
172 let model = test_model();
173 let data = extract_cache_data(&model, 4720);
174
175 let dir = tempfile::tempdir().unwrap();
176 let path = dir.path().join("test.rkyv");
177
178 write_cache(&data, &path).unwrap();
179 assert!(path.exists());
180
181 let restored = read_cache(&path).unwrap();
182 assert_eq!(restored.systems.len(), data.systems.len());
183 }
184
185 #[test]
186 fn test_read_nonexistent_cache_errors() {
187 let result = read_cache(Path::new("/tmp/nonexistent_nms_cache.rkyv"));
188 assert!(result.is_err());
189 }
190
191 #[test]
192 fn test_rebuild_preserves_player_state() {
193 let model = test_model();
194 let data = extract_cache_data(&model, 4720);
195 let rebuilt = rebuild_model(&data);
196 assert!(rebuilt.player_state.is_some());
197 assert_eq!(
198 rebuilt.player_state.as_ref().unwrap().current_address,
199 model.player_state.as_ref().unwrap().current_address
200 );
201 }
202
203 #[test]
204 fn test_rebuild_preserves_base_lookup() {
205 let model = test_model();
206 let data = extract_cache_data(&model, 4720);
207 let rebuilt = rebuild_model(&data);
208 assert!(rebuilt.base("Test Base").is_some());
209 }
210}