1use std::fmt::{Display, Formatter};
2use std::num::ParseIntError;
3use std::str::FromStr;
4
5use lazy_static::lazy_static;
6use regex::Regex;
7use serde::Serialize;
8use thiserror::Error;
9
10use crate::from_str_deserialize_impl;
11use crate::modules::ship::{
12 ArmorModule, ArmorModuleError, InternalModule, InternalType, ModuleClass, ModuleClassError,
13};
14
15pub mod armor_grade;
16pub mod armor_module;
17pub mod internal_module;
18pub mod internal_type;
19
20#[derive(Debug, Serialize, Clone, PartialEq)]
22pub struct ShipInternalModule {
23 pub module: InternalModule,
25
26 pub size: u8,
28
29 pub class: ModuleClass,
31
32 pub free: bool,
34}
35
36#[derive(Debug, Error)]
37pub enum ShipInternalModuleError {
38 #[error("Failed to parse module: {0}]")]
39 FailedToParseModule(#[source] serde_json::Error),
40
41 #[error("Failed to parse size: {0}")]
42 FailedToParseSize(#[from] ParseIntError),
43
44 #[error("Failed to parse class number: {0}")]
45 FailedToParseClassNumber(#[source] ParseIntError),
46
47 #[error(transparent)]
48 ArmorModuleError(#[from] ArmorModuleError),
49
50 #[error(transparent)]
51 UnknownClass(#[from] ModuleClassError),
52
53 #[error("Failed to parse internal ship module: '{0}'")]
54 FailedToParse(String),
55}
56
57impl ShipInternalModule {
58 pub fn internal_type(&self) -> InternalType {
60 self.module.internal_type()
61 }
62
63 pub fn is_core_internal(&self) -> bool {
65 self.module.is_core()
66 }
67
68 pub fn is_optional_internal(&self) -> bool {
70 self.module.is_optional()
71 }
72
73 pub fn is_powerplay_module(&self) -> bool {
75 self.module.is_powerplay_module()
76 }
77
78 pub fn is_guardian_module(&self) -> bool {
81 self.module.is_guardian_module()
82 }
83}
84
85lazy_static! {
86 static ref SHIP_INTERNAL_MODULE_REGEX: Regex =
87 Regex::new(r#"^\$?[iI]nt_([a-zA-Z0-9_]+?)(_[sS]ize(1|2|3|4|5|6|7|8))?(_[cC]lass(1|2|3|4|5))?(_[tT]iny)?(_[a-zA-Z_]+?)?(_name;)?$"#).unwrap();
88}
89
90impl FromStr for ShipInternalModule {
91 type Err = ShipInternalModuleError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 let armor_result = ArmorModule::from_str(s);
95
96 match armor_result {
97 Ok(armor_module) => {
98 return Ok(ShipInternalModule {
99 size: 1,
100 class: (&armor_module).into(),
101 module: InternalModule::Armor(armor_module),
102 free: false,
103 })
104 }
105 Err(ArmorModuleError::FailedToParse(_)) => {}
106 Err(e) => return Err(e.into()),
107 }
108
109 let Some(captures) = SHIP_INTERNAL_MODULE_REGEX.captures(s) else {
110 return Err(ShipInternalModuleError::FailedToParse(s.to_string()));
111 };
112
113 let module_string = captures
114 .get(1)
115 .expect("Should have been captured already")
116 .as_str();
117
118 let module_suffix = captures
119 .get(7)
120 .map(|capture| capture.as_str())
121 .unwrap_or_default();
122
123 let mut free = false;
124 let mut module_string = format!("{module_string}{module_suffix}");
125
126 if module_string.ends_with("_free") {
127 module_string = module_string.replace("_free", "");
128 free = true;
129 }
130
131 let module = module_string
132 .parse()
133 .map_err(ShipInternalModuleError::FailedToParseModule)?;
134
135 if captures.get(6).is_some() {
136 return Ok(ShipInternalModule {
137 module,
138 size: 1,
139 class: ModuleClass::I,
140 free,
141 });
142 }
143
144 let size = match captures.get(3) {
145 Some(capture) => capture.as_str().parse()?,
146 None => 1,
147 };
148
149 let class = captures
155 .get(5)
156 .map(|capture| {
157 capture
158 .as_str()
159 .parse::<u8>()
160 .map_err(ShipInternalModuleError::FailedToParseClassNumber)
161 })
162 .transpose()?
163 .map(|grade_nr| grade_nr.try_into())
164 .transpose()?
165 .map(|class| module.special_grades(size, Some(&class)).unwrap_or(class))
166 .or_else(|| module.special_grades(size, None))
167 .unwrap_or(ModuleClass::E);
168
169 Ok(ShipInternalModule {
170 module,
171 size,
172 class,
173 free,
174 })
175 }
176}
177
178from_str_deserialize_impl!(ShipInternalModule);
179
180impl Display for ShipInternalModule {
181 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
182 write!(f, "{}{} {}", self.size, self.class, self.module)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use std::str::FromStr;
189
190 use crate::modules::ship::{
191 ArmorGrade, ArmorModule, InternalModule, ModuleClass, ShipInternalModule, ShipType,
192 };
193
194 #[test]
195 fn ship_internal_module_test_cases_are_parsed_correctly() {
196 let test_cases = [
197 (
198 "$int_dronecontrol_collection_size3_class5_name;",
199 ShipInternalModule {
200 module: InternalModule::CollectorLimpetController,
201 size: 3,
202 class: ModuleClass::A,
203 free: false,
204 },
205 ),
206 (
207 "int_engine_size5_class5",
208 ShipInternalModule {
209 module: InternalModule::Thrusters,
210 size: 5,
211 class: ModuleClass::A,
212 free: false,
213 },
214 ),
215 (
216 "$int_engine_size5_class5_name;",
217 ShipInternalModule {
218 module: InternalModule::Thrusters,
219 size: 5,
220 class: ModuleClass::A,
221 free: false,
222 },
223 ),
224 (
225 "$int_supercruiseassist_name;",
226 ShipInternalModule {
227 module: InternalModule::SupercruiseAssist,
228 size: 1,
229 class: ModuleClass::E,
230 free: false,
231 },
232 ),
233 (
234 "$int_detailedsurfacescanner_tiny_name;",
235 ShipInternalModule {
236 module: InternalModule::DetailedSurfaceScanner,
237 size: 1,
238 class: ModuleClass::I,
239 free: false,
240 },
241 ),
242 (
243 "$int_lifesupport_size4_class1_name;",
244 ShipInternalModule {
245 module: InternalModule::LifeSupport,
246 size: 4,
247 class: ModuleClass::E,
248 free: false,
249 },
250 ),
251 (
252 "Int_Hyperdrive_Size5_Class5",
253 ShipInternalModule {
254 module: InternalModule::FrameShiftDrive,
255 size: 5,
256 class: ModuleClass::A,
257 free: false,
258 },
259 ),
260 (
261 "$int_cargorack_size7_class1_name;",
262 ShipInternalModule {
263 module: InternalModule::CargoRack,
264 size: 7,
265 class: ModuleClass::E,
266 free: false,
267 },
268 ),
269 (
270 "$int_shieldgenerator_size8_class3_fast_name;",
271 ShipInternalModule {
272 module: InternalModule::BiWeaveShieldGenerator,
273 size: 8,
274 class: ModuleClass::C,
275 free: false,
276 },
277 ),
278 (
279 "$type9_military_armour_grade1_name;",
280 ShipInternalModule {
281 module: InternalModule::Armor(ArmorModule {
282 ship: ShipType::Type10Defender,
283 grade: ArmorGrade::LightweightAlloy,
284 }),
285 size: 1,
286 class: ModuleClass::C,
287 free: false,
288 },
289 ),
290 (
291 "krait_mkii_armour_grade3",
292 ShipInternalModule {
293 module: InternalModule::Armor(ArmorModule {
294 ship: ShipType::KraitMkII,
295 grade: ArmorGrade::MilitaryGradeComposite,
296 }),
297 size: 1,
298 class: ModuleClass::A,
299 free: false,
300 },
301 ),
302 (
303 "int_planetapproachsuite_advanced",
304 ShipInternalModule {
305 module: InternalModule::PlanetApproachSuite,
306 size: 1,
307 class: ModuleClass::I,
308 free: false,
309 },
310 ),
311 (
312 "Int_MkIIAgileBoost_Engine_Size5_Class5",
313 ShipInternalModule {
314 module: InternalModule::MkIIAgileBoostThrusters,
315 size: 5,
316 class: ModuleClass::A,
317 free: false,
318 },
319 ),
320 (
321 "Int_Engine_Size7_Class5_GravityOptimised_MkII",
322 ShipInternalModule {
323 module: InternalModule::GravityOptimisedThrustersMkII,
324 size: 7,
325 class: ModuleClass::A,
326 free: false,
327 },
328 ),
329 (
330 "$int_multidronecontrol_miningv2_size5_class5_name;",
331 ShipInternalModule {
332 module: InternalModule::MiningMultiLimpetControllerMkII,
333 size: 5,
334 class: ModuleClass::A,
335 free: false,
336 },
337 ),
338 (
339 "explorer_nx_armour_grade1_default",
340 ShipInternalModule {
341 module: InternalModule::Armor(ArmorModule {
342 ship: ShipType::CaspianExplorer,
343 grade: ArmorGrade::LightweightAlloy,
344 }),
345 size: 1,
346 class: ModuleClass::C,
347 free: false,
348 },
349 ),
350 (
351 "$federation_corvette_armour_grade1_name;",
352 ShipInternalModule {
353 module: InternalModule::Armor(ArmorModule {
354 ship: ShipType::FederalCorvette,
355 grade: ArmorGrade::LightweightAlloy,
356 }),
357 size: 1,
358 class: ModuleClass::C,
359 free: false,
360 },
361 ),
362 ];
363
364 for (input, expected) in test_cases {
365 let result = ShipInternalModule::from_str(input);
366
367 dbg!(&result);
368
369 assert!(result.is_ok());
370 assert_eq!(result.unwrap(), expected);
371 }
372 }
373}