Skip to main content

ed_journals/modules/ship/models/
ship_module.rs

1use crate::from_str_deserialize_impl;
2use crate::modules::ship::models::ship_module::ship_bobble::ShipBobble;
3use crate::modules::ship::models::ship_module::ship_engine_color::ShipEngineColor;
4use crate::modules::ship::models::ship_module::ship_kit_module::ShipKitModule;
5use crate::modules::ship::models::ship_module::ship_string_lights::ShipStringLights;
6use crate::modules::ship::models::ship_module::ship_weapon_color::ShipWeaponColor;
7use crate::modules::ship::{
8    ShipCockpitModule, ShipDecal, ShipHardpointModule, ShipInternalModule, ShipNameplate,
9    ShipPaintJob, ShipVoicepack,
10};
11use crate::ship::FighterType;
12use serde::Serialize;
13use std::fmt::{Display, Formatter};
14use std::str::FromStr;
15use thiserror::Error;
16
17pub mod module_class;
18pub mod ship_bobble;
19pub mod ship_cockpit_module;
20pub mod ship_decal;
21pub mod ship_engine_color;
22pub mod ship_hardpoint_module;
23pub mod ship_internal_module;
24pub mod ship_kit_module;
25pub mod ship_nameplate;
26pub mod ship_paint_job;
27pub mod ship_string_lights;
28pub mod ship_voicepack;
29pub mod ship_weapon_color;
30
31/// Any ship module, covering all the different kinds of modules: hardpoints, utility modules,
32/// core internals and optional internals. This also covers cosmetic items like paint job, decals,
33/// engine colors etc.
34///
35/// The game considers full-sized hardpoints and utility modules both use [ShipHardpointModule],
36/// where utility modules have a hardpoint size of [HardpointSize::Tiny].
37///
38/// The same is true for core internals and optional internals which both use [ShipInternalModule].
39#[derive(Debug, Serialize, Clone, PartialEq)]
40pub enum ShipModule {
41    /// Special case for the cargo bay door.
42    CargoBayDoor,
43
44    /// Spacial case for the data link scanner.
45    DataLinkScanner,
46
47    /// Spacial case for the codex scanner.
48    CodexScanner,
49
50    /// Spacial case for the discovery scanner.
51    DiscoverScanner,
52
53    /// Some fighter types show up as ship modules when unlocking them from technology brokers.
54    Fighter(FighterType),
55
56    /// Any internal module, this includes core and optional modules.
57    Internal(ShipInternalModule),
58
59    /// For external modules, both full-sized hardpoints and utility modules.
60    Hardpoint(ShipHardpointModule),
61
62    /// Special module for a cockpit for a specific ship.
63    Cockpit(ShipCockpitModule),
64
65    // Cosmetic
66    PaintJob(ShipPaintJob),
67    Decal(ShipDecal),
68    VoicePack(ShipVoicepack),
69    Nameplate(ShipNameplate),
70    EngineColor(ShipEngineColor),
71    WeaponColor(ShipWeaponColor),
72    ShipKitModule(ShipKitModule),
73    Bobble(ShipBobble),
74    StringLights(ShipStringLights),
75
76    #[cfg(feature = "allow-unknown")]
77    #[cfg_attr(docsrs, doc(cfg(feature = "allow-unknown")))]
78    #[serde(untagged)]
79    Unknown(String),
80}
81
82impl ShipModule {
83    /// Whether the module is any kind of hardpoint module, including utility modules.
84    pub fn is_hardpoint_module(&self) -> bool {
85        matches!(self, ShipModule::Hardpoint(_))
86    }
87
88    /// Whether the module is a full-sized hardpoint module. This does not include utility modules.
89    pub fn is_full_sized_hardpoint_module(&self) -> bool {
90        let ShipModule::Hardpoint(hardpoint) = self else {
91            return false;
92        };
93
94        hardpoint.is_full_sized_module()
95    }
96
97    /// Whether the module is a utility module.
98    pub fn is_utility_module(&self) -> bool {
99        let ShipModule::Hardpoint(hardpoint) = self else {
100            return false;
101        };
102
103        hardpoint.is_utility_module()
104    }
105
106    /// Whether the module is any internal module. This includes both core- and optional internals.
107    pub fn is_internal_module(&self) -> bool {
108        matches!(self, ShipModule::Internal(_))
109    }
110
111    /// Whether the module is a core internal module.
112    pub fn is_core_internal(&self) -> bool {
113        let ShipModule::Internal(internal) = self else {
114            return false;
115        };
116
117        internal.is_core_internal()
118    }
119
120    /// Whether the module is an optional internal module.
121    pub fn is_optional_internal(&self) -> bool {
122        let ShipModule::Internal(internal) = self else {
123            return false;
124        };
125
126        internal.is_optional_internal()
127    }
128
129    /// Whether the module is a module that is unlocked through powerplay.
130    pub fn is_powerplay_module(&self) -> bool {
131        match self {
132            ShipModule::Internal(internal) => internal.is_powerplay_module(),
133            ShipModule::Hardpoint(hardpoint) => hardpoint.is_powerplay_module(),
134            _ => false,
135        }
136    }
137
138    /// Whether the module is a module that is unlocked using guardian parts at a guardian
139    /// technology broker.
140    pub fn is_guardian_module(&self) -> bool {
141        match self {
142            ShipModule::Internal(internal) => internal.is_guardian_module(),
143            ShipModule::Hardpoint(hardpoint) => hardpoint.is_guardian_module(),
144            _ => false,
145        }
146    }
147
148    /// Whether the module is a cosmetic module. The game tracks these cosmetics as modules that
149    /// slot into special slots.
150    pub fn is_cosmetic(&self) -> bool {
151        matches!(
152            self,
153            ShipModule::PaintJob(_)
154                | ShipModule::Decal(_)
155                | ShipModule::VoicePack(_)
156                | ShipModule::Nameplate(_)
157                | ShipModule::EngineColor(_)
158                | ShipModule::WeaponColor(_)
159                | ShipModule::ShipKitModule(_)
160                | ShipModule::Bobble(_)
161                | ShipModule::StringLights(_)
162        )
163    }
164
165    fn exact_match(s: &str) -> Option<ShipModule> {
166        Some(match s {
167            "modularcargobaydoor"
168            | "modularcargobaydoorfdl"
169            | "$modularcargobaydoor_name;"
170            | "$modularcargobaydoorfdl_name;" => ShipModule::CargoBayDoor,
171
172            "hpt_shipdatalinkscanner" => ShipModule::DataLinkScanner,
173            "int_codexscanner" => ShipModule::CodexScanner,
174            "int_stellarbodydiscoveryscanner_standard" => ShipModule::DiscoverScanner,
175
176            _ => return None,
177        })
178    }
179}
180
181#[derive(Debug, Error)]
182pub enum ShipModuleError {
183    #[error("Unknown ship module: '{0}'")]
184    UnknownEntry(String),
185}
186
187impl FromStr for ShipModule {
188    type Err = ShipModuleError;
189
190    // TODO this needs to be cleaner eventually
191    #[cfg(not(feature = "allow-unknown"))]
192    fn from_str(s: &str) -> Result<Self, Self::Err> {
193        if let Some(exact_match) = Self::exact_match(s) {
194            return Ok(exact_match);
195        }
196
197        if let Ok(fighter) = FighterType::from_str(s) {
198            return Ok(ShipModule::Fighter(fighter));
199        }
200
201        if let Ok(interal) = ShipInternalModule::from_str(s) {
202            return Ok(ShipModule::Internal(interal));
203        }
204
205        if let Ok(hardpoint) = ShipHardpointModule::from_str(s) {
206            return Ok(ShipModule::Hardpoint(hardpoint));
207        }
208
209        if let Ok(cockpit) = ShipCockpitModule::from_str(s) {
210            return Ok(ShipModule::Cockpit(cockpit));
211        }
212
213        if let Ok(paint_job) = ShipPaintJob::from_str(s) {
214            return Ok(ShipModule::PaintJob(paint_job));
215        }
216
217        if let Ok(decal_job) = ShipDecal::from_str(s) {
218            return Ok(ShipModule::Decal(decal_job));
219        }
220
221        if let Ok(voice_pack) = ShipVoicepack::from_str(s) {
222            return Ok(ShipModule::VoicePack(voice_pack));
223        }
224
225        if let Ok(nameplate) = ShipNameplate::from_str(s) {
226            return Ok(ShipModule::Nameplate(nameplate));
227        }
228
229        if let Ok(engine_color) = ShipEngineColor::from_str(s) {
230            return Ok(ShipModule::EngineColor(engine_color));
231        }
232
233        if let Ok(weapon_color) = ShipWeaponColor::from_str(s) {
234            return Ok(ShipModule::WeaponColor(weapon_color));
235        }
236
237        if let Ok(ship_kit_module) = ShipKitModule::from_str(s) {
238            return Ok(ShipModule::ShipKitModule(ship_kit_module));
239        }
240
241        if let Ok(bobble) = ShipBobble::from_str(s) {
242            return Ok(ShipModule::Bobble(bobble));
243        }
244
245        if let Ok(string_lights) = ShipStringLights::from_str(s) {
246            return Ok(ShipModule::StringLights(string_lights));
247        }
248
249        Err(ShipModuleError::UnknownEntry(s.to_string()))
250    }
251
252    #[cfg(feature = "allow-unknown")]
253    fn from_str(s: &str) -> Result<Self, Self::Err> {
254        if let Some(exact_match) = Self::exact_match(s) {
255            return Ok(exact_match);
256        }
257
258        if let Ok(fighter) = FighterType::from_str(s) {
259            if !fighter.is_unknown() {
260                return Ok(ShipModule::Fighter(fighter));
261            }
262        }
263
264        if let Ok(internal) = ShipInternalModule::from_str(s) {
265            return Ok(ShipModule::Internal(internal));
266        }
267
268        if let Ok(hardpoint) = ShipHardpointModule::from_str(s) {
269            return Ok(ShipModule::Hardpoint(hardpoint));
270        }
271
272        if let Ok(cockpit) = ShipCockpitModule::from_str(s) {
273            return Ok(ShipModule::Cockpit(cockpit));
274        }
275
276        if let Ok(paint_job) = ShipPaintJob::from_str(s) {
277            return Ok(ShipModule::PaintJob(paint_job));
278        }
279
280        if let Ok(decal_job) = ShipDecal::from_str(s) {
281            return Ok(ShipModule::Decal(decal_job));
282        }
283
284        if let Ok(voice_pack) = ShipVoicepack::from_str(s) {
285            return Ok(ShipModule::VoicePack(voice_pack));
286        }
287
288        if let Ok(nameplate) = ShipNameplate::from_str(s) {
289            return Ok(ShipModule::Nameplate(nameplate));
290        }
291
292        if let Ok(engine_color) = ShipEngineColor::from_str(s) {
293            return Ok(ShipModule::EngineColor(engine_color));
294        }
295
296        if let Ok(weapon_color) = ShipWeaponColor::from_str(s) {
297            return Ok(ShipModule::WeaponColor(weapon_color));
298        }
299
300        if let Ok(ship_kit_module) = ShipKitModule::from_str(s) {
301            return Ok(ShipModule::ShipKitModule(ship_kit_module));
302        }
303
304        if let Ok(bobble) = ShipBobble::from_str(s) {
305            return Ok(ShipModule::Bobble(bobble));
306        }
307
308        if let Ok(string_lights) = ShipStringLights::from_str(s) {
309            return Ok(ShipModule::StringLights(string_lights));
310        }
311
312        Err(ShipModuleError::UnknownEntry(s.to_string()))
313    }
314}
315
316from_str_deserialize_impl!(ShipModule);
317
318impl Display for ShipModule {
319    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
320        match self {
321            ShipModule::CargoBayDoor => write!(f, "Cargo Hatch"),
322            ShipModule::DataLinkScanner => write!(f, "Data Link Scanner"),
323            ShipModule::CodexScanner => write!(f, "Codex Scanner"),
324            ShipModule::DiscoverScanner => write!(f, "Discovery Scanner"),
325            ShipModule::Fighter(fighter) => write!(f, "{}", fighter),
326            ShipModule::Internal(internal_module) => internal_module.fmt(f),
327            ShipModule::Hardpoint(hardpoint_module) => hardpoint_module.fmt(f),
328            ShipModule::Cockpit(_) => write!(f, "Cockpit"),
329            ShipModule::PaintJob(_) => write!(f, "Paint job"),
330            ShipModule::Decal(_) => write!(f, "Decal"),
331            ShipModule::VoicePack(_) => write!(f, "Voicepack"),
332            ShipModule::Nameplate(_) => write!(f, "Nameplate"),
333            ShipModule::EngineColor(_) => write!(f, "Engine Color"),
334            ShipModule::WeaponColor(_) => write!(f, "Weapon Color"),
335            ShipModule::Bobble(_) => write!(f, "Bobble"),
336            ShipModule::StringLights(_) => write!(f, "String Lights"),
337            ShipModule::ShipKitModule(module) => write!(f, "Skip kit module: {}", module.name),
338
339            #[cfg(feature = "allow-unknown")]
340            ShipModule::Unknown(unknown) => write!(f, "Unknown module: {unknown}"),
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use serde_json::Value;
348
349    use crate::modules::ship::ShipModule;
350    use crate::ship::{
351        ArmorGrade, ArmorModule, HardpointMounting, HardpointSize, InternalModule, ModuleClass,
352        ShipInternalModule, ShipType,
353    };
354
355    #[test]
356    fn modules_are_parsed_correctly() {
357        let test_cases = include_str!("zz_ship_module_test_cases.txt").lines();
358
359        let mut count = 0;
360
361        for line in test_cases {
362            let result = serde_json::from_value::<ShipModule>(Value::String(line.to_string()));
363            count += 1;
364
365            if result.is_err() {
366                dbg!(&line);
367                dbg!(&result);
368            }
369
370            assert!(result.is_ok());
371        }
372
373        assert!(count > 1000);
374    }
375
376    #[test]
377    fn specific_ship_module_test_cases_are_parsed_correctly() {
378        let test_cases = [(
379            "$federation_corvette_armour_grade1_name;",
380            ShipModule::Internal(ShipInternalModule {
381                module: InternalModule::Armor(ArmorModule {
382                    ship: ShipType::FederalCorvette,
383                    grade: ArmorGrade::LightweightAlloy,
384                }),
385                size: 1,
386                class: ModuleClass::C,
387                free: false,
388            }),
389        )];
390
391        for (input, expected) in test_cases {
392            let result = serde_json::from_value::<ShipModule>(Value::String(input.to_string()));
393
394            if result.is_err() {
395                dbg!(&input);
396                dbg!(&result);
397            }
398
399            assert_eq!(result.unwrap(), expected);
400        }
401    }
402
403    #[test]
404    fn all_eddn_test_cases_are_parsed_correctly() {
405        let content = include_str!("zz_ship_modules.txt");
406        let lines = content.lines();
407
408        for line in lines {
409            if line.starts_with('#') {
410                continue;
411            }
412
413            let mut parts = line.split(',');
414            parts.next().unwrap();
415
416            let input = parts.next().unwrap();
417            parts.next().unwrap();
418            parts.next().unwrap();
419
420            let mounting: Option<HardpointMounting> = parts
421                .next()
422                .and_then(|string| {
423                    if string.is_empty() {
424                        None
425                    } else {
426                        Some(string)
427                    }
428                })
429                .map(|mounting| mounting.parse())
430                .transpose()
431                .unwrap();
432
433            parts.next().unwrap();
434            parts.next().unwrap();
435
436            let size = parts.next().unwrap().parse::<u8>().unwrap();
437
438            let class = parts.next().unwrap().parse::<ModuleClass>().unwrap();
439
440            let parsed = serde_json::from_value::<ShipModule>(Value::String(input.to_string()));
441
442            dbg!(&input);
443            dbg!(&parsed);
444            assert!(parsed.is_ok());
445
446            match parsed.unwrap() {
447                // ShipModule::CargoBayDoor => {}
448                // ShipModule::DataLinkScanner => {}
449                // ShipModule::CodexScanner => {}
450                // ShipModule::DiscoverScanner => {}
451                ShipModule::Internal(internal) => {
452                    dbg!(&internal);
453
454                    assert_eq!(internal.size, size);
455                    assert_eq!(internal.class, class);
456                }
457                ShipModule::Hardpoint(hardpoint) => {
458                    dbg!(&hardpoint);
459
460                    let mounting = mounting.unwrap_or(HardpointMounting::Turreted);
461                    let hardpoint_size = HardpointSize::try_from(size).unwrap();
462
463                    assert_eq!(hardpoint.mounting, mounting);
464                    assert_eq!(hardpoint.class, class);
465                    assert_eq!(hardpoint.size, hardpoint_size);
466                }
467                _ => {}
468            }
469        }
470    }
471}