Skip to main content

ed_journals/modules/ship/models/
ship_slot.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::models::ship_slot::core_slot::CoreSlot;
12use crate::modules::ship::{HardpointSize, HardpointSizeError};
13
14pub mod core_slot;
15
16#[derive(Debug, Serialize, Clone, PartialEq)]
17pub struct ShipSlot {
18    pub slot_nr: u8,
19    pub kind: ShipSlotKind,
20}
21
22// TODO kinda want to refactor this to use untagged variants
23#[derive(Debug, Serialize, Clone, PartialEq)]
24pub enum ShipSlotKind {
25    /// Special "slot" reserved for the ship's cockpit.
26    ShipCockpit,
27
28    /// Special "slot" reserved for the cargo hatch.
29    CargoHatch,
30
31    /// Slot for utility modules.
32    UtilityMount,
33
34    /// General non-restricted hardpoint.
35    Hardpoint(HardpointSize),
36
37    /// Hardpoint reserved for mining hardware.
38    MiningHardPoint(HardpointSize),
39
40    /// Any non-restricted optional internal slot with the size of the slot.
41    OptionalInternal(u8),
42
43    /// A slot reserved for a limpet controller as seen on the Type 11.
44    LimpetController,
45
46    /// A slot reserved for a fighter bay.
47    FighterBay,
48
49    /// Optional internal slot reserved for military modules.
50    Military,
51
52    /// Core internal slot for the different core modules.
53    CoreInternal(CoreSlot),
54
55    /// Special "slot" reserved for the data link scanner.
56    DataLinkScanner,
57
58    /// Special "slot" reserved for the codex scanner.
59    CodexScanner,
60
61    /// Special "slot" reserved for the discovery scanner.
62    DiscoveryScanner,
63
64    // Cosmetic
65    PaintJob,
66    Decal,
67    VesselVoice,
68    Nameplate,
69    IDPlate,
70    Bobble,
71    StringLights,
72    EngineColor,
73    WeaponColor,
74    ShipKitSpoiler,
75    ShipKitWings,
76    ShipKitTail,
77    ShipKitBumper,
78}
79
80#[derive(Debug, Error)]
81pub enum ShipSlotError {
82    #[error("Failed to parse slot number in: '{0}'")]
83    FailedToParseSlotNr(String),
84
85    #[error(transparent)]
86    HardpointSizeParseError(#[from] HardpointSizeError),
87
88    #[error("Failed to parse optional internal size: {0}")]
89    OptionalInternalSizeParseError(#[source] ParseIntError),
90
91    #[error("Failed to parse ship slot: '{0}'")]
92    FailedToParse(String),
93}
94
95lazy_static! {
96    static ref UTILITY_HARDPOINT_REGEX: Regex = Regex::new(r#"^TinyHardpoint(\d+)$"#).unwrap();
97    static ref HARDPOINT_REGEX: Regex =
98        Regex::new(r#"^(Small|Medium|Large|Huge)Hardpoint(\d+)$"#).unwrap();
99    static ref MINING_HARDPOINT_REGEX: Regex =
100        Regex::new(r#"^(Small|Medium|Large|Huge)MiningHardpoint(\d+)$"#).unwrap();
101    static ref OPTIONAL_INTERNAL_REGEX: Regex = Regex::new(r#"^Slot(\d+)_Size(\d+)$"#).unwrap();
102    static ref MILITARY_REGEX: Regex = Regex::new(r#"^Military(\d+)$"#).unwrap();
103    static ref LIMPET_CONTROLLER_REGEX: Regex = Regex::new(r#"^LimpetController(\d+)$"#).unwrap();
104    static ref FIGHTER_BAY_REGEX: Regex = Regex::new(r#"^FighterBay(\d+)$"#).unwrap();
105    static ref DECAL_REGEX: Regex = Regex::new(r#"^Decal(\d+)$"#).unwrap();
106    static ref NAMEPLATE_REGEX: Regex = Regex::new(r#"^ShipName(\d+)$"#).unwrap();
107    static ref ID_PLATE_REGEX: Regex = Regex::new(r#"^ShipID(\d+)$"#).unwrap();
108    static ref BOBBLE_REGEX: Regex = Regex::new(r#"^Bobble(\d+)$"#).unwrap();
109}
110
111impl FromStr for ShipSlot {
112    type Err = ShipSlotError;
113
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        let specific = match s {
116            "ShipCockpit" => Some(ShipSlotKind::ShipCockpit),
117            "CargoHatch" => Some(ShipSlotKind::CargoHatch),
118            "PaintJob" => Some(ShipSlotKind::PaintJob),
119            "VesselVoice" => Some(ShipSlotKind::VesselVoice),
120            "DataLinkScanner" => Some(ShipSlotKind::DataLinkScanner),
121            "CodexScanner" => Some(ShipSlotKind::CodexScanner),
122            "DiscoveryScanner" => Some(ShipSlotKind::DiscoveryScanner),
123            "EngineColour" => Some(ShipSlotKind::EngineColor),
124            "WeaponColour" => Some(ShipSlotKind::WeaponColor),
125            "StringLights" => Some(ShipSlotKind::StringLights),
126            "ShipKitSpoiler" => Some(ShipSlotKind::ShipKitSpoiler),
127            "ShipKitWings" => Some(ShipSlotKind::ShipKitWings),
128            "ShipKitTail" => Some(ShipSlotKind::ShipKitTail),
129            "ShipKitBumper" => Some(ShipSlotKind::ShipKitBumper),
130            _ => None,
131        };
132
133        if let Some(kind) = specific {
134            return Ok(ShipSlot { slot_nr: 0, kind });
135        }
136
137        if let Some(captures) = UTILITY_HARDPOINT_REGEX.captures(s) {
138            let slot_nr = captures
139                .get(1)
140                .expect("Should have been captured already")
141                .as_str()
142                .parse()
143                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
144
145            return Ok(ShipSlot {
146                slot_nr,
147                kind: ShipSlotKind::UtilityMount,
148            });
149        }
150
151        if let Some(captures) = HARDPOINT_REGEX.captures(s) {
152            let size = captures
153                .get(1)
154                .expect("Should have been captured already")
155                .as_str()
156                .parse()?;
157
158            let slot_nr = captures
159                .get(2)
160                .expect("Should have been captured already")
161                .as_str()
162                .parse()
163                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
164
165            return Ok(ShipSlot {
166                slot_nr,
167                kind: ShipSlotKind::Hardpoint(size),
168            });
169        }
170
171        if let Some(captures) = MINING_HARDPOINT_REGEX.captures(s) {
172            let size = captures
173                .get(1)
174                .expect("Should have been captured already")
175                .as_str()
176                .parse()?;
177
178            let slot_nr = captures
179                .get(2)
180                .expect("Should have been captured already")
181                .as_str()
182                .parse()
183                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
184
185            return Ok(ShipSlot {
186                slot_nr,
187                kind: ShipSlotKind::MiningHardPoint(size),
188            });
189        }
190
191        if let Some(captures) = OPTIONAL_INTERNAL_REGEX.captures(s) {
192            let slot_nr = captures
193                .get(1)
194                .expect("Should have been captured already")
195                .as_str()
196                .parse()
197                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
198
199            let size = captures
200                .get(2)
201                .expect("Should have been captured already")
202                .as_str()
203                .parse()
204                .map_err(ShipSlotError::OptionalInternalSizeParseError)?;
205
206            return Ok(ShipSlot {
207                slot_nr,
208                kind: ShipSlotKind::OptionalInternal(size),
209            });
210        }
211
212        if let Some(captures) = MILITARY_REGEX.captures(s) {
213            let slot_nr = captures
214                .get(1)
215                .expect("Should have been captured already")
216                .as_str()
217                .parse()
218                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
219
220            return Ok(ShipSlot {
221                slot_nr,
222                kind: ShipSlotKind::Military,
223            });
224        }
225
226        if let Some(captures) = LIMPET_CONTROLLER_REGEX.captures(s) {
227            let slot_nr = captures
228                .get(1)
229                .expect("Should have been captured already")
230                .as_str()
231                .parse()
232                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
233
234            return Ok(ShipSlot {
235                slot_nr,
236                kind: ShipSlotKind::LimpetController,
237            });
238        }
239
240        if let Some(captures) = FIGHTER_BAY_REGEX.captures(s) {
241            let slot_nr = captures
242                .get(1)
243                .expect("Should have been captured already")
244                .as_str()
245                .parse()
246                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
247
248            return Ok(ShipSlot {
249                slot_nr,
250                kind: ShipSlotKind::FighterBay,
251            });
252        }
253
254        if let Some(captures) = DECAL_REGEX.captures(s) {
255            let slot_nr = captures
256                .get(1)
257                .expect("Should have been captured already")
258                .as_str()
259                .parse()
260                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
261
262            return Ok(ShipSlot {
263                slot_nr,
264                kind: ShipSlotKind::Decal,
265            });
266        }
267
268        if let Some(captures) = NAMEPLATE_REGEX.captures(s) {
269            let slot_nr = captures
270                .get(1)
271                .expect("Should have been captured already")
272                .as_str()
273                .parse()
274                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
275
276            return Ok(ShipSlot {
277                slot_nr,
278                kind: ShipSlotKind::Nameplate,
279            });
280        }
281
282        if let Some(captures) = ID_PLATE_REGEX.captures(s) {
283            let slot_nr = captures
284                .get(1)
285                .expect("Should have been captured already")
286                .as_str()
287                .parse()
288                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
289
290            return Ok(ShipSlot {
291                slot_nr,
292                kind: ShipSlotKind::IDPlate,
293            });
294        }
295
296        if let Some(captures) = BOBBLE_REGEX.captures(s) {
297            let slot_nr = captures
298                .get(1)
299                .expect("Should have been captured already")
300                .as_str()
301                .parse()
302                .map_err(|_| ShipSlotError::FailedToParseSlotNr(s.to_string()))?;
303
304            return Ok(ShipSlot {
305                slot_nr,
306                kind: ShipSlotKind::Bobble,
307            });
308        }
309
310        if let Ok(core_slot) = s.parse() {
311            return Ok(ShipSlot {
312                slot_nr: 1,
313                kind: ShipSlotKind::CoreInternal(core_slot),
314            });
315        }
316
317        Err(ShipSlotError::FailedToParse(s.to_string()))
318    }
319}
320
321from_str_deserialize_impl!(ShipSlot);
322
323impl Display for ShipSlot {
324    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
325        match &self.kind {
326            ShipSlotKind::ShipCockpit => write!(f, "Ship Cockpit"),
327            ShipSlotKind::CargoHatch => write!(f, "Cargo Hatch"),
328            ShipSlotKind::UtilityMount => write!(f, "Utility Mount"),
329            ShipSlotKind::Hardpoint(size) => write!(f, "{} Hardpoint", size.size_str()),
330            ShipSlotKind::MiningHardPoint(size) => {
331                write!(f, "{} Mining Hardpoint", size.size_str())
332            }
333            ShipSlotKind::OptionalInternal(size) => write!(f, "Size {size} Optional Internal"),
334            ShipSlotKind::Military => write!(f, "Military Slot"),
335            ShipSlotKind::LimpetController => write!(f, "Limpet Controller Slot"),
336            ShipSlotKind::FighterBay => write!(f, "Fighter Bay Slot"),
337            ShipSlotKind::CoreInternal(core_slot) => write!(f, "{core_slot} Core Internal"),
338            ShipSlotKind::DataLinkScanner => write!(f, "Data Link Scanner"),
339            ShipSlotKind::CodexScanner => write!(f, "Codex Scanner"),
340            ShipSlotKind::DiscoveryScanner => write!(f, "Discovery Scanner"),
341
342            // Cosmetic
343            ShipSlotKind::PaintJob => write!(f, "Paint job"),
344            ShipSlotKind::Decal => write!(f, "Decal"),
345            ShipSlotKind::VesselVoice => write!(f, "COVAS Voice"),
346            ShipSlotKind::Nameplate => write!(f, "Nameplate"),
347            ShipSlotKind::IDPlate => write!(f, "ID-Plate"),
348            ShipSlotKind::Bobble => write!(f, "Bobble"),
349            ShipSlotKind::StringLights => write!(f, "String Lights"),
350            ShipSlotKind::EngineColor => write!(f, "Engine Colour"),
351            ShipSlotKind::WeaponColor => write!(f, "Weapon Colour"),
352            ShipSlotKind::ShipKitSpoiler => write!(f, "Ship Kit Spoiler"),
353            ShipSlotKind::ShipKitWings => write!(f, "Ship Kit Wing"),
354            ShipSlotKind::ShipKitTail => write!(f, "Ship Kit Tail"),
355            ShipSlotKind::ShipKitBumper => write!(f, "Ship Kit Bumper"),
356        }
357    }
358}