use crate::entry::route::NavRoute;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(tag = "event")]
pub enum Event {
Fileheader(startup::Fileheader),
Cargo(startup::Manifest),
NewCommander(startup::NewCommander),
ClearSavedGame(startup::Commander),
Commander(startup::Commander),
LoadGame(startup::LoadGame),
Materials(startup::Materials),
Location(travel::Location),
Docked(travel::Docked),
DockingGranted(travel::DockingGranted),
DockingRequested(travel::DockingRequested),
DockingDenied(travel::DockingDenied),
DockingCancelled(travel::DockingCancelled),
DockingTimeout(travel::DockingTimeout),
Undocked(travel::Undocked),
Liftoff(travel::Liftoff),
LeaveBody(travel::LeaveBody),
ApproachBody(travel::ApproachBody),
ApproachSettlement(travel::ApproachSettlement),
#[serde(rename = "FSDTarget")]
FsdTarget(travel::FsdTarget),
#[serde(rename = "FSDJump")]
FsdJump(travel::FsdJump),
CarrierJump(travel::CarrierJump),
NavRoute(NavRoute),
BuyExplorationData(exploration::BuyExplorationData),
SellExplorationData(exploration::SellExplorationData),
SAASignalsFound(exploration::SAASignalsFound),
Scan(exploration::Scan),
ScanBaryCentre(exploration::ScanBaryCentre),
#[serde(rename = "FSSDiscoveryScan")]
FssDiscoveryScan(exploration::FssDiscoveryScan),
#[serde(rename = "FSSAllBodiesFound")]
FssAllBodiesFound(exploration::FssAllBodiesFound),
NavBeaconScan(exploration::NavBeaconScan),
#[serde(rename = "FSSBodySignals")]
FssBodySignals(exploration::FssBodySignals),
#[serde(rename = "FSSSignalDiscovered")]
FssSignalDiscovered(exploration::FssSignalDiscovered),
CodexEntry(exploration::CodexEntry),
#[serde(other)]
Other,
}
pub mod exploration;
pub mod startup;
pub mod travel;
#[cfg(test)]
mod tests {
use super::*;
use crate::entry::incremental::exploration::{ScanTarget, ScanType};
use crate::entry::Entry;
use crate::system::Economy;
fn event(json: &str) -> Event {
serde_json::from_str::<Entry<Event>>(json)
.expect("message should parse")
.event
}
fn assert_read(json: &str) -> Event {
let event = event(json);
assert!(
!matches!(event, Event::Other),
"fell through to Other: {}",
json,
);
event
}
#[test]
fn the_journal_schema() {
assert!(matches!(
assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "FSDJump",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803
}"#
),
Event::FsdJump(_)
));
assert!(matches!(
assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "Location",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"Docked": false
}"#
),
Event::Location(_)
));
}
#[test]
fn a_carrier_jump_is_a_visit() {
let Event::CarrierJump(jump) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "CarrierJump",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"SystemAllegiance": "Federation",
"SystemEconomy": "$economy_Refinery;",
"Population": 22780919531,
"Docked": true,
"StationName": "K7Q-BQL",
"StationType": "FleetCarrier",
"MarketID": 3700571136
}"#,
) else {
panic!("not a carrier jump")
};
assert_eq!(jump.system.name, "Sol");
assert_eq!(jump.system.population, Some(22780919531));
assert_eq!(jump.docked, Some(true));
assert_eq!(
jump.station.as_ref().map(|s| s.name.as_str()),
Some("K7Q-BQL"),
);
}
#[test]
fn a_carrier_jump_without_docked() {
let Event::CarrierJump(jump) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "CarrierJump",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803
}"#,
) else {
panic!("not a carrier jump")
};
assert_eq!(jump.docked, None);
}
#[test]
fn a_surface_scan_names_its_body_rather_than_describing_it() {
let Event::SAASignalsFound(found) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "SAASignalsFound",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyName": "Sol 4",
"BodyID": 12,
"Signals": [
{ "Type": "$SAA_SignalType_Geological;", "Count": 3 },
{ "Type": "$SAA_SignalType_Biological;", "Count": 1 }
]
}"#,
) else {
panic!("not a surface scan")
};
assert_eq!(found.body_name, "Sol 4");
assert_eq!(found.body_id, 12);
assert_eq!(found.signals.len(), 2);
assert_eq!(found.signals[0].count, 3);
}
#[test]
fn the_honk_counts_bodies_and_the_rest() {
let Event::FssDiscoveryScan(honk) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "FSSDiscoveryScan",
"SystemName": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyCount": 40,
"NonBodyCount": 10,
"horizons": true,
"odyssey": true
}"#,
) else {
panic!("not a honk")
};
assert_eq!(honk.system_name, "Sol");
assert_eq!(honk.body_count, 40);
assert_eq!(honk.non_body_count, 10);
}
#[test]
fn every_body_found_is_the_same_count() {
let Event::FssAllBodiesFound(found) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "FSSAllBodiesFound",
"SystemName": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"Count": 40
}"#,
) else {
panic!("not an all-bodies-found")
};
assert_eq!(found.count, 40);
}
#[test]
fn a_beacon_names_its_system_the_usual_way() {
let Event::NavBeaconScan(scan) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "NavBeaconScan",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"NumBodies": 40
}"#,
) else {
panic!("not a beacon scan")
};
assert_eq!(scan.star_system.as_deref(), Some("Sol"));
assert_eq!(scan.num_bodies, 40);
}
#[test]
fn the_honk_finds_signals_on_bodies() {
let Event::FssBodySignals(found) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "FSSBodySignals",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyID": 12,
"Signals": [
{ "Type": "$SAA_SignalType_Geological;", "Count": 3 }
]
}"#,
) else {
panic!("not body signals")
};
assert_eq!(found.body_id, 12);
assert_eq!(found.body_name, None);
assert_eq!(found.signals.len(), 1);
}
#[test]
fn system_signals_arrive_in_batches() {
let Event::FssSignalDiscovered(found) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "FSSSignalDiscovered",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"signals": [
{
"timestamp": "2026-08-08T12:00:00Z",
"SignalName": "Abraham Lincoln",
"IsStation": true
},
{
"timestamp": "2026-08-08T12:05:00Z",
"SignalName": "$USS_HighGradeEmissions;",
"SignalType": "USS",
"USSType": "$USS_Type_VeryValuableSalvage;",
"SpawningState": "$FactionState_None;",
"SpawningFaction": "Mother Gaia",
"ThreatLevel": 0
}
]
}"#,
) else {
panic!("not signals discovered")
};
assert_eq!(found.signals.len(), 2);
assert_eq!(found.signals[0].signal_name, "Abraham Lincoln");
assert_eq!(found.signals[0].is_station, Some(true));
assert_eq!(
found.signals[1].uss_type.as_deref(),
Some("$USS_Type_VeryValuableSalvage;"),
);
assert_eq!(found.signals[1].threat_level, Some(0));
assert_ne!(found.signals[0].timestamp, found.signals[1].timestamp);
}
#[test]
fn a_codex_sighting_names_its_system_its_own_way() {
let Event::CodexEntry(entry) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "CodexEntry",
"System": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"EntryID": 2100701,
"Name": "$Codex_Ent_Sulphur_Name;",
"Category": "$Codex_Category_Biology;",
"SubCategory": "$Codex_SubCategory_Organic_Structures;",
"Region": "$Codex_RegionName_18;",
"BodyID": 12,
"BodyName": "Sol 4"
}"#,
) else {
panic!("not a codex entry")
};
assert_eq!(entry.system_name, "Sol");
assert_eq!(entry.entry_id, 2100701);
assert_eq!(entry.body_id, Some(12));
}
#[test]
fn a_settlement_is_a_station_somewhere_on_a_body() {
let Event::ApproachSettlement(settlement) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "ApproachSettlement",
"Name": "Bloomfield Vision Installation",
"MarketID": 3510085376,
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyID": 12,
"BodyName": "Sol 4",
"Latitude": 12.5,
"Longitude": -47.25,
"StationAllegiance": "Federation",
"StationGovernment": "$government_Corporate;",
"StationServices": ["dock", "refuel"]
}"#,
) else {
panic!("not a settlement")
};
assert_eq!(settlement.name, "Bloomfield Vision Installation");
assert_eq!(settlement.body_name, "Sol 4");
assert_eq!(settlement.latitude, Some(12.5));
assert_eq!(settlement.longitude, Some(-47.25));
assert_eq!(settlement.market_id, Some(3510085376));
}
#[test]
fn an_unread_event_is_other() {
assert!(matches!(
event(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "ProspectedAsteroid",
"Content": "$AsteroidMaterialContent_High;",
"Remaining": 100.0
}"#
),
Event::Other
));
}
#[test]
fn the_docking_events_that_only_reach_a_journal_file() {
let requested = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "DockingRequested",
"MarketID": 3226577920,
"StationName": "Ray Gateway",
"StationType": "Coriolis",
"LandingPads": { "Small": 19, "Medium": 21, "Large": 9 }
}"#,
);
let Event::DockingRequested(requested) = requested else {
panic!("not a docking request")
};
assert_eq!(requested.market_id, 3226577920);
let pads = requested.landing_pads.expect("pads should read");
assert_eq!(pads.small, 19);
assert_eq!(pads.medium, 21);
assert_eq!(pads.large, 9);
assert!(matches!(
assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "DockingCancelled",
"MarketID": 3226577920,
"StationName": "Ray Gateway",
"StationType": "Coriolis"
}"#
),
Event::DockingCancelled(_)
));
assert!(matches!(
assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "DockingTimeout",
"MarketID": 3226577920,
"StationName": "Ray Gateway",
"StationType": "Coriolis"
}"#
),
Event::DockingTimeout(_)
));
}
#[test]
fn undocking_is_read() {
let Event::Undocked(undocked) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "Undocked",
"StationName": "Ray Gateway",
"MarketID": 3226577920,
"Taxi": false,
"Multicrew": false
}"#,
) else {
panic!("not an undocking")
};
assert_eq!(undocked.station_name, "Ray Gateway");
assert_eq!(undocked.market_id, 3226577920);
}
#[test]
fn a_belt_cluster_is_scanned_without_being_measured() {
let Event::Scan(scan) = assert_read(
r#"{
"timestamp": "2026-08-11T18:00:00Z",
"event": "Scan",
"ScanType": "AutoScan",
"StarSystem": "Eol Prou JH-C d13-55",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyName": "Eol Prou JH-C d13-55 A Belt Cluster 5",
"BodyID": 5,
"Parents": [{ "Ring": 1 }, { "Star": 0 }],
"DistanceFromArrivalLS": 0.0,
"WasDiscovered": true,
"WasMapped": false
}"#,
) else {
panic!("not a scan")
};
let ScanTarget::Cluster(cluster) = scan.target else {
panic!("not a cluster")
};
assert_eq!(cluster.name, "Eol Prou JH-C d13-55 A Belt Cluster 5");
assert_eq!(cluster.id, 5);
assert!(cluster.discovery.discovered);
assert!(!cluster.discovery.mapped);
assert_eq!(cluster.parents.len(), 2);
assert_eq!(cluster.parents[0].get("Ring"), Some(&1));
}
#[test]
fn a_star_and_a_planet_are_read_as_themselves() {
let Event::Scan(star) = assert_read(
r#"{
"timestamp": "2026-08-11T18:00:00Z",
"event": "Scan",
"ScanType": "AutoScan",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyName": "Sol",
"BodyID": 0,
"Parents": [],
"StarType": "G",
"Subclass": 2,
"StellarMass": 1.0,
"Radius": 695700000.0,
"AbsoluteMagnitude": 4.83,
"Age_MY": 4600,
"SurfaceTemperature": 5778.0,
"Luminosity": "V",
"DistanceFromArrivalLS": 0.0,
"RotationPeriod": 2164000.0,
"AxialTilt": 0.126,
"WasDiscovered": true,
"WasMapped": false
}"#,
) else {
panic!("not a scan")
};
assert!(matches!(star.target, ScanTarget::Star(_)));
let Event::Scan(planet) = assert_read(
r#"{
"timestamp": "2026-08-11T18:00:00Z",
"event": "Scan",
"ScanType": "Detailed",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyName": "Earth",
"BodyID": 3,
"Parents": [{ "Star": 0 }],
"PlanetClass": "Earthlike body",
"TidalLock": false,
"MassEM": 1.0,
"Radius": 6371000.0,
"SurfaceGravity": 9.807,
"SurfaceTemperature": 288.0,
"SemiMajorAxis": 149000000000.0,
"Eccentricity": 0.0167,
"OrbitalInclination": 0.0,
"Periapsis": 114.2,
"OrbitalPeriod": 31500000.0,
"AscendingNode": 0.0,
"MeanAnomaly": 0.0,
"RotationPeriod": 86164.0,
"AxialTilt": 0.409,
"DistanceFromArrivalLS": 499.0,
"WasDiscovered": true,
"WasMapped": true
}"#,
) else {
panic!("not a scan")
};
assert!(matches!(planet.target, ScanTarget::Body(_)));
}
#[test]
fn a_star_short_a_field_says_which_field() {
let json = r#"{
"timestamp": "2026-08-11T18:00:00Z",
"event": "Scan",
"ScanType": "AutoScan",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyName": "Sol",
"BodyID": 0,
"Parents": [],
"StarType": "G",
"Subclass": 2,
"StellarMass": 1.0,
"Radius": 695700000.0,
"AbsoluteMagnitude": 4.83,
"Age_MY": 4600,
"SurfaceTemperature": 5778.0,
"DistanceFromArrivalLS": 0.0,
"RotationPeriod": 2164000.0,
"AxialTilt": 0.126,
"WasDiscovered": true,
"WasMapped": false
}"#;
let err = serde_json::from_str::<Entry<Event>>(json)
.expect_err("a star without its luminosity should not read");
assert!(
err.to_string().contains("Luminosity"),
"said nothing about the missing field: {}",
err,
);
}
#[test]
fn a_basic_scan_carries_no_temperature() {
let Event::Scan(scan) = assert_read(
r#"{
"timestamp": "2026-08-11T19:38:40Z",
"event": "Scan",
"ScanType": "Basic",
"BodyName": "Blo Eurl TI-K d8-176 A 1",
"BodyID": 5,
"Parents": [{ "Star": 1 }, { "Null": 0 }],
"StarSystem": "Blo Eurl TI-K d8-176",
"StarPos": [3403.84375, 106.25, 4807.75],
"SystemAddress": 6058512763723,
"DistanceFromArrivalLS": 566.304246,
"PlanetClass": "Sudarsky class V gas giant",
"MassEM": 816.8172,
"Radius": 71228168,
"SurfaceGravity": 64.169796,
"SemiMajorAxis": 111697691679.00085,
"Eccentricity": 0.644541,
"OrbitalInclination": -162.793724,
"Periapsis": 241.507528,
"OrbitalPeriod": 22660554.051399,
"AscendingNode": -75.687522,
"MeanAnomaly": 121.948236,
"RotationPeriod": 287282.874636,
"AxialTilt": 0.159411,
"WasDiscovered": true,
"WasMapped": false
}"#,
) else {
panic!("not a scan")
};
let ScanTarget::Body(body) = scan.target else { panic!("not a body") };
assert_eq!(body.name, "Blo Eurl TI-K d8-176 A 1");
assert_eq!(body.temperature, None);
assert_eq!(body.planet_class, "Sudarsky class V gas giant");
assert_eq!(body.gravity, 64.169796);
}
#[test]
fn a_scan_without_a_scan_type_reads() {
let scan = |json: &str| {
let Event::Scan(scan) = assert_read(json) else {
panic!("not a scan")
};
assert!(scan.scan_type.is_none());
scan.target
};
assert!(matches!(
scan(
r#"{
"timestamp": "2026-08-11T19:44:56Z",
"event": "Scan",
"BodyName": "Synuefe DO-F d12-54 A Belt Cluster 1",
"BodyID": 2,
"Parents": [{ "Ring": 1 }, { "Star": 0 }],
"StarSystem": "Synuefe DO-F d12-54",
"StarPos": [196.1875, -210.65625, 25.125],
"SystemAddress": 1865953528171,
"DistanceFromArrivalLS": 7.693174,
"WasDiscovered": true,
"WasMapped": false
}"#
),
ScanTarget::Cluster(_)
));
assert!(matches!(
scan(
r#"{
"timestamp": "2026-08-11T19:44:56Z",
"event": "Scan",
"BodyName": "Synuefe DO-F d12-54",
"BodyID": 0,
"StarSystem": "Synuefe DO-F d12-54",
"StarPos": [196.1875, -210.65625, 25.125],
"SystemAddress": 1865953528171,
"DistanceFromArrivalLS": 0,
"StarType": "F",
"Subclass": 7,
"StellarMass": 1.238281,
"Radius": 738883776,
"AbsoluteMagnitude": 4.245636,
"Age_MY": 516,
"SurfaceTemperature": 6413,
"Luminosity": "Vb",
"RotationPeriod": 395502.084419,
"AxialTilt": 0,
"WasDiscovered": true,
"WasMapped": false
}"#
),
ScanTarget::Star(_)
));
}
#[test]
fn a_scan_says_how_close_a_look_it_was() {
let scanned = |kind: &str| {
let json = format!(
r#"{{
"timestamp": "2026-08-11T19:44:56Z",
"event": "Scan",
"ScanType": "{kind}",
"BodyName": "Synuefe DO-F d12-54 A Belt Cluster 1",
"BodyID": 2,
"Parents": [{{ "Ring": 1 }}, {{ "Star": 0 }}],
"StarSystem": "Synuefe DO-F d12-54",
"StarPos": [196.1875, -210.65625, 25.125],
"SystemAddress": 1865953528171,
"DistanceFromArrivalLS": 7.693174,
"WasDiscovered": true,
"WasMapped": false
}}"#
);
let Event::Scan(scan) = assert_read(&json) else {
panic!("not a scan")
};
scan.scan_type.expect("a scan type was sent")
};
assert_eq!(scanned("Detailed"), ScanType::Detailed);
assert!(!scanned("Detailed").is_beacon());
assert!(scanned("NavBeaconDetail").is_beacon());
assert!(scanned("NavBeacon").is_beacon());
assert_eq!(
scanned("SomethingNewInUpdate20"),
ScanType::Other("SomethingNewInUpdate20".to_owned())
);
assert!(!scanned("SomethingNewInUpdate20").is_beacon());
}
#[test]
fn a_settlement_with_an_empty_faction_reads() {
let Event::ApproachSettlement(settlement) = assert_read(
r#"{
"timestamp": "2026-08-11T19:39:29Z",
"event": "ApproachSettlement",
"StarSystem": "Synuefe NL-N c23-4",
"StarPos": [860.125, -124.59375, -61.0625],
"StationFaction": {},
"SystemAddress": 1184840454858,
"Name": "$Ancient:#index=3;",
"BodyID": 18,
"BodyName": "Synuefe NL-N c23-4 B 3",
"Latitude": 5.614992,
"Longitude": -148.089981
}"#,
) else {
panic!("not a settlement")
};
assert_eq!(settlement.name, "$Ancient:#index=3;");
assert!(settlement.faction.is_none());
let Event::ApproachSettlement(named_nothing) = assert_read(
r#"{
"timestamp": "2026-08-11T19:39:29Z",
"event": "ApproachSettlement",
"StarSystem": "Synuefe NL-N c23-4",
"StarPos": [860.125, -124.59375, -61.0625],
"StationFaction": { "Name": null },
"SystemAddress": 1184840454858,
"Name": "$Ancient:#index=3;",
"BodyID": 18,
"BodyName": "Synuefe NL-N c23-4 B 3",
"Latitude": 5.614992,
"Longitude": -148.089981
}"#,
) else {
panic!("not a settlement")
};
assert!(named_nothing.faction.is_none());
}
#[test]
fn a_scan_of_no_known_kind_is_reported() {
let err = serde_json::from_str::<Entry<Event>>(
r#"{
"timestamp": "2026-08-11T20:00:00Z",
"event": "Scan",
"ScanType": "Detailed",
"StarSystem": "Sol",
"StarPos": [0.0, 0.0, 0.0],
"SystemAddress": 10477373803,
"BodyName": "Sol 5 Something New",
"BodyID": 9,
"Parents": [{ "Planet": 8 }, { "Star": 0 }],
"DistanceFromArrivalLS": 2.5,
"WasDiscovered": true,
"WasMapped": false
}"#,
)
.expect_err("a scan of no known kind should not read");
let said = err.to_string();
assert!(
said.contains("no kind read here"),
"did not say what was wrong: {}",
said,
);
assert!(
said.contains("Sol 5 Something New"),
"did not name it: {}",
said,
);
}
#[test]
fn a_cluster_is_taken_by_the_ring_it_lies_in() {
let Event::Scan(scan) = assert_read(
r#"{
"timestamp": "2026-08-11T19:44:56Z",
"event": "Scan",
"ScanType": "AutoScan",
"BodyName": "Synuefe UF-K b55-4 A A Belt Cluster 5",
"BodyID": 10,
"Parents": [{ "Ring": 5 }, { "Star": 1 }, { "Null": 0 }],
"StarSystem": "Synuefe UF-K b55-4",
"StarPos": [386.75, -220.125, 125.25],
"SystemAddress": 9472147072473,
"DistanceFromArrivalLS": 5.433045,
"WasDiscovered": true,
"WasMapped": false
}"#,
) else {
panic!("not a scan")
};
let ScanTarget::Cluster(cluster) = scan.target else {
panic!("a cluster in a ring should be read as one")
};
assert_eq!(cluster.id, 10);
assert_eq!(cluster.parents[0].get("Ring"), Some(&5));
}
#[test]
fn a_ring_scanned_in_its_own_right_is_read_as_one() {
let Event::Scan(scan) = assert_read(
r#"{
"timestamp": "2026-08-11T21:16:29Z",
"event": "Scan",
"ScanType": "AutoScan",
"BodyName": "Dryeejeae AA-A d4 D 12 A Ring",
"BodyID": 65,
"Parents": [{ "Planet": 64 }, { "Star": 6 }, { "Null": 0 }],
"StarSystem": "Dryeejeae AA-A d4",
"StarPos": [-8982.125, 1258.21875, 10460.625],
"SystemAddress": 146037542275,
"DistanceFromArrivalLS": 377022.119004,
"SemiMajorAxis": 36267683.804035,
"Eccentricity": 0,
"OrbitalInclination": 0,
"Periapsis": 0,
"OrbitalPeriod": 15402.967334,
"AscendingNode": 0,
"MeanAnomaly": 166.00566,
"WasDiscovered": false,
"WasMapped": false
}"#,
) else {
panic!("not a scan")
};
let ScanTarget::Ring(ring) = scan.target else {
panic!("a ring should not be read as anything else")
};
assert_eq!(ring.name, "Dryeejeae AA-A d4 D 12 A Ring");
assert_eq!(ring.id, 65);
assert_eq!(ring.parents[0].get("Planet"), Some(&64));
assert_eq!(ring.orbit.orbital_period, 15402.967334);
}
#[test]
fn an_orbit_without_its_last_two_still_reads() {
let Event::Scan(scan) = assert_read(
r#"{
"timestamp": "2026-08-11T22:20:15Z",
"event": "Scan",
"ScanType": "AutoScan",
"StarSystem": "Colonia",
"StarPos": [-9530.5, -910.28125, 19808.125],
"SystemAddress": 3238296097059,
"BodyName": "Colonia 7 c",
"BodyID": 50,
"Parents": [{ "Star": 44 }, { "Star": 0 }],
"PlanetClass": "Rocky body",
"TidalLock": true,
"MassEM": 0.029988,
"Radius": 2152178.5,
"SurfaceGravity": 2.580479,
"SurfaceTemperature": 128.0793,
"SurfacePressure": 0,
"Landable": true,
"Atmosphere": "",
"AtmosphereType": "None",
"Volcanism": "",
"TerraformState": "",
"Composition": { "Ice": 0, "Metal": 0.088844, "Rock": 0.911156 },
"SemiMajorAxis": 10566104650.497437,
"Eccentricity": 0.000385,
"OrbitalInclination": -0.010772,
"Periapsis": 185.317867,
"OrbitalPeriod": 5153734.087944,
"RotationPeriod": 5153831.783096,
"AxialTilt": 0.032617,
"WasDiscovered": true,
"WasMapped": true
}"#,
) else {
panic!("not a scan")
};
let ScanTarget::Body(body) = scan.target else { panic!("not a body") };
assert_eq!(body.orbit.semi_major_axis, 10566104650.497437);
assert_eq!(body.orbit.orbital_period, 5153734.087944);
assert_eq!(body.orbit.ascending_node, None);
assert_eq!(body.orbit.mean_anomaly, None);
}
#[test]
fn a_rescue_ship_docks_under_its_own_economy() {
let Event::Docked(docked) = assert_read(
r#"{
"timestamp": "2026-08-11T22:26:55Z",
"event": "Docked",
"StarSystem": "Luyten's Star",
"StarPos": [6.5625, 2.34375, -10.25],
"SystemAddress": 7268024264097,
"StationName": "Rescue Ship Hutner",
"StationType": "MegaShip",
"MarketID": 129020287,
"StationFaction": { "Name": "Independent Rescue Coalition" },
"StationGovernment": "$government_Corporate;",
"StationEconomy": "$economy_Rescue;",
"StationEconomies": [
{ "Name": "$economy_Rescue;", "Proportion": 1 }
],
"DistFromStarLS": 297.0377,
"LandingPads": { "Small": 4, "Medium": 2, "Large": 1 }
}"#,
) else {
panic!("not a docking")
};
let economies =
docked.station.economies.as_ref().expect("an economy share");
assert_eq!(economies.len(), 1);
assert_eq!(economies[0].name, Economy::Rescue);
}
#[test]
fn a_planet_short_of_its_class_is_not_taken_for_a_ring() {
let err = serde_json::from_str::<Entry<Event>>(
r#"{
"timestamp": "2026-08-11T22:20:15Z",
"event": "Scan",
"ScanType": "Detailed",
"StarSystem": "Colonia",
"StarPos": [-9530.5, -910.28125, 19808.125],
"SystemAddress": 3238296097059,
"BodyName": "Colonia 7 c",
"BodyID": 50,
"Parents": [{ "Star": 44 }, { "Star": 0 }],
"MassEM": 0.029988,
"Radius": 2152178.5,
"SurfaceGravity": 2.580479,
"SurfaceTemperature": 128.0793,
"SemiMajorAxis": 10566104650.497437,
"Eccentricity": 0.000385,
"OrbitalInclination": -0.010772,
"Periapsis": 185.317867,
"OrbitalPeriod": 5153734.087944,
"RotationPeriod": 5153831.783096,
"AxialTilt": 0.032617,
"WasDiscovered": true,
"WasMapped": true
}"#,
)
.expect_err("a planet with no class should not read as anything");
let said = err.to_string();
assert!(
said.contains("no kind read here"),
"did not report it: {}",
said,
);
assert!(said.contains("Colonia 7 c"), "did not name it: {}", said);
}
#[test]
fn a_scan_the_game_wrote_says_where_nothing_is() {
let Event::Scan(scan) = assert_read(
r#"{
"timestamp": "2026-08-11T18:00:00Z",
"event": "Scan",
"ScanType": "AutoScan",
"StarSystem": "Sol",
"SystemAddress": 10477373803,
"BodyName": "Sol",
"BodyID": 0,
"Parents": [],
"StarType": "G",
"Subclass": 2,
"StellarMass": 1.0,
"Radius": 695700000.0,
"AbsoluteMagnitude": 4.83,
"Age_MY": 4600,
"SurfaceTemperature": 5778.0,
"Luminosity": "V",
"DistanceFromArrivalLS": 0.0,
"RotationPeriod": 2164000.0,
"AxialTilt": 0.126,
"WasDiscovered": true,
"WasMapped": false
}"#,
) else {
panic!("not a scan")
};
assert_eq!(scan.star_system, "Sol");
assert_eq!(scan.star_pos, None);
let Event::ScanBaryCentre(barycenter) = assert_read(
r#"{
"timestamp": "2021-07-27T13:52:20Z",
"event": "ScanBaryCentre",
"StarSystem": "Col 285 Sector YX-N b21-1",
"SystemAddress": 2867561768401,
"BodyID": 10,
"SemiMajorAxis": 2107998251914.978,
"Eccentricity": 0.033074,
"OrbitalInclination": 0.019013,
"Periapsis": 342.187341,
"OrbitalPeriod": 3739380657.672882,
"AscendingNode": -31.477241,
"MeanAnomaly": 64.03028
}"#,
) else {
panic!("not a barycenter scan")
};
assert_eq!(barycenter.star_pos, None);
let Event::FssDiscoveryScan(honk) = assert_read(
r#"{
"timestamp": "2020-08-03T12:52:19Z",
"event": "FSSDiscoveryScan",
"Progress": 0.68,
"BodyCount": 12,
"NonBodyCount": 11,
"SystemName": "Col 285 Sector CC-K a38-2",
"SystemAddress": 2871051298715
}"#,
) else {
panic!("not a honk")
};
assert_eq!(honk.star_pos, None);
assert_eq!(honk.body_count, 12);
let Event::FssAllBodiesFound(found) = assert_read(
r#"{
"timestamp": "2020-08-03T12:52:19Z",
"event": "FSSAllBodiesFound",
"SystemName": "Col 285 Sector CC-K a38-2",
"SystemAddress": 2871051298715,
"Count": 12
}"#,
) else {
panic!("not an all-bodies-found")
};
assert_eq!(found.star_pos, None);
let Event::CodexEntry(entry) = assert_read(
r#"{
"timestamp": "2019-05-13T13:28:51Z",
"event": "CodexEntry",
"EntryID": 1400159,
"Name": "$Codex_Ent_IceFumarole_CarbonDioxideGeysers_Name;",
"SubCategory": "$Codex_SubCategory_Geology_and_Anomalies;",
"Category": "$Codex_Category_Biology;",
"Region": "$Codex_RegionName_18;",
"System": "Hermitage",
"SystemAddress": 5363877956440,
"IsNewEntry": true
}"#,
) else {
panic!("not a codex entry")
};
assert_eq!(entry.system_name, "Hermitage");
assert_eq!(entry.star_pos, None);
}
#[test]
fn some_of_what_the_game_writes_names_no_system() {
let Event::NavBeaconScan(beacon) = assert_read(
r#"{
"timestamp": "2020-08-03T12:52:19Z",
"event": "NavBeaconScan",
"SystemAddress": 2871051298715,
"NumBodies": 12
}"#,
) else {
panic!("not a beacon scan")
};
assert_eq!(beacon.star_system, None);
assert_eq!(beacon.num_bodies, 12);
let Event::SAASignalsFound(surface) = assert_read(
r#"{
"timestamp": "2019-04-17T13:38:18Z",
"event": "SAASignalsFound",
"BodyName": "Hermitage 4 A Ring",
"SystemAddress": 5363877956440,
"BodyID": 11,
"Signals": [{ "Type": "Alexandrite", "Count": 1 }]
}"#,
) else {
panic!("not a surface scan")
};
assert_eq!(surface.star_system, None);
assert_eq!(surface.body_name, "Hermitage 4 A Ring");
let Event::FssBodySignals(orbital) = assert_read(
r#"{
"timestamp": "2022-03-17T18:20:53Z",
"event": "FSSBodySignals",
"BodyName": "Phroi Blou EW-W d1-1056 2 a",
"BodyID": 18,
"SystemAddress": 36293555558035,
"Signals": [
{ "Type": "$SAA_SignalType_Geological;", "Count": 3 }
]
}"#,
) else {
panic!("not body signals")
};
assert_eq!(orbital.star_system, None);
assert_eq!(orbital.body_id, 18);
let Event::ApproachSettlement(settlement) = assert_read(
r#"{
"timestamp": "2021-03-04T18:23:22Z",
"event": "ApproachSettlement",
"Name": "Bandi Enterprise",
"MarketID": 3510023680,
"SystemAddress": 2871051298715,
"BodyID": 25,
"BodyName": "Col 285 Sector CC-K a38-2 A 5",
"Latitude": -25.0,
"Longitude": 63.0
}"#,
) else {
panic!("not a settlement")
};
assert_eq!(settlement.system_name, None);
assert_eq!(settlement.name, "Bandi Enterprise");
}
#[test]
fn a_signal_the_game_wrote_is_a_batch_of_one() {
let Event::FssSignalDiscovered(found) = assert_read(
r#"{
"timestamp": "2022-03-17T18:20:53Z",
"event": "FSSSignalDiscovered",
"SystemAddress": 36293555558035,
"SignalName": "$USS_HighGradeEmissions;",
"SignalType": "USS",
"USSType": "$USS_Type_ValuableSalvage;",
"ThreatLevel": 0,
"TimeRemaining": 653.0
}"#,
) else {
panic!("not signals discovered")
};
assert_eq!(found.star_system, None);
assert_eq!(found.signals.len(), 1);
assert_eq!(found.signals[0].signal_name, "$USS_HighGradeEmissions;");
assert_eq!(found.signals[0].threat_level, Some(0));
assert_eq!(found.signals[0].timestamp, None);
}
#[test]
fn a_signal_batch_naming_a_null_system_names_none() {
let Event::FssSignalDiscovered(found) = assert_read(
r#"{
"timestamp": "2026-08-08T12:00:00Z",
"event": "FSSSignalDiscovered",
"StarSystem": null,
"StarPos": null,
"SystemAddress": 10477373803,
"signals": [
{
"timestamp": "2026-08-08T12:00:00Z",
"SignalName": "Abraham Lincoln"
}
]
}"#,
) else {
panic!("not signals discovered")
};
assert_eq!(found.star_system, None);
assert_eq!(found.star_pos, None);
assert_eq!(found.signals.len(), 1);
}
#[test]
fn the_log_says_a_route_was_plotted_and_not_where_it_goes() {
let Event::NavRoute(plotted) = assert_read(
r#"{
"timestamp": "2020-04-27T08:02:52Z",
"event": "NavRoute"
}"#,
) else {
panic!("not a route")
};
assert!(plotted.destinations.is_empty());
let Event::NavRoute(sent) = assert_read(
r#"{
"timestamp": "2020-04-27T08:02:52Z",
"event": "NavRoute",
"Route": [
{
"StarSystem": "i Bootis",
"SystemAddress": 1281787693419,
"StarPos": [-22.375, 34.84375, 4.0],
"StarClass": "G"
}
]
}"#,
) else {
panic!("not a route")
};
assert_eq!(sent.destinations.len(), 1);
assert_eq!(sent.destinations[0].star_system, "i Bootis");
}
#[test]
fn an_archived_commander_has_no_fid() {
let Event::Commander(commander) = assert_read(
r#"{
"timestamp": "2017-01-01T00:00:00Z",
"event": "Commander",
"Name": "Nixpulvis"
}"#,
) else {
panic!("not a commander")
};
assert_eq!(commander.name, "Nixpulvis");
assert_eq!(commander.fid, None);
}
}