elite_journal 0.7.0

Elite: Dangerous journal file stuctures and parsers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
//! What a commander's scanners found, and how much there is left to find
//!
//! `StarPos` is written by the game on three events only, `Location`,
//! `FSDJump` and `CarrierJump`, and several of these name no system either.
//! EDDN requires a sender to add both before forwarding, so an event arrives
//! carrying them or not according to which feed it came down. Neither is
//! asked for here. `SystemAddress` is on all of them and is what everything
//! downstream hangs off.

use crate::body::Discovery;
use crate::prelude::*;
use chrono::{DateTime, Utc};
use serde::{de, Deserialize, Deserializer};
use std::collections::BTreeMap as Map;

#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct BuyExplorationData {
    /// System name of purchased data
    pub system: String,
    /// Cost for system data
    pub cost: u64,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct SellExplorationData {
    /// List of system names which were discovered
    pub systems: Vec<String>,
    /// List of **body** names which were discovered
    pub discovered: Vec<String>,
    /// Credit value for the discoveries
    pub base_value: u64,
    /// Credit bonus for efficency
    /// TODO: Are there other ways to get a bonus?
    pub bonus: u64,
    /// Total credit value, `base_value` + `bonus` + other factors
    ///
    /// Other factors are things like the 200% Li Yong Rui bonus.
    pub total_earnings: u64,
}

/// How close a look a `Scan` was
///
/// The five the Player Journal manual gives, and whatever else the game
/// writes. [`ScanType::Other`] rather than an error because `ScanType` is one
/// field of a thirty-field message and refusing it would refuse the scan: the
/// day a sixth kind is added, every body scanned that way would go unrecorded
/// galaxy-wide until this list caught up. It is not a place to leave a kind
/// sitting, though, which is why the sync warns on one -- what arrives in that
/// log is what to add here.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScanType {
    Basic,
    Detailed,
    NavBeacon,
    NavBeaconDetail,
    AutoScan,
    Other(String),
}

impl<'de> Deserialize<'de> for ScanType {
    /// Read the name, and keep it whole where it is not one of the five
    ///
    /// By hand because serde cannot do both halves at once. Derived, the
    /// variants read from their own names and an unfamiliar one is an error;
    /// `#[serde(untagged)]` gets the catch-all but stops the unit variants
    /// reading from a string at all, so every scan would come back as
    /// [`ScanType::Other`]. This is the one-line match those attributes were
    /// standing in for.
    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
        let name = String::deserialize(de)?;
        Ok(match name.as_str() {
            "Basic" => ScanType::Basic,
            "Detailed" => ScanType::Detailed,
            "NavBeacon" => ScanType::NavBeacon,
            "NavBeaconDetail" => ScanType::NavBeaconDetail,
            "AutoScan" => ScanType::AutoScan,
            _ => ScanType::Other(name),
        })
    }
}

impl ScanType {
    /// Whether this is a nav beacon handing over a system it did not look at
    ///
    /// A beacon is read once and answers for every body in the system at
    /// once, so the scans it writes are a transcription of what the beacon
    /// holds rather than one commander's look at one body. The discovery flag
    /// on them is not to be trusted: they carry `WasDiscovered: false` beside
    /// `WasMapped: true`, which cannot both be true of one body, nobody having
    /// mapped what nobody found -- 56 of 244 beacon scans in ten minutes of
    /// EDDN, against 11 of 1,095 ordinary detailed ones. Sol arrives this way,
    /// all forty of its objects in a single instant with the flag clear, which
    /// read as one commander having discovered the solar system.
    pub fn is_beacon(&self) -> bool {
        matches!(self, ScanType::NavBeacon | ScanType::NavBeaconDetail)
    }
}

/// What a `Scan` turned out to be about
#[derive(Debug)]
pub enum ScanTarget {
    Star(Star),
    Body(Body),
    Cluster(Cluster),
    Ring(Ring),
}

impl ScanTarget {
    /// The `BodyID` of whatever was scanned
    ///
    /// Every kind of scan target is one of the system's numbered bodies, so
    /// all four carry an id. Which kind it is does not matter to something
    /// counting distinct bodies.
    pub fn body_id(&self) -> i16 {
        match self {
            ScanTarget::Star(star) => star.id,
            ScanTarget::Body(body) => body.id,
            ScanTarget::Cluster(cluster) => cluster.id,
            ScanTarget::Ring(ring) => ring.id,
        }
    }

