Skip to main content

ed_journals/modules/ship/models/ship_module/
ship_internal_module.rs

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/// Model for internal modules, this includes both core internals and optional internals.
21#[derive(Debug, Serialize, Clone, PartialEq)]
22pub struct ShipInternalModule {
23    /// The kind internal module, which can both be core- and optional internals.
24    pub module: InternalModule,
25
26    /// The module size.
27    pub size: u8,
28
29    /// The class of the module.
30    pub class: ModuleClass,
31
32    /// Whether the module is a free module.
33    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    /// Returns whether the module is a core- or optional type.
59    pub fn internal_type(&self) -> InternalType {
60        self.module.internal_type()
61    }
62
63    /// Whether the module is a core internal.
64    pub fn is_core_internal(&self) -> bool {
65        self.module.is_core()
66    }
67
68    /// Whether the module is an option internal.
69    pub fn is_optional_internal(&self) -> bool {
70        self.module.is_optional()
71    }
72
73    /// Whether the module is a module that is unlocked through powerplay.
74    pub fn is_powerplay_module(&self) -> bool {
75        self.module.is_powerplay_module()
76    }
77
78    /// Whether the module is a module that is unlocked using guardian parts at a guardian
79    /// technology broker.
80    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        // Phew... Okay. So. This part checks if there is a capture for the '_grade' part. If there
150        // is one it will first check `special_grades` if it should be another grade instead. Else
151        // if there is no match, then it will still check `special_grades` but without providing
152        // a ModuleGrade input. If none of these things return a Some value, then the class will
153        // default to `ModuleClass::E`.
154        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}