Skip to main content

ed_journals/modules/logs/content/log_event_content/
scan_event.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use serde_json::Value;
3
4use crate::galaxy::{Gravity, LocalDistance, PlanetComposition};
5use crate::modules::galaxy::{
6    Atmosphere, AtmosphereElement, AtmosphereType, OrbitInfo, PlanetClass, RingClass, StarClass,
7    StarLuminosity, TerraformState, Volcanism,
8};
9use crate::modules::materials::Material;
10
11#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
12#[serde(rename_all = "PascalCase")]
13pub struct ScanEvent {
14    pub scan_type: ScanEventScanType,
15    pub body_name: String,
16
17    #[serde(rename = "BodyID")]
18    pub body_id: u8,
19
20    #[serde(default)]
21    pub parents: Vec<ScanEventParent>,
22    pub star_system: String,
23    pub system_address: u64,
24
25    #[serde(rename = "DistanceFromArrivalLS")]
26    pub distance_from_arrival: LocalDistance,
27
28    #[serde(default)]
29    pub was_discovered: bool,
30
31    #[serde(default)]
32    pub was_mapped: bool,
33
34    /// [None] value should be considered a belt cluster
35    #[serde(flatten)]
36    pub kind: ScanEventKind,
37}
38
39#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
40#[serde(rename_all = "PascalCase")]
41pub enum ScanEventScanType {
42    Basic,
43    NavBeaconDetail,
44    AutoScan,
45    Detailed,
46}
47
48impl ScanEventScanType {
49    pub fn is_basic(&self) -> bool {
50        matches!(self, ScanEventScanType::Basic)
51    }
52
53    pub fn is_nav_beacon_detail(&self) -> bool {
54        matches!(self, ScanEventScanType::NavBeaconDetail)
55    }
56
57    pub fn is_auto_scan(&self) -> bool {
58        matches!(self, ScanEventScanType::AutoScan)
59    }
60
61    pub fn is_detailed(&self) -> bool {
62        matches!(self, ScanEventScanType::Detailed)
63    }
64}
65
66#[derive(Debug, Serialize, Clone, PartialEq)]
67pub enum ScanEventKind {
68    Star(ScanEventStar),
69    Planet(ScanEventPlanet),
70    BeltCluster(ScanEventBeltCluster),
71}
72
73impl ScanEventKind {
74    pub fn is_star(&self) -> bool {
75        matches!(self, ScanEventKind::Star(_))
76    }
77
78    pub fn star(&self) -> Option<&ScanEventStar> {
79        if let ScanEventKind::Star(star) = self {
80            return Some(star);
81        }
82
83        None
84    }
85
86    pub fn is_planet(&self) -> bool {
87        matches!(self, ScanEventKind::Planet(_))
88    }
89
90    pub fn planet(&self) -> Option<&ScanEventPlanet> {
91        if let ScanEventKind::Planet(planet) = self {
92            return Some(planet);
93        }
94
95        None
96    }
97
98    pub fn is_belt_cluster(&self) -> bool {
99        matches!(self, ScanEventKind::BeltCluster(_))
100    }
101
102    pub fn belt_cluster(&self) -> Option<&ScanEventBeltCluster> {
103        if let ScanEventKind::BeltCluster(cluster) = self {
104            return Some(cluster);
105        }
106
107        None
108    }
109}
110
111impl<'de> Deserialize<'de> for ScanEventKind {
112    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113    where
114        D: Deserializer<'de>,
115    {
116        let value = Value::deserialize(deserializer)?;
117
118        let Value::Object(map) = &value else {
119            return Err(serde::de::Error::custom("Failed to parse scan event kind"));
120        };
121
122        // If the 'StarType' key is present, then the whole object should be parsed as a star
123        // variant or fail.
124        if map.get("StarType").is_some() {
125            return Ok(ScanEventKind::Star(
126                serde_json::from_value(value)
127                    .map_err(|e| serde::de::Error::custom(format!("{e}")))?,
128            ));
129        }
130
131        // If the 'TidalLock' key is present, then the whole object should be parsed as a planet
132        // variant or fail.
133        if map.get("TidalLock").is_some() {
134            return Ok(ScanEventKind::Planet(
135                serde_json::from_value(value)
136                    .map_err(|e| serde::de::Error::custom(format!("{e}")))?,
137            ));
138        }
139
140        // It none of the above match only then should it be considered a belt cluster.
141        Ok(ScanEventKind::BeltCluster(
142            serde_json::from_value(value).map_err(|e| serde::de::Error::custom(format!("{e}")))?,
143        ))
144    }
145}
146
147#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
148#[serde(rename_all = "PascalCase")]
149pub struct ScanEventStar {
150    pub star_type: StarClass,
151
152    #[serde(default)]
153    pub subclass: u8,
154    pub stellar_mass: f32,
155    pub radius: f32,
156    pub absolute_magnitude: f32,
157
158    #[serde(rename = "Age_MY")]
159    pub age_my: u32,
160    pub surface_temperature: f32,
161    pub luminosity: StarLuminosity,
162
163    /// Missing if it's a single primary star instead of a binary or star system or more stars.
164    pub orbit_info: Option<OrbitInfo>,
165    pub rotation_period: f32,
166    pub axial_tilt: f32,
167
168    #[serde(default)]
169    pub rings: Vec<ScanEventRing>,
170}
171
172#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
173#[serde(rename_all = "PascalCase")]
174pub struct ScanEventPlanet {
175    pub tidal_lock: bool,
176    pub terraform_state: TerraformState,
177    pub planet_class: PlanetClass,
178    pub atmosphere: Atmosphere,
179    pub atmosphere_type: Option<AtmosphereType>,
180
181    #[serde(default)]
182    pub atmosphere_composition: Vec<ScanEventPlanetAtmosphereComposition>,
183    pub volcanism: Volcanism,
184
185    #[serde(rename = "MassEM")]
186    pub mass_em: f32,
187
188    /// Radius of the planet in meters.
189    pub radius: f32,
190    pub surface_gravity: Gravity,
191    pub surface_temperature: f32,
192    pub surface_pressure: f32,
193    pub landable: bool,
194
195    #[serde(default)]
196    pub materials: Vec<ScanEventPlanetMaterial>,
197    pub composition: Option<PlanetComposition>,
198
199    #[serde(flatten)]
200    pub orbit_info: OrbitInfo,
201    pub rotation_period: f32,
202    pub axial_tilt: f32,
203
204    #[serde(default)]
205    pub rings: Vec<ScanEventRing>,
206}
207
208#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
209#[serde(rename_all = "PascalCase")]
210pub struct ScanEventPlanetAtmosphereComposition {
211    pub name: AtmosphereElement,
212    pub percent: f32,
213}
214
215#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
216#[serde(rename_all = "PascalCase")]
217pub struct ScanEventPlanetMaterial {
218    pub name: Material,
219    pub percent: f32,
220}
221
222#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
223#[serde(rename_all = "PascalCase")]
224pub enum ScanEventParent {
225    Null(u8),
226    Star(u8),
227    Ring(u8),
228    Planet(u8),
229}
230
231#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
232#[serde(rename_all = "PascalCase")]
233pub struct ScanEventRing {
234    pub name: String,
235    pub ring_class: RingClass,
236
237    #[serde(rename = "MassMT")]
238    pub mass_mt: f32,
239    pub inner_rad: f32,
240    pub outer_rad: f32,
241}
242
243/// This struct is always empty and is just here to make sure [serde] recognizes the empty variant.
244#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
245pub struct ScanEventBeltCluster {}
246
247#[cfg(test)]
248mod tests {
249    use crate::galaxy::LocalDistance;
250    use crate::modules::logs::content::log_event_content::scan_event::ScanEvent;
251
252    #[test]
253    fn scan_event_is_parsed_correctly() {
254        let value = serde_json::from_str::<ScanEvent>(
255            r#"
256            {
257                "timestamp": "2022-10-11T19:59:10Z",
258                "event": "Scan",
259                "ScanType": "AutoScan",
260                "BodyName": "Etain A Belt Cluster 1",
261                "BodyID": 2,
262                "Parents": [
263                    {
264                        "Ring": 1
265                    },
266                    {
267                        "Star": 0
268                    }
269                ],
270                "StarSystem": "Etain",
271                "SystemAddress": 2869977884057,
272                "DistanceFromArrivalLS": 4.884683,
273                "WasDiscovered": true,
274                "WasMapped": false
275            }
276        "#,
277        );
278
279        assert!(value.is_ok());
280    }
281
282    #[test]
283    fn distance_is_converted_correctly() {
284        fn assert_roughly_eq(a: f32, b: f32) {
285            assert!((a - b).abs() < 0.0001);
286        }
287
288        let distance = LocalDistance(1000.0);
289        assert_roughly_eq(distance.as_au(), 2.0);
290
291        let distance = LocalDistance::from_au(2.0);
292        assert_roughly_eq(distance.as_ls(), 1000.0);
293    }
294}