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 {
pub system: String,
pub cost: u64,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct SellExplorationData {
pub systems: Vec<String>,
pub discovered: Vec<String>,
pub base_value: u64,
pub bonus: u64,
pub total_earnings: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScanType {
Basic,
Detailed,
NavBeacon,
NavBeaconDetail,
AutoScan,
Other(String),
}
impl<'de> Deserialize<'de> for ScanType {
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 {
pub fn is_beacon(&self) -> bool {
matches!(self, ScanType::NavBeacon | ScanType::NavBeaconDetail)
}
}
#[derive(Debug)]
pub enum ScanTarget {
Star(Star),
Body(Body),
Cluster(Cluster),
Ring(Ring),
}
impl ScanTarget {
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,
}
}
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 {
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)
}
}
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())
}
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()
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Ring {
#[serde(rename = "BodyName")]
pub name: String,
#[serde(rename = "BodyID")]
pub id: i16,
#[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,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Cluster {
#[serde(rename = "BodyName")]
pub name: String,
#[serde(rename = "BodyID")]
pub id: i16,
#[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 {
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,
}
#[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>,
}
#[derive(Debug)]
pub struct FssSignalDiscovered {
pub star_system: Option<String>,
pub star_pos: Option<Coordinate>,
pub system_address: i64,
pub signals: Vec<SystemSignal>,
}
impl<'de> Deserialize<'de> for FssSignalDiscovered {
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"))?;
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,
})
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct SystemSignal {
#[serde(rename = "timestamp")]
pub timestamp: Option<DateTime<Utc>>,
pub signal_name: String,
pub signal_type: Option<String>,
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>,
}
#[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,
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>,
}
#[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,
pub body_count: i32,
pub non_body_count: i32,
}
#[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,
pub count: i32,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct NavBeaconScan {
pub star_system: Option<String>,
pub star_pos: Option<Coordinate>,
pub system_address: i64,
pub num_bodies: i32,
}
#[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 {
#[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,
pub signals: Vec<Signal>,
}