    /// What the scan said had already been done to whatever it looked at
    ///
    /// All four carry it, and what it means is the same for all four: not
    /// what the body is, but what somebody had made of it before this scan
    /// was taken.
    pub fn discovery(&self) -> &Discovery {
        match self {
            ScanTarget::Star(star) => &star.discovery,
            ScanTarget::Body(body) => &body.discovery,
            ScanTarget::Cluster(cluster) => &cluster.discovery,
            ScanTarget::Ring(ring) => &ring.discovery,
        }
    }
}

impl<'de> Deserialize<'de> for ScanTarget {
    /// Read the field that tells the four apart, then read that one variant
    ///
    /// `#[serde(untagged)]` instead tries each in turn and, when none fits,
    /// reports only that none fitted. Which field of which variant was wrong
    /// it does not say, and a scan is thirty fields wide.
    ///
    /// It is also unsound here. A cluster asks for the little that every scan
    /// carries, so under `untagged` it would accept a star that had failed its
    /// own variant over a single missing field, and the star would be filed as
    /// a stretch of belt.
    ///
    /// Each of the four is asked for by something it has rather than by
    /// something it lacks: a star carries `StarType`, a planet `PlanetClass`, a
    /// cluster lies in a ring and names it as the nearest of its parents, and a
    /// ring carries an orbit and nothing of substance. A scan answering to none
    /// of them is reported, since a shape nobody has modeled stored as the
    /// nearest thing to hand is worse than a shape nobody has modeled said out
    /// loud.
    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
        let scan = serde_json::Value::deserialize(de)?;

        if scan.get("StarType").is_some() {
            Star::deserialize(scan).map(ScanTarget::Star)
        } else if scan.get("PlanetClass").is_some() {
            Body::deserialize(scan).map(ScanTarget::Body)
        } else if lies_in_a_ring(&scan) {
            Cluster::deserialize(scan).map(ScanTarget::Cluster)
        } else if carries_only_an_orbit(&scan) {
            Ring::deserialize(scan).map(ScanTarget::Ring)
        } else {
            return Err(de::Error::custom(format!(
                "a scan of no kind read here: {}",
                scan.get("BodyName")
                    .and_then(|name| name.as_str())
                    .unwrap_or("something unnamed")
            )));
        }
        .map_err(de::Error::custom)
    }
}

/// Whether the nearest thing a scan hangs off is a ring
///
/// What a belt cluster is: a stretch of one of the rings a star or a planet
/// carries. The game says so in the first of its parents, nearest first, and
/// says it of nothing else.
fn lies_in_a_ring(scan: &serde_json::Value) -> bool {
    scan.get("Parents")
        .and_then(|parents| parents.get(0))
        .is_some_and(|nearest| nearest.get("Ring").is_some())
}

/// Whether a scan says where a thing goes and nothing about the thing
///
/// What a ring is: a name, an id, what it goes round, and the path. A star and
/// a planet each carry what they are made of as well, and of that a radius and
/// a rotation are the two every one of them has. So a scan carrying either is
/// not a ring however much else it is missing, and a planet that arrived
/// without its class is reported rather than filed as a path.
fn carries_only_an_orbit(scan: &serde_json::Value) -> bool {
    scan.get("SemiMajorAxis").is_some()
        && scan.get("Radius").is_none()
        && scan.get("RotationPeriod").is_none()
}

/// A ring, scanned in its own right rather than as something a body carries
///
/// A body's own scan lists the rings it has, with what they are made of and how
/// wide they are. This is the other way the game reports one: as a body in the
/// numbering, going round the body it belongs to, carrying an orbit and nothing
/// else. It is the orbit that tells it from a belt cluster, which lies in a ring
/// and has none.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Ring {
    #[serde(rename = "BodyName")]
    pub name: String,
    #[serde(rename = "BodyID")]
    pub id: i16,
    /// The body it goes round, nearest first
    #[serde(default)]
    pub parents: Vec<Map<String, i16>>,
    #[serde(rename = "DistanceFromArrivalLS")]
    pub distance_from_arrival: Option<f32>,
    #[serde(flatten)]
    pub orbit: Orbit,
    #[serde(flatten)]
    pub discovery: Discovery,
}

