ed_journals/modules/ship/models/
ship_module.rs1use 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#[derive(Debug, Serialize, Clone, PartialEq)]
40pub enum ShipModule {
41 CargoBayDoor,
43
44 DataLinkScanner,
46
47 CodexScanner,
49
50 DiscoverScanner,
52
53 Fighter(FighterType),
55
56 Internal(ShipInternalModule),
58
59 Hardpoint(ShipHardpointModule),
61
62 Cockpit(ShipCockpitModule),
64
65 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 pub fn is_hardpoint_module(&self) -> bool {
85 matches!(self, ShipModule::Hardpoint(_))
86 }
87
88 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 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 pub fn is_internal_module(&self) -> bool {
108 matches!(self, ShipModule::Internal(_))
109 }
110
111 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 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 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 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 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 #[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::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}