1use std::collections::HashMap;
4
5use nms_core::address::GalacticAddress;
6use nms_core::biome::Biome;
7use nms_core::system::{Planet, System};
8use nms_save::model::SaveRoot;
9
10use crate::spatial::SystemId;
11
12#[derive(Debug)]
14struct SystemBuilder {
15 address: GalacticAddress,
16 name: Option<String>,
17 discoverer: Option<String>,
18 timestamp: Option<chrono::DateTime<chrono::Utc>>,
19 planets: Vec<Planet>,
20}
21
22pub fn extract_biome_from_vp(vp: &[serde_json::Value]) -> (Option<Biome>, bool) {
33 if vp.len() < 2 {
34 return (None, false);
35 }
36
37 let flags = match &vp[1] {
39 serde_json::Value::Number(n) => n.as_u64(),
40 serde_json::Value::String(s) => {
41 let hex = s
42 .strip_prefix("0x")
43 .or_else(|| s.strip_prefix("0X"))
44 .unwrap_or(s);
45 u64::from_str_radix(hex, 16).ok()
46 }
47 _ => None,
48 };
49
50 let Some(flags) = flags else {
51 return (None, false);
52 };
53
54 let infested = (flags >> 16) & 1 == 1;
55
56 let biome_index = (flags & 0xFFFF) as u16;
59 let biome = match biome_index {
60 0 => Some(Biome::Lush),
61 1 => Some(Biome::Toxic),
62 2 => Some(Biome::Scorched),
63 3 => Some(Biome::Radioactive),
64 4 => Some(Biome::Frozen),
65 5 => Some(Biome::Barren),
66 6 => Some(Biome::Dead),
67 7 => Some(Biome::Weird),
68 8 => Some(Biome::Red),
69 9 => Some(Biome::Green),
70 10 => Some(Biome::Blue),
71 11 => None, 12 => Some(Biome::Swamp),
73 13 => Some(Biome::Lava),
74 14 => Some(Biome::Waterworld),
75 15 => Some(Biome::GasGiant),
76 _ => None,
77 };
78
79 (biome, infested)
80}
81
82pub fn extract_seed_from_vp(vp: &[serde_json::Value]) -> Option<u64> {
84 if vp.is_empty() {
85 return None;
86 }
87 match &vp[0] {
88 serde_json::Value::Number(n) => n.as_u64(),
89 serde_json::Value::String(s) => {
90 let hex = s
91 .strip_prefix("0x")
92 .or_else(|| s.strip_prefix("0X"))
93 .unwrap_or(s);
94 u64::from_str_radix(hex, 16).ok()
95 }
96 _ => None,
97 }
98}
99
100pub fn extract_systems(save: &SaveRoot) -> HashMap<SystemId, System> {
105 let records = &save.discovery_manager_data.discovery_data_v1.store.record;
106 let mut builders: HashMap<SystemId, SystemBuilder> = HashMap::new();
107
108 for rec in records {
110 if rec.dd.dt != "SolarSystem" {
111 continue;
112 }
113 let addr = GalacticAddress::from_packed(rec.dd.ua.0, 0);
114 let sys_id = SystemId::from_address(&addr);
115
116 let timestamp = if rec.ows.ts > 0 {
117 chrono::DateTime::from_timestamp(rec.ows.ts as i64, 0)
118 } else {
119 None
120 };
121
122 let discoverer = if rec.ows.usn.is_empty() {
123 None
124 } else {
125 Some(rec.ows.usn.clone())
126 };
127
128 builders.entry(sys_id).or_insert_with(|| SystemBuilder {
129 address: addr,
130 name: None, discoverer,
132 timestamp,
133 planets: Vec::new(),
134 });
135 }
136
137 for rec in records {
139 if rec.dd.dt != "Planet" {
140 continue;
141 }
142 let addr = GalacticAddress::from_packed(rec.dd.ua.0, 0);
143 let sys_id = SystemId::from_address(&addr);
144 let planet_index = addr.planet_index();
145
146 let (biome, infested) = extract_biome_from_vp(&rec.dd.vp);
147 let seed_hash = extract_seed_from_vp(&rec.dd.vp);
148
149 let planet = Planet::new(
150 planet_index,
151 biome,
152 None, infested,
154 None, seed_hash,
156 );
157
158 let builder = builders.entry(sys_id).or_insert_with(|| SystemBuilder {
159 address: addr,
160 name: None,
161 discoverer: None,
162 timestamp: None,
163 planets: Vec::new(),
164 });
165
166 if !builder.planets.iter().any(|p| p.index == planet_index) {
168 builder.planets.push(planet);
169 }
170 }
171
172 builders
174 .into_iter()
175 .map(|(id, b)| {
176 let system = System::new(b.address, b.name, b.discoverer, b.timestamp, b.planets);
177 (id, system)
178 })
179 .collect()
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_extract_biome_from_vp_empty() {
188 assert_eq!(extract_biome_from_vp(&[]), (None, false));
189 }
190
191 #[test]
192 fn test_extract_biome_from_vp_single_element() {
193 let vp = vec![serde_json::json!("0xABCD")];
194 assert_eq!(extract_biome_from_vp(&vp), (None, false));
195 }
196
197 #[test]
198 fn test_extract_biome_lush_not_infested() {
199 let vp = vec![serde_json::json!("0xABCD"), serde_json::json!(0)];
200 assert_eq!(extract_biome_from_vp(&vp), (Some(Biome::Lush), false));
201 }
202
203 #[test]
204 fn test_extract_biome_toxic_infested() {
205 let flags = (1u64 << 16) | 1;
207 let vp = vec![serde_json::json!("0xABCD"), serde_json::json!(flags)];
208 assert_eq!(extract_biome_from_vp(&vp), (Some(Biome::Toxic), true));
209 }
210
211 #[test]
212 fn test_extract_biome_from_hex_string() {
213 let vp = vec![serde_json::json!("0xABCD"), serde_json::json!("0x10005")];
215 assert_eq!(extract_biome_from_vp(&vp), (Some(Biome::Barren), true));
216 }
217
218 #[test]
219 fn test_extract_biome_unknown_index() {
220 let vp = vec![serde_json::json!("0xABCD"), serde_json::json!(255)];
221 assert_eq!(extract_biome_from_vp(&vp), (None, false));
222 }
223
224 #[test]
225 fn test_extract_biome_test_index_skipped() {
226 let vp = vec![serde_json::json!("0xABCD"), serde_json::json!(11)];
228 assert_eq!(extract_biome_from_vp(&vp), (None, false));
229 }
230
231 #[test]
232 fn test_extract_biome_all_valid_indices() {
233 let expected = [
234 (0, Some(Biome::Lush)),
235 (1, Some(Biome::Toxic)),
236 (2, Some(Biome::Scorched)),
237 (3, Some(Biome::Radioactive)),
238 (4, Some(Biome::Frozen)),
239 (5, Some(Biome::Barren)),
240 (6, Some(Biome::Dead)),
241 (7, Some(Biome::Weird)),
242 (8, Some(Biome::Red)),
243 (9, Some(Biome::Green)),
244 (10, Some(Biome::Blue)),
245 (12, Some(Biome::Swamp)),
246 (13, Some(Biome::Lava)),
247 (14, Some(Biome::Waterworld)),
248 (15, Some(Biome::GasGiant)),
249 ];
250 for (idx, biome) in expected {
251 let vp = vec![serde_json::json!("0x0"), serde_json::json!(idx)];
252 assert_eq!(
253 extract_biome_from_vp(&vp),
254 (biome, false),
255 "Failed for biome index {idx}"
256 );
257 }
258 }
259
260 #[test]
261 fn test_extract_biome_vp1_not_number_or_string() {
262 let vp = vec![serde_json::json!("0xABCD"), serde_json::json!(true)];
263 assert_eq!(extract_biome_from_vp(&vp), (None, false));
264 }
265
266 #[test]
267 fn test_extract_seed_from_vp_hex() {
268 let vp = vec![serde_json::json!("0xD6911E7B1D31085E")];
269 assert_eq!(extract_seed_from_vp(&vp), Some(0xD6911E7B1D31085E));
270 }
271
272 #[test]
273 fn test_extract_seed_from_vp_integer() {
274 let vp = vec![serde_json::json!(12345)];
275 assert_eq!(extract_seed_from_vp(&vp), Some(12345));
276 }
277
278 #[test]
279 fn test_extract_seed_from_vp_empty() {
280 assert_eq!(extract_seed_from_vp(&[]), None);
281 }
282
283 #[test]
284 fn test_extract_seed_from_vp_not_number_or_string() {
285 let vp = vec![serde_json::json!(null)];
286 assert_eq!(extract_seed_from_vp(&vp), None);
287 }
288}