/// A belt cluster, which is scanned as a body and has none of a body's figures
///
/// A quarter of the scans EDDN carries are these. No class, mass, radius or
/// temperature, because there is no single object there to measure: it is a
/// stretch of a belt, named for the ring it belongs to and numbered among the
/// system's bodies.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Cluster {
    #[serde(rename = "BodyName")]
    pub name: String,
    #[serde(rename = "BodyID")]
    pub id: i16,
    /// The ring it lies in, and what that ring goes round
    #[serde(default)]
    pub parents: Vec<Map<String, i16>>,
    #[serde(rename = "DistanceFromArrivalLS")]
    pub distance_from_arrival: Option<f32>,
    #[serde(flatten)]
    pub discovery: Discovery,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Scan {
    /// How close a look was taken, where the sender says
    ///
    /// [`None`] because not every uploader sends it, which is why nothing may
    /// insist on reading it. What does read it reads [`ScanType::is_beacon`],
    /// a beacon's scans being worth less than they look.
    pub scan_type: Option<ScanType>,
    pub star_system: String,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,
    #[serde(flatten)]
    pub target: ScanTarget,

    #[serde(flatten)]
    pub other: serde_json::Value,
}

/// Signals read off a body from orbit, which the honk finds
///
/// The same kinds and counts [`SAASignalsFound`] reports, seen from further
/// off: the honk finds them, a surface scan is what maps them. Either may
/// arrive first, and either may arrive for a body nothing has scanned.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct FssBodySignals {
    #[serde(rename = "BodyName")]
    pub body_name: Option<String>,
    #[serde(rename = "BodyID")]
    pub body_id: i16,

    pub star_system: Option<String>,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,

    pub signals: Vec<Signal>,
}

/// Everything hanging in a system that is not a body
///
/// Stations, megaships, installations, beacons, and the unidentified sources
/// that come and go.
#[derive(Debug)]
pub struct FssSignalDiscovered {
    pub star_system: Option<String>,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,

    /// The signals, however many of them arrived together
    pub signals: Vec<SystemSignal>,
}

impl<'de> Deserialize<'de> for FssSignalDiscovered {
    /// Read EDDN's batch and the game's lone signal as the one thing
    ///
    /// The game writes an event per signal, with the signal's own fields on
    /// the event. EDDN gathers a system's worth under `signals`, each with
    /// the stamp it was seen at, so the outer timestamp is the first one's.
    /// That key is what tells the two apart, and a lone signal is a batch of
    /// one.
    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
        let found = serde_json::Value::deserialize(de)?;

        let signals = match found.get("signals") {
            Some(batch) => Vec::deserialize(batch),
            None => SystemSignal::deserialize(&found).map(|one| vec![one]),
        }
        .map_err(de::Error::custom)?;

        let address = found
            .get("SystemAddress")
            .ok_or_else(|| de::Error::missing_field("SystemAddress"))?;

        // A key that is there and null is a key that says nothing, which is
        // what a derived `Option` reads it as. Read as the bare type instead
        // it would refuse the whole message over a field the event is
        // allowed not to carry.
        let said = |field| found.get(field).filter(|value| !value.is_null());

        Ok(FssSignalDiscovered {
            star_system: said("StarSystem")
                .map(String::deserialize)
                .transpose()
                .map_err(de::Error::custom)?,
            star_pos: said("StarPos")
                .map(Coordinate::deserialize)
                .transpose()
                .map_err(de::Error::custom)?,
            system_address: i64::deserialize(address)
                .map_err(de::Error::custom)?,
            signals,
        })
    }
}

