1use std::collections::HashMap;
4
5use petgraph::Undirected;
6use petgraph::stable_graph::{NodeIndex, StableGraph};
7use rstar::RTree;
8
9use nms_core::address::GalacticAddress;
10use nms_core::biome::Biome;
11use nms_core::delta::SaveDelta;
12use nms_core::player::{PlayerBase, PlayerState};
13use nms_core::system::{Planet, System};
14use nms_save::model::SaveRoot;
15
16use crate::extract::extract_systems;
17use crate::spatial::{SystemId, SystemPoint};
18
19pub type PlanetKey = (SystemId, u8);
21
22#[derive(Debug)]
29pub struct GalaxyModel {
30 pub graph: StableGraph<SystemId, f64, Undirected>,
32
33 pub spatial: HashMap<u8, RTree<SystemPoint>>,
35
36 pub active_galaxy: u8,
38
39 pub systems: HashMap<SystemId, System>,
41
42 pub planets: HashMap<PlanetKey, Planet>,
44
45 pub bases: HashMap<String, PlayerBase>,
47
48 pub biome_index: HashMap<Biome, Vec<PlanetKey>>,
50
51 pub name_index: HashMap<String, SystemId>,
53
54 pub address_to_id: HashMap<u64, SystemId>,
56
57 pub node_map: HashMap<SystemId, NodeIndex>,
59
60 pub player_state: Option<PlayerState>,
62}
63
64impl Default for GalaxyModel {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl GalaxyModel {
71 pub fn new() -> Self {
73 Self {
74 graph: StableGraph::default(),
75 spatial: HashMap::new(),
76 active_galaxy: 0,
77 systems: HashMap::new(),
78 planets: HashMap::new(),
79 bases: HashMap::new(),
80 biome_index: HashMap::new(),
81 name_index: HashMap::new(),
82 address_to_id: HashMap::new(),
83 node_map: HashMap::new(),
84 player_state: None,
85 }
86 }
87
88 pub fn from_save(save: &SaveRoot) -> Self {
90 let extracted = extract_systems(save);
91
92 let mut graph: StableGraph<SystemId, f64, Undirected> = StableGraph::default();
93 let mut galaxy_points: HashMap<u8, Vec<SystemPoint>> = HashMap::new();
94 let mut systems = HashMap::with_capacity(extracted.len());
95 let mut planets = HashMap::new();
96 let mut biome_index: HashMap<Biome, Vec<PlanetKey>> = HashMap::new();
97 let mut name_index = HashMap::new();
98 let mut address_to_id = HashMap::new();
99 let mut node_map = HashMap::new();
100
101 for (sys_id, system) in extracted {
102 let node_idx = graph.add_node(sys_id);
104 node_map.insert(sys_id, node_idx);
105
106 let point = SystemPoint::from_address(&system.address);
108 let galaxy = system.address.reality_index;
109 galaxy_points.entry(galaxy).or_default().push(point);
110
111 address_to_id.insert(sys_id.0, sys_id);
113
114 if let Some(ref name) = system.name {
116 name_index.insert(name.to_lowercase(), sys_id);
117 }
118
119 for planet in &system.planets {
121 let key = (sys_id, planet.index);
122 if let Some(biome) = planet.biome {
123 biome_index.entry(biome).or_default().push(key);
124 }
125 planets.insert(key, planet.clone());
126 }
127
128 systems.insert(sys_id, system);
129 }
130
131 let spatial: HashMap<u8, RTree<SystemPoint>> = galaxy_points
133 .into_iter()
134 .map(|(galaxy, points)| (galaxy, RTree::bulk_load(points)))
135 .collect();
136
137 let player_state = Some(save.to_core_player_state());
139 let active_galaxy = save.active_player_state().universe_address.reality_index;
140
141 let ps = save.active_player_state();
143 let mut bases = HashMap::new();
144 for base in &ps.persistent_player_bases {
145 let core_base = base.to_core_base();
146 if !core_base.name.is_empty() {
147 bases.insert(core_base.name.to_lowercase(), core_base);
148 }
149 }
150
151 let mut model = Self {
152 graph,
153 spatial,
154 active_galaxy,
155 systems,
156 planets,
157 bases,
158 biome_index,
159 name_index,
160 address_to_id,
161 node_map,
162 player_state,
163 };
164
165 model.build_edges(crate::edges::EdgeStrategy::default());
166 model
167 }
168
169 pub fn system_count(&self) -> usize {
171 self.systems.len()
172 }
173
174 pub fn planet_count(&self) -> usize {
176 self.planets.len()
177 }
178
179 pub fn base_count(&self) -> usize {
181 self.bases.len()
182 }
183
184 pub fn system(&self, id: &SystemId) -> Option<&System> {
186 self.systems.get(id)
187 }
188
189 pub fn system_by_name(&self, name: &str) -> Option<(&SystemId, &System)> {
191 self.name_index
192 .get(&name.to_lowercase())
193 .and_then(|id| self.systems.get(id).map(|s| (id, s)))
194 }
195
196 pub fn base(&self, name: &str) -> Option<&PlayerBase> {
198 self.bases.get(&name.to_lowercase())
199 }
200
201 pub fn player_position(&self) -> Option<&GalacticAddress> {
203 self.player_state.as_ref().map(|ps| &ps.current_address)
204 }
205
206 pub fn active_spatial(&self) -> Option<&RTree<SystemPoint>> {
208 self.spatial.get(&self.active_galaxy)
209 }
210
211 pub fn spatial_for(&self, galaxy: u8) -> Option<&RTree<SystemPoint>> {
213 self.spatial.get(&galaxy)
214 }
215
216 pub fn set_active_galaxy(&mut self, galaxy: u8) {
218 self.active_galaxy = galaxy;
219 }
220
221 pub fn discovered_galaxies(&self) -> Vec<u8> {
223 let mut galaxies: Vec<u8> = self.spatial.keys().copied().collect();
224 galaxies.sort();
225 galaxies
226 }
227
228 pub fn rebuild_spatial(&mut self) {
230 let mut galaxy_points: HashMap<u8, Vec<SystemPoint>> = HashMap::new();
231 for system in self.systems.values() {
232 let point = SystemPoint::from_address(&system.address);
233 let galaxy = system.address.reality_index;
234 galaxy_points.entry(galaxy).or_default().push(point);
235 }
236 self.spatial = galaxy_points
237 .into_iter()
238 .map(|(galaxy, points)| (galaxy, RTree::bulk_load(points)))
239 .collect();
240 }
241
242 pub fn spatial_size(&self) -> usize {
244 self.spatial.values().map(|t| t.size()).sum()
245 }
246
247 pub fn planets_by_biome(&self, biome: Biome) -> Vec<&Planet> {
249 self.biome_index
250 .get(&biome)
251 .map(|keys| keys.iter().filter_map(|k| self.planets.get(k)).collect())
252 .unwrap_or_default()
253 }
254
255 pub fn insert_system(&mut self, system: System) {
257 let sys_id = SystemId::from_address(&system.address);
258
259 if self.systems.contains_key(&sys_id) {
260 return; }
262
263 let node_idx = self.graph.add_node(sys_id);
264 self.node_map.insert(sys_id, node_idx);
265
266 let point = SystemPoint::from_address(&system.address);
267 let galaxy = system.address.reality_index;
268 self.spatial.entry(galaxy).or_default().insert(point);
269
270 self.address_to_id.insert(sys_id.0, sys_id);
271
272 if let Some(ref name) = system.name {
273 self.name_index.insert(name.to_lowercase(), sys_id);
274 }
275
276 for planet in &system.planets {
277 let key = (sys_id, planet.index);
278 if let Some(biome) = planet.biome {
279 self.biome_index.entry(biome).or_default().push(key);
280 }
281 self.planets.insert(key, planet.clone());
282 }
283
284 self.systems.insert(sys_id, system);
285 }
286
287 pub fn ensure_player_system(&mut self) {
293 if let Some(ref ps) = self.player_state {
294 let player_addr = ps.current_address;
295 let player_sys_id = SystemId::from_address(&player_addr);
296 if !self.systems.contains_key(&player_sys_id) {
297 let sentinel = System::new(player_addr, None, None, None, vec![]);
298 self.insert_system(sentinel);
299 }
300 }
301 }
302
303 pub fn insert_base(&mut self, base: PlayerBase) {
305 if !base.name.is_empty() {
306 self.bases.insert(base.name.to_lowercase(), base);
307 }
308 }
309
310 pub fn apply_delta(&mut self, delta: &SaveDelta) {
316 for system in &delta.new_systems {
318 self.insert_system(system.clone());
319 }
320
321 for (sys_id, planet) in &delta.new_planets {
323 let key = (*sys_id, planet.index);
324 if !self.planets.contains_key(&key) {
325 if let Some(biome) = planet.biome {
326 self.biome_index.entry(biome).or_default().push(key);
327 }
328 self.planets.insert(key, planet.clone());
329
330 if let Some(system) = self.systems.get_mut(sys_id)
332 && !system.planets.iter().any(|p| p.index == planet.index)
333 {
334 system.planets.push(planet.clone());
335 }
336 }
337 }
338
339 if let Some(ref moved) = delta.player_moved
341 && let Some(ref mut ps) = self.player_state
342 {
343 ps.current_address = moved.to;
344 }
345
346 for base in &delta.new_bases {
348 self.insert_base(base.clone());
349 }
350
351 for base in &delta.modified_bases {
353 self.insert_base(base.clone());
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 fn minimal_save() -> SaveRoot {
364 let json = r#"{
365 "Version": 4720,
366 "Platform": "Mac|Final",
367 "ActiveContext": "Main",
368 "CommonStateData": {"SaveName": "Test", "TotalPlayTime": 100},
369 "BaseContext": {
370 "GameMode": 1,
371 "PlayerStateData": {
372 "UniverseAddress": {"RealityIndex": 0, "GalacticAddress": {"VoxelX": 100, "VoxelY": 50, "VoxelZ": -200, "SolarSystemIndex": 42, "PlanetIndex": 0}},
373 "Units": 1000000, "Nanites": 5000, "Specials": 200,
374 "PersistentPlayerBases": [
375 {
376 "BaseVersion": 8, "GalacticAddress": "0x050003AB8C07",
377 "Position": [0.0, 0.0, 0.0], "Forward": [1.0, 0.0, 0.0],
378 "LastUpdateTimestamp": 1700000000, "Objects": [], "RID": "",
379 "Owner": {"LID": "", "UID": "123", "USN": "Test", "PTK": "ST", "TS": 0},
380 "Name": "Home Base",
381 "BaseType": {"PersistentBaseTypes": "HomePlanetBase"},
382 "LastEditedById": "", "LastEditedByUsername": ""
383 }
384 ]
385 }
386 },
387 "ExpeditionContext": {
388 "GameMode": 6,
389 "PlayerStateData": {
390 "UniverseAddress": {"RealityIndex": 0, "GalacticAddress": {"VoxelX": 0, "VoxelY": 0, "VoxelZ": 0, "SolarSystemIndex": 0, "PlanetIndex": 0}},
391 "Units": 0, "Nanites": 0, "Specials": 0,
392 "PersistentPlayerBases": []
393 }
394 },
395 "DiscoveryManagerData": {
396 "DiscoveryData-v1": {
397 "ReserveStore": 100, "ReserveManaged": 100,
398 "Store": {
399 "Record": [
400 {"DD": {"UA": "0x002A32F38064", "DT": "SolarSystem", "VP": ["0xAAAA"]}, "DM": {}, "OWS": {"LID": "", "UID": "1", "USN": "Explorer", "PTK": "ST", "TS": 1700000000}, "FL": {"U": 1}},
401 {"DD": {"UA": "0x050003AB8C07", "DT": "SolarSystem", "VP": ["0xABCD"]}, "DM": {}, "OWS": {"LID": "", "UID": "1", "USN": "Explorer", "PTK": "ST", "TS": 1700000000}, "FL": {"U": 1}},
402 {"DD": {"UA": "0x150003AB8C07", "DT": "Planet", "VP": ["0xDEAD", 0]}, "DM": {}, "OWS": {"LID": "", "UID": "1", "USN": "Explorer", "PTK": "ST", "TS": 1700000000}, "FL": {"U": 1}},
403 {"DD": {"UA": "0x0A0002001234", "DT": "SolarSystem", "VP": ["0x1234"]}, "DM": {}, "OWS": {"LID": "", "UID": "1", "USN": "Explorer", "PTK": "ST", "TS": 1700000000}, "FL": {"U": 1}}
404 ]
405 }
406 }
407 }
408 }"#;
409 nms_save::parse_save(json.as_bytes()).unwrap()
410 }
411
412 #[test]
413 fn test_from_save_basic_counts() {
414 let save = minimal_save();
415 let model = GalaxyModel::from_save(&save);
416 assert_eq!(model.system_count(), 3);
418 assert_eq!(model.planet_count(), 1);
419 assert_eq!(model.base_count(), 1);
420 }
421
422 #[test]
423 fn test_from_save_base_lookup() {
424 let save = minimal_save();
425 let model = GalaxyModel::from_save(&save);
426 let base = model.base("Home Base").unwrap();
427 assert_eq!(base.name, "Home Base");
428 }
429
430 #[test]
431 fn test_from_save_base_lookup_case_insensitive() {
432 let save = minimal_save();
433 let model = GalaxyModel::from_save(&save);
434 assert!(model.base("home base").is_some());
435 assert!(model.base("HOME BASE").is_some());
436 }
437
438 #[test]
439 fn test_from_save_player_position() {
440 let save = minimal_save();
441 let model = GalaxyModel::from_save(&save);
442 let pos = model.player_position().unwrap();
443 assert_eq!(pos.voxel_x(), 100);
444 assert_eq!(pos.voxel_y(), 50);
445 assert_eq!(pos.voxel_z(), -200);
446 }
447
448 #[test]
449 fn test_from_save_spatial_index_populated() {
450 let save = minimal_save();
451 let model = GalaxyModel::from_save(&save);
452 assert_eq!(model.spatial_size(), 3);
454 }
455
456 #[test]
457 fn test_from_save_graph_nodes() {
458 let save = minimal_save();
459 let model = GalaxyModel::from_save(&save);
460 assert_eq!(model.graph.node_count(), 3);
462 }
463
464 #[test]
465 fn test_from_save_biome_index() {
466 let save = minimal_save();
467 let model = GalaxyModel::from_save(&save);
468 let lush = model.planets_by_biome(Biome::Lush);
469 assert_eq!(lush.len(), 1);
470 }
471
472 #[test]
473 fn test_from_save_biome_index_empty() {
474 let save = minimal_save();
475 let model = GalaxyModel::from_save(&save);
476 let toxic = model.planets_by_biome(Biome::Toxic);
477 assert!(toxic.is_empty());
478 }
479
480 #[test]
481 fn test_insert_system_adds_to_all_indexes() {
482 let save = minimal_save();
483 let mut model = GalaxyModel::from_save(&save);
484 let count_before = model.system_count();
485
486 let addr = GalacticAddress::new(500, 10, -300, 0x999, 0, 0);
487 let system = System::new(
488 addr,
489 Some("New System".to_string()),
490 None,
491 None,
492 vec![Planet::new(0, Some(Biome::Lava), None, false, None, None)],
493 );
494 model.insert_system(system);
495
496 assert_eq!(model.system_count(), count_before + 1);
497 assert!(model.system_by_name("New System").is_some());
498 assert_eq!(model.spatial_size(), count_before + 1);
499 assert_eq!(model.graph.node_count(), count_before + 1);
500 assert_eq!(model.planets_by_biome(Biome::Lava).len(), 1);
501 }
502
503 #[test]
504 fn test_insert_duplicate_system_is_noop() {
505 let save = minimal_save();
506 let mut model = GalaxyModel::from_save(&save);
507 let count_before = model.system_count();
508
509 let existing_id = *model.systems.keys().next().unwrap();
511 let existing = model.systems.get(&existing_id).unwrap().clone();
512 model.insert_system(existing);
513
514 assert_eq!(model.system_count(), count_before);
515 }
516
517 #[test]
518 fn test_system_not_found_returns_none() {
519 let save = minimal_save();
520 let model = GalaxyModel::from_save(&save);
521 assert!(model.system(&SystemId(0xDEADBEEF)).is_none());
522 }
523
524 #[test]
525 fn test_system_by_name_not_found() {
526 let save = minimal_save();
527 let model = GalaxyModel::from_save(&save);
528 assert!(model.system_by_name("No Such System").is_none());
529 }
530
531 #[test]
532 fn test_base_not_found() {
533 let save = minimal_save();
534 let model = GalaxyModel::from_save(&save);
535 assert!(model.base("No Such Base").is_none());
536 }
537
538 #[test]
539 fn test_from_save_address_to_id() {
540 let save = minimal_save();
541 let model = GalaxyModel::from_save(&save);
542 for &sys_id in model.systems.keys() {
544 assert!(model.address_to_id.contains_key(&sys_id.0));
545 }
546 }
547
548 #[test]
549 fn test_from_save_node_map() {
550 let save = minimal_save();
551 let model = GalaxyModel::from_save(&save);
552 for &sys_id in model.systems.keys() {
554 assert!(model.node_map.contains_key(&sys_id));
555 }
556 }
557
558 #[test]
559 fn test_apply_delta_empty_is_noop() {
560 let save = minimal_save();
561 let mut model = GalaxyModel::from_save(&save);
562 let sys_count = model.system_count();
563 let planet_count = model.planet_count();
564 let base_count = model.base_count();
565
566 model.apply_delta(&nms_core::delta::SaveDelta::empty());
567
568 assert_eq!(model.system_count(), sys_count);
569 assert_eq!(model.planet_count(), planet_count);
570 assert_eq!(model.base_count(), base_count);
571 }
572
573 #[test]
574 fn test_apply_delta_new_system() {
575 let save = minimal_save();
576 let mut model = GalaxyModel::from_save(&save);
577 let count_before = model.system_count();
578
579 let delta = nms_core::delta::SaveDelta {
580 new_systems: vec![System::new(
581 GalacticAddress::new(500, 10, -300, 0x999, 0, 0),
582 Some("Delta System".into()),
583 None,
584 None,
585 vec![Planet::new(0, Some(Biome::Lava), None, false, None, None)],
586 )],
587 ..nms_core::delta::SaveDelta::empty()
588 };
589
590 model.apply_delta(&delta);
591 assert_eq!(model.system_count(), count_before + 1);
592 assert!(model.system_by_name("Delta System").is_some());
593 }
594
595 #[test]
596 fn test_apply_delta_player_moved() {
597 let save = minimal_save();
598 let mut model = GalaxyModel::from_save(&save);
599 let old_pos = *model.player_position().unwrap();
600 let new_addr = GalacticAddress::new(999, 0, 0, 1, 0, 0);
601
602 let delta = nms_core::delta::SaveDelta {
603 player_moved: Some(nms_core::delta::PlayerMoved {
604 from: old_pos,
605 to: new_addr,
606 }),
607 ..nms_core::delta::SaveDelta::empty()
608 };
609
610 model.apply_delta(&delta);
611 assert_eq!(model.player_position().unwrap().voxel_x(), 999);
612 }
613
614 #[test]
615 fn test_apply_delta_new_base() {
616 let save = minimal_save();
617 let mut model = GalaxyModel::from_save(&save);
618 let base_count_before = model.base_count();
619
620 let addr = GalacticAddress::new(100, 50, -200, 42, 0, 0);
621 let base = nms_core::player::PlayerBase::new(
622 "Delta Base".into(),
623 nms_core::player::BaseType::HomePlanetBase,
624 addr,
625 [0.0, 0.0, 0.0],
626 None,
627 );
628
629 let delta = nms_core::delta::SaveDelta {
630 new_bases: vec![base],
631 ..nms_core::delta::SaveDelta::empty()
632 };
633
634 model.apply_delta(&delta);
635 assert_eq!(model.base_count(), base_count_before + 1);
636 assert!(model.base("Delta Base").is_some());
637 }
638
639 #[test]
640 fn test_apply_delta_new_planet_to_existing_system() {
641 let save = minimal_save();
642 let mut model = GalaxyModel::from_save(&save);
643 let planet_count_before = model.planet_count();
644
645 let sys_id = *model.systems.keys().next().unwrap();
646 let planet = Planet::new(7, Some(Biome::Frozen), None, false, None, None);
647
648 let delta = nms_core::delta::SaveDelta {
649 new_planets: vec![(sys_id, planet)],
650 ..nms_core::delta::SaveDelta::empty()
651 };
652
653 model.apply_delta(&delta);
654 assert_eq!(model.planet_count(), planet_count_before + 1);
655 assert_eq!(model.planets_by_biome(Biome::Frozen).len(), 1);
656 }
657
658 #[test]
659 fn test_insert_system_unnamed() {
660 let save = minimal_save();
661 let mut model = GalaxyModel::from_save(&save);
662 let name_count_before = model.name_index.len();
663
664 let addr = GalacticAddress::new(600, 20, -400, 0xAAA, 0, 0);
665 let system = System::new(addr, None, None, None, vec![]);
666 model.insert_system(system);
667
668 assert_eq!(model.name_index.len(), name_count_before);
670 }
671
672 #[test]
675 fn test_active_galaxy_defaults_to_zero() {
676 let model = GalaxyModel::new();
677 assert_eq!(model.active_galaxy, 0);
678 }
679
680 #[test]
681 fn test_set_active_galaxy() {
682 let mut model = GalaxyModel::new();
683 model.set_active_galaxy(9);
684 assert_eq!(model.active_galaxy, 9);
685 }
686
687 #[test]
688 fn test_active_spatial_empty_model_returns_none() {
689 let model = GalaxyModel::new();
690 assert!(model.active_spatial().is_none());
691 }
692
693 #[test]
694 fn test_spatial_for_unknown_galaxy_returns_none() {
695 let model = GalaxyModel::new();
696 assert!(model.spatial_for(42).is_none());
697 }
698
699 #[test]
700 fn test_discovered_galaxies_empty() {
701 let model = GalaxyModel::new();
702 assert!(model.discovered_galaxies().is_empty());
703 }
704
705 #[test]
706 fn test_insert_system_separate_galaxies() {
707 let mut model = GalaxyModel::new();
708
709 let addr0 = GalacticAddress::new(10, 0, 0, 0x100, 0, 0);
711 let sys0 = System::new(addr0, Some("Euclid Sys".into()), None, None, vec![]);
712 model.insert_system(sys0);
713
714 let addr9 = GalacticAddress::new(20, 0, 0, 0x200, 0, 9);
716 let sys9 = System::new(addr9, Some("Eissentam Sys".into()), None, None, vec![]);
717 model.insert_system(sys9);
718
719 assert_eq!(model.system_count(), 2);
720 assert_eq!(model.spatial_size(), 2);
721
722 let tree0 = model.spatial_for(0).unwrap();
724 assert_eq!(tree0.size(), 1);
725
726 let tree9 = model.spatial_for(9).unwrap();
728 assert_eq!(tree9.size(), 1);
729
730 assert!(model.spatial_for(5).is_none());
732 }
733
734 #[test]
735 fn test_discovered_galaxies_sorted() {
736 let mut model = GalaxyModel::new();
737
738 let addr9 = GalacticAddress::new(0, 0, 0, 0x100, 0, 9);
739 model.insert_system(System::new(addr9, None, None, None, vec![]));
740
741 let addr0 = GalacticAddress::new(0, 0, 0, 0x200, 0, 0);
742 model.insert_system(System::new(addr0, None, None, None, vec![]));
743
744 let addr2 = GalacticAddress::new(0, 0, 0, 0x300, 0, 2);
745 model.insert_system(System::new(addr2, None, None, None, vec![]));
746
747 let galaxies = model.discovered_galaxies();
748 assert_eq!(galaxies, vec![0, 2, 9]);
749 }
750
751 #[test]
752 fn test_rebuild_spatial() {
753 let mut model = GalaxyModel::new();
754
755 let addr0 = GalacticAddress::new(10, 0, 0, 0x100, 0, 0);
756 model.insert_system(System::new(addr0, None, None, None, vec![]));
757
758 let addr9 = GalacticAddress::new(20, 0, 0, 0x200, 0, 9);
759 model.insert_system(System::new(addr9, None, None, None, vec![]));
760
761 model.spatial.clear();
763 assert_eq!(model.spatial_size(), 0);
764
765 model.rebuild_spatial();
766 assert_eq!(model.spatial_size(), 2);
767 assert_eq!(model.spatial_for(0).unwrap().size(), 1);
768 assert_eq!(model.spatial_for(9).unwrap().size(), 1);
769 }
770
771 #[test]
772 fn test_active_spatial_returns_correct_tree() {
773 let mut model = GalaxyModel::new();
774
775 let addr0 = GalacticAddress::new(10, 0, 0, 0x100, 0, 0);
776 model.insert_system(System::new(addr0, Some("E1".into()), None, None, vec![]));
777
778 let addr9 = GalacticAddress::new(20, 0, 0, 0x200, 0, 9);
779 model.insert_system(System::new(addr9, Some("E2".into()), None, None, vec![]));
780
781 assert_eq!(model.active_spatial().unwrap().size(), 1);
783
784 model.set_active_galaxy(9);
785 assert_eq!(model.active_spatial().unwrap().size(), 1);
786
787 model.set_active_galaxy(42);
788 assert!(model.active_spatial().is_none());
789 }
790
791 #[test]
792 fn test_player_system_distance_zero() {
793 let json = std::fs::read_to_string("../../data/test/multi_system_save.json").unwrap();
796 let save = nms_save::parse_save(json.as_bytes()).unwrap();
797 let model = GalaxyModel::from_save(&save);
798
799 let player_pos = model.player_position().unwrap();
800 assert_eq!(player_pos.voxel_x(), 100);
801 assert_eq!(player_pos.voxel_y(), 50);
802 assert_eq!(player_pos.voxel_z(), -200);
803 assert_eq!(player_pos.solar_system_index(), 42);
804
805 let nearest = model.nearest_systems(player_pos, 1);
807 assert!(
808 !nearest.is_empty(),
809 "Expected at least one system near player"
810 );
811 let (_, distance) = &nearest[0];
812 assert_eq!(
813 *distance, 0.0,
814 "Player's own system should be at distance 0.0, got {distance}"
815 );
816 }
817
818 #[test]
819 fn test_ensure_player_system_inserts_sentinel() {
820 let json = r#"{
822 "Version": 4720, "Platform": "Mac|Final", "ActiveContext": "Main",
823 "CommonStateData": {"SaveName": "Test", "TotalPlayTime": 100},
824 "BaseContext": {"GameMode": 1, "PlayerStateData": {"UniverseAddress": {"RealityIndex": 0, "GalacticAddress": {"VoxelX": 999, "VoxelY": 10, "VoxelZ": -500, "SolarSystemIndex": 77, "PlanetIndex": 0}}, "Units": 0, "Nanites": 0, "Specials": 0, "PersistentPlayerBases": []}},
825 "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": []}},
826 "DiscoveryManagerData": {"DiscoveryData-v1": {"ReserveStore": 0, "ReserveManaged": 0, "Store": {"Record": []}}}
827 }"#;
828 let save = nms_save::parse_save(json.as_bytes()).unwrap();
829 let mut model = GalaxyModel::from_save(&save);
830
831 let player_pos = *model.player_position().unwrap();
832 let player_sys_id = SystemId::from_address(&player_pos);
833 assert!(model.system(&player_sys_id).is_none());
834
835 model.ensure_player_system();
836 assert!(model.system(&player_sys_id).is_some());
837
838 let nearest = model.nearest_systems(&player_pos, 1);
839 assert!(!nearest.is_empty());
840 assert_eq!(nearest[0].1, 0.0);
841 }
842}