1use nms_core::address::GalacticAddress;
8use nms_core::biome::Biome;
9use nms_core::galaxy::Galaxy;
10use nms_graph::GalaxyModel;
11
12#[derive(Debug)]
14pub struct SessionState {
15 pub position: Option<PositionContext>,
17
18 pub biome_filter: Option<Biome>,
20
21 pub warp_range: Option<f64>,
23
24 pub galaxy: Galaxy,
26
27 pub system_count: usize,
29
30 pub planet_count: usize,
32}
33
34#[derive(Debug, Clone)]
36pub enum PositionContext {
37 Base {
39 name: String,
40 address: GalacticAddress,
41 },
42 PlayerPosition(GalacticAddress),
44 Address(GalacticAddress),
46}
47
48impl PositionContext {
49 pub fn address(&self) -> &GalacticAddress {
50 match self {
51 Self::Base { address, .. } => address,
52 Self::PlayerPosition(a) | Self::Address(a) => a,
53 }
54 }
55
56 pub fn label(&self) -> String {
57 match self {
58 Self::Base { name, .. } => name.clone(),
59 Self::PlayerPosition(_) => "player position".into(),
60 Self::Address(a) => format!("0x{:012X}", a.packed()),
61 }
62 }
63}
64
65impl SessionState {
66 pub fn from_model(model: &GalaxyModel) -> Self {
68 let position = model
69 .player_state
70 .as_ref()
71 .map(|ps| PositionContext::PlayerPosition(ps.current_address));
72
73 let galaxy = model
74 .player_state
75 .as_ref()
76 .map(|ps| Galaxy::by_index(ps.current_address.reality_index))
77 .unwrap_or_else(|| Galaxy::by_index(0));
78
79 Self {
80 position,
81 biome_filter: None,
82 warp_range: None,
83 galaxy,
84 system_count: model.systems.len(),
85 planet_count: model.planets.len(),
86 }
87 }
88
89 pub fn set_position_base(&mut self, name: &str, model: &GalaxyModel) -> Result<String, String> {
91 let base = model
92 .base(name)
93 .ok_or_else(|| format!("Base not found: \"{name}\""))?;
94 let address = base.address;
95 let display_name = base.name.clone();
96 self.position = Some(PositionContext::Base {
97 name: display_name.clone(),
98 address,
99 });
100 Ok(format!("Position set to {display_name}"))
101 }
102
103 pub fn set_position_address(&mut self, address: GalacticAddress) -> String {
105 let label = format!("0x{:012X}", address.packed());
106 self.position = Some(PositionContext::Address(address));
107 format!("Position set to {label}")
108 }
109
110 pub fn reset_position(&mut self, model: &GalaxyModel) -> String {
112 self.position = model
113 .player_state
114 .as_ref()
115 .map(|ps| PositionContext::PlayerPosition(ps.current_address));
116 "Position reset to player location".into()
117 }
118
119 pub fn set_biome_filter(&mut self, biome: Biome) -> String {
121 let name = format!("{biome:?}");
122 self.biome_filter = Some(biome);
123 format!("Biome filter set to {name}")
124 }
125
126 pub fn clear_biome_filter(&mut self) -> &'static str {
128 self.biome_filter = None;
129 "Biome filter cleared"
130 }
131
132 pub fn set_warp_range(&mut self, ly: f64) -> String {
134 self.warp_range = Some(ly);
135 format!("Warp range set to {} ly", ly as u64)
136 }
137
138 pub fn clear_warp_range(&mut self) -> &'static str {
140 self.warp_range = None;
141 "Warp range cleared"
142 }
143
144 pub fn reset_all(&mut self, model: &GalaxyModel) -> &'static str {
146 self.reset_position(model);
147 self.biome_filter = None;
148 self.warp_range = None;
149 "Session state reset"
150 }
151
152 pub fn format_status(&self) -> String {
154 let mut lines = Vec::new();
155
156 lines.push(format!(
157 "Galaxy: {} ({})",
158 self.galaxy.name, self.galaxy.galaxy_type
159 ));
160 lines.push(format!(
161 "Model: {} systems, {} planets",
162 self.system_count, self.planet_count
163 ));
164
165 match &self.position {
166 Some(pos) => lines.push(format!("Position: {}", pos.label())),
167 None => lines.push("Position: unknown".into()),
168 }
169
170 match &self.biome_filter {
171 Some(b) => lines.push(format!("Biome: {b:?}")),
172 None => lines.push("Biome: (none)".into()),
173 }
174
175 match self.warp_range {
176 Some(r) => lines.push(format!("Warp range: {} ly", r as u64)),
177 None => lines.push("Warp range: (none)".into()),
178 }
179
180 lines.join("\n") + "\n"
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn test_model() -> GalaxyModel {
189 let json = r#"{
190 "Version": 4720, "Platform": "Mac|Final", "ActiveContext": "Main",
191 "CommonStateData": {"SaveName": "Test", "TotalPlayTime": 100},
192 "BaseContext": {
193 "GameMode": 1,
194 "PlayerStateData": {
195 "UniverseAddress": {"RealityIndex": 0, "GalacticAddress": {"VoxelX": 100, "VoxelY": 50, "VoxelZ": -200, "SolarSystemIndex": 42, "PlanetIndex": 0}},
196 "Units": 0, "Nanites": 0, "Specials": 0,
197 "PersistentPlayerBases": [
198 {"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": "Home Base", "BaseType": {"PersistentBaseTypes": "HomePlanetBase"}, "LastEditedById": "", "LastEditedByUsername": ""}
199 ]
200 }
201 },
202 "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": []}},
203 "DiscoveryManagerData": {"DiscoveryData-v1": {"ReserveStore": 0, "ReserveManaged": 0, "Store": {"Record": [
204 {"DD": {"UA": "0x050003AB8C07", "DT": "SolarSystem", "VP": []}, "DM": {}, "OWS": {"LID":"","UID":"1","USN":"Explorer","PTK":"ST","TS":0}, "FL": {"U": 1}},
205 {"DD": {"UA": "0x150003AB8C07", "DT": "Planet", "VP": ["0xAB", 0]}, "DM": {}, "OWS": {"LID":"","UID":"1","USN":"Explorer","PTK":"ST","TS":0}, "FL": {"U": 1}}
206 ]}}}
207 }"#;
208 let save = nms_save::parse_save(json.as_bytes()).unwrap();
209 GalaxyModel::from_save(&save)
210 }
211
212 #[test]
213 fn test_session_from_model() {
214 let model = test_model();
215 let session = SessionState::from_model(&model);
216 assert!(session.position.is_some());
217 assert_eq!(session.galaxy.name, "Euclid");
218 assert!(session.system_count > 0);
219 }
220
221 #[test]
222 fn test_set_position_base() {
223 let model = test_model();
224 let mut session = SessionState::from_model(&model);
225 let result = session.set_position_base("Home Base", &model);
226 assert!(result.is_ok());
227 assert!(result.unwrap().contains("Home Base"));
228 match &session.position {
229 Some(PositionContext::Base { name, .. }) => assert_eq!(name, "Home Base"),
230 _ => panic!("Expected Base position"),
231 }
232 }
233
234 #[test]
235 fn test_set_position_unknown_base_errors() {
236 let model = test_model();
237 let mut session = SessionState::from_model(&model);
238 assert!(session.set_position_base("No Such Base", &model).is_err());
239 }
240
241 #[test]
242 fn test_set_biome_filter() {
243 let model = test_model();
244 let mut session = SessionState::from_model(&model);
245 session.set_biome_filter(Biome::Lush);
246 assert_eq!(session.biome_filter, Some(Biome::Lush));
247 }
248
249 #[test]
250 fn test_clear_biome_filter() {
251 let model = test_model();
252 let mut session = SessionState::from_model(&model);
253 session.set_biome_filter(Biome::Lush);
254 session.clear_biome_filter();
255 assert!(session.biome_filter.is_none());
256 }
257
258 #[test]
259 fn test_set_warp_range() {
260 let model = test_model();
261 let mut session = SessionState::from_model(&model);
262 session.set_warp_range(2500.0);
263 assert_eq!(session.warp_range, Some(2500.0));
264 }
265
266 #[test]
267 fn test_reset_all() {
268 let model = test_model();
269 let mut session = SessionState::from_model(&model);
270 session.set_biome_filter(Biome::Toxic);
271 session.set_warp_range(1000.0);
272 session.reset_all(&model);
273 assert!(session.biome_filter.is_none());
274 assert!(session.warp_range.is_none());
275 }
276
277 #[test]
278 fn test_format_status() {
279 let model = test_model();
280 let session = SessionState::from_model(&model);
281 let output = session.format_status();
282 assert!(output.contains("Euclid"));
283 assert!(output.contains("systems"));
284 }
285
286 #[test]
287 fn test_position_context_label() {
288 let addr = GalacticAddress::new(0, 0, 0, 0, 0, 0);
289 let base = PositionContext::Base {
290 name: "Test".into(),
291 address: addr,
292 };
293 assert_eq!(base.label(), "Test");
294
295 let player = PositionContext::PlayerPosition(addr);
296 assert_eq!(player.label(), "player position");
297 }
298
299 #[test]
300 fn test_set_position_address() {
301 let model = test_model();
302 let mut session = SessionState::from_model(&model);
303 let addr = GalacticAddress::new(100, 50, -200, 42, 0, 0);
304 let msg = session.set_position_address(addr);
305 assert!(msg.contains("Position set to"));
306 assert!(matches!(
307 &session.position,
308 Some(PositionContext::Address(_))
309 ));
310 }
311
312 #[test]
313 fn test_reset_position() {
314 let model = test_model();
315 let mut session = SessionState::from_model(&model);
316 session.set_position_base("Home Base", &model).unwrap();
317 session.reset_position(&model);
318 assert!(matches!(
319 &session.position,
320 Some(PositionContext::PlayerPosition(_))
321 ));
322 }
323}