/// One signal out of an [`FssSignalDiscovered`] batch
///
/// Only the name is certain. What kind of thing it is, who spawned it and how
/// dangerous it is are all told where the game bothered to say, which depends
/// on what the signal is.
///
/// How long it has left is never told. The journal carries it and the schema
/// disallows it, so a signal that has since despawned is indistinguishable
/// from one still there apart from how long ago this was sent.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct SystemSignal {
    /// When it was seen, where the signal was one of a batch
    ///
    /// Spelled lowercase alone among the fields of this event, which is what
    /// the schema says, so it is held out of the renaming by hand. [`None`]
    /// for a signal the game wrote on its own, which is stamped by the entry
    /// carrying it.
    #[serde(rename = "timestamp")]
    pub timestamp: Option<DateTime<Utc>>,
    pub signal_name: String,
    pub signal_type: Option<String>,
    /// Permanent where [`Some(true)`], which is as near an expiry as there is
    pub is_station: Option<bool>,
    #[serde(rename = "USSType")]
    pub uss_type: Option<String>,
    pub spawning_state: Option<String>,
    pub spawning_faction: Option<String>,
    pub spawning_power: Option<String>,
    pub opposing_power: Option<String>,
    pub threat_level: Option<i32>,
}

/// A codex sighting: a kind of thing, found somewhere
///
/// Names the system `System`, which no other event does. Whether the sender
/// was first to it is not here and cannot be -- the schema disallows it as
/// personal data -- so this says a thing was found, not that it was
/// discovered.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct CodexEntry {
    #[serde(rename = "System")]
    pub system_name: String,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,

    #[serde(rename = "EntryID")]
    pub entry_id: i64,
    /// Not required by the schema, though always sent in practice
    pub name: Option<String>,
    pub category: Option<String>,
    pub sub_category: Option<String>,
    pub region: Option<String>,

    #[serde(rename = "BodyID")]
    pub body_id: Option<i16>,
    pub body_name: Option<String>,
    pub nearest_destination: Option<String>,
    pub latitude: Option<f64>,
    pub longitude: Option<f64>,
}

/// The honk: what a system holds, counted before any of it is identified
///
/// The first thing done on arriving somewhere, and the only event that says
/// how much there is to find. Everything else describes what has been found.
///
/// Names the system `SystemName` where nearly every other event calls it
/// `StarSystem`. It is not a mistake in the schema, and a struct that assumes
/// otherwise reads nothing.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct FssDiscoveryScan {
    #[serde(rename = "SystemName")]
    pub system_name: String,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,

    /// Bodies in the system: stars, planets, moons
    pub body_count: i32,
    /// Everything else the honk finds, being belts and rings
    ///
    /// Never appears in a body table, here or in the game's own, because
    /// none of it is a body.
    pub non_body_count: i32,
}

/// Every body in a system found, which fixes the count as certain
///
/// Says the same thing [`FssDiscoveryScan`] does and says it having finished:
/// the honk's count is what the sensors made of the system on arrival, this
/// is the tally once every one of them has been resolved.
///
/// Names the system `SystemName`, as [`FssDiscoveryScan`] does.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct FssAllBodiesFound {
    #[serde(rename = "SystemName")]
    pub system_name: String,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,

    /// Bodies in the system, all of them now accounted for
    pub count: i32,
}

/// A nav beacon read, which hands over the system's body count for free
///
/// The count is the same quantity [`FssDiscoveryScan`] reports, arrived at by
/// reading a beacon rather than by honking. Unlike those two this event names
/// the system `StarSystem`, as most events do.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct NavBeaconScan {
    pub star_system: Option<String>,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,

    /// Bodies in the system
    pub num_bodies: i32,
}

/// The center of mass a close pair goes round, scanned as a body in its own
/// right
///
/// [`None`] for the orbit where the barycenter goes round nothing, which is
/// what the one at the root of a multi-star system comes back as.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ScanBaryCentre {
    pub star_system: String,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,
    #[serde(rename = "BodyID")]
    pub body_id: i16,
    #[serde(flatten)]
    pub orbit: Option<Orbit>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct SAASignalsFound {
    /// The body the surface scan was of, named and numbered
    ///
    /// Not a [`Body`]. A scan of a body says what it is made of and how it
    /// moves; this says which body signals were found on and nothing else
    /// about it, so there is no body here to describe. Asking for one is
    /// what stopped this event from being read at all: every message failed
    /// on a missing `Body` field that the game has never sent.
    #[serde(rename = "BodyName")]
    pub body_name: String,
    #[serde(rename = "BodyID")]
    pub body_id: i16,

    pub star_system: Option<String>,
    pub star_pos: Option<Coordinate>,
    pub system_address: i64,

    /// Detected nearby signals
    pub signals: Vec<Signal>,
}