feat(simulation): atlas proxy layers + data-delivery endpoints (T-960, T-949)
T-960: RoadGraphLayer (L2 edges/junctions, MaintenanceAuthority, rail flag, named routes) + SettlementLayer (name/position/size-class/is_capital/is_port from T-955 placements) as new Option fields on AtlasLayerResponse — T-1046 precedent; layer_proxy doc comment corrected (cascade runs through RoadGraph, client up_to ignored). T-949: new StarMapRequest/StarMapResponse (raw JSON passthrough of the star_map_data.json bake) + CityNamesRequest/CityNamesResponse (atlas_city_names via CityContextReader) with defense-in-depth Sol exclusion (GJ-0 system id OR settlement_wave='origin', D-236). Inbound demux extended with mandatory boolean discriminator fields (serde ignores unknown fields — optional-shape sniffing would be ambiguous); AtlasLayerRequest unchanged. SimBridge trait +2 methods, implemented on TcpBridge + LocalBridge. CityPlacement/CityRecord gained name/population/is_capital (BodyWorldState cache-hit path stays DB-free). ~30 new unit tests incl. demux disambiguation + Sol branches; gen_fixtures extended for road/settlement samples. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,13 +14,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::atlas::body_params_reader::BodyParamsReader;
|
||||
use crate::atlas::body_world_state::{BodyWorldStateCache, SimTick};
|
||||
use crate::atlas::body_world_state::{BodyWorldState, BodyWorldStateCache, SimTick};
|
||||
use crate::atlas::cascade::CascadeLayer;
|
||||
use crate::atlas::city_context_reader::CityContextReader;
|
||||
use crate::atlas::gen_queue::{GenPriority, GenWorkItem, GenerationQueue};
|
||||
use crate::atlas::layer1::Layer1Output;
|
||||
use crate::atlas::road_graph::RoadNodeKind;
|
||||
use crate::atlas::source_resolver::{BodySourceResolver, SourceResolveError};
|
||||
use crate::seed::SeedChain;
|
||||
use crate::simulation::generator::{AttractorType, MaintenanceAuthority};
|
||||
|
||||
/// Fallback sea level when the heightmap PNG carries no `sea_level` tEXt chunk
|
||||
/// (the loader prefers the chunk; this is only the floor).
|
||||
@@ -29,9 +31,10 @@ const DEFAULT_SEA_LEVEL: f32 = 0.3;
|
||||
/// A client request for a body's generation layers (D-225).
|
||||
///
|
||||
/// `up_to` is a forward-compat seam that is **not yet honored**: `run_work_item`
|
||||
/// currently runs the cascade through `CascadeLayer::Settlement` unconditionally,
|
||||
/// ignoring this field. Wiring per-request depth (and the partial caching it
|
||||
/// implies) is deferred to #1021.
|
||||
/// (`gen_queue.rs`) currently runs the cascade through `CascadeLayer::RoadGraph`
|
||||
/// (the terminal layer, T-1038) unconditionally on every request, ignoring this
|
||||
/// field. Wiring per-request depth (and the partial caching it implies) is
|
||||
/// deferred to #1021.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AtlasLayerRequest {
|
||||
pub body_id: String,
|
||||
@@ -67,7 +70,8 @@ pub struct DistrictGridLayer {
|
||||
}
|
||||
|
||||
/// A layer response: the computed `Layer1Output` + the coarse district grid
|
||||
/// (D-225, T-1046), or a non-ready status.
|
||||
/// (D-225, T-1046) + the road-graph and settlement overlays (T-960 §1/§2), or
|
||||
/// a non-ready status.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AtlasLayerResponse {
|
||||
pub body_id: String,
|
||||
@@ -76,6 +80,15 @@ pub struct AtlasLayerResponse {
|
||||
/// The coarse district/morphology grid for the Atlas overlay (T-1046).
|
||||
/// `Some` on a cache hit once the DistrictProfile layer has run; `None` otherwise.
|
||||
pub district_grid: Option<DistrictGridLayer>,
|
||||
/// The inter-settlement road/rail graph overlay (T-960 §1, T-1038).
|
||||
/// `Some` on a cache hit once the RoadGraph layer has run; `None` otherwise
|
||||
/// (including a body with zero placed settlements — an empty graph has no
|
||||
/// nodes to draw, so it collapses to `None` the same way `district_grid`
|
||||
/// does for an unrun layer).
|
||||
pub road_graph: Option<RoadGraphLayer>,
|
||||
/// The settlement-placement overlay (T-960 §2, #955). `Some` on a cache hit
|
||||
/// once the Settlement layer has placed at least one city; `None` otherwise.
|
||||
pub settlements: Option<SettlementLayer>,
|
||||
}
|
||||
|
||||
/// Build the coarse [`DistrictGridLayer`] from a body's cached state (T-1046).
|
||||
@@ -111,6 +124,173 @@ pub fn build_district_grid(
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RoadGraphLayer (T-960 §1, T-1038)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One node in the [`RoadGraphLayer`] overlay — a settlement junction or a
|
||||
/// waypoint. Trimmed from the internal [`crate::atlas::road_graph::RoadNode`]:
|
||||
/// `degree` and `parent_edge` are internal bookkeeping a planetary-map overlay
|
||||
/// doesn't need (degree is trivially re-derivable client-side by counting
|
||||
/// edges per node index if a renderer wants junction highlighting).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RoadGraphNode {
|
||||
/// Position in working-heightmap-grid coordinates `(row, col)` — the same
|
||||
/// space as `Layer1Output` attractors/rivers and `SettlementLayer` positions.
|
||||
pub position: (u16, u16),
|
||||
pub kind: RoadNodeKind,
|
||||
/// The settlement's `city_id` (cross-references `SettlementLayer`), or
|
||||
/// `None` for a waypoint.
|
||||
pub city_id: Option<u64>,
|
||||
}
|
||||
|
||||
/// One edge in the [`RoadGraphLayer`] overlay — a routed road or rail segment.
|
||||
/// Trimmed from [`crate::atlas::road_graph::RoadEdge`]: `length_cells` is an
|
||||
/// internal A* routing-grid measure with no meaning outside that grid's scale
|
||||
/// (the polyline `path` is what an overlay actually draws).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RoadGraphEdge {
|
||||
/// `nodes` indices of the endpoint settlements (`from < to`).
|
||||
pub from: usize,
|
||||
pub to: usize,
|
||||
/// Routed polyline in working-heightmap-grid coordinates `(row, col)`.
|
||||
pub path: Vec<(u16, u16)>,
|
||||
pub maintenance: MaintenanceAuthority,
|
||||
/// `true` if this edge is a railroad; `false` is a road.
|
||||
pub is_rail: bool,
|
||||
/// Joined `systems.db` named-route id, if any (empty pool today — D-223).
|
||||
pub named_route_id: Option<String>,
|
||||
}
|
||||
|
||||
/// The inter-settlement road/rail graph, trimmed for the Atlas planetary-map
|
||||
/// overlay (T-960 §1, D-211, T-1038). See [`RoadGraphNode`]/[`RoadGraphEdge`]
|
||||
/// for what was dropped from the internal `RoadGraph`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RoadGraphLayer {
|
||||
pub nodes: Vec<RoadGraphNode>,
|
||||
pub edges: Vec<RoadGraphEdge>,
|
||||
}
|
||||
|
||||
/// Build the [`RoadGraphLayer`] from a body's cached state (T-960 §1).
|
||||
/// Returns `None` when the RoadGraph layer has not run, which coincides
|
||||
/// exactly with "no settlements placed" (`build_road_graph` returns an empty
|
||||
/// graph for zero placements, and every placement yields at least one node).
|
||||
pub fn build_road_graph_layer(state: &BodyWorldState) -> Option<RoadGraphLayer> {
|
||||
if state.road_graph.nodes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let nodes = state
|
||||
.road_graph
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|n| RoadGraphNode {
|
||||
position: n.position,
|
||||
kind: n.kind,
|
||||
city_id: n.city_id,
|
||||
})
|
||||
.collect();
|
||||
let edges = state
|
||||
.road_graph
|
||||
.edges
|
||||
.iter()
|
||||
.map(|e| RoadGraphEdge {
|
||||
from: e.from,
|
||||
to: e.to,
|
||||
path: e.path.clone(),
|
||||
maintenance: e.maintenance,
|
||||
is_rail: e.is_rail,
|
||||
named_route_id: e.named_route_id.clone(),
|
||||
})
|
||||
.collect();
|
||||
Some(RoadGraphLayer { nodes, edges })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SettlementLayer (T-960 §2, #955)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Coarse settlement size class for the Atlas overlay (T-960 §2), derived from
|
||||
/// raw population using the same Tier A/B population cutoffs the D-211
|
||||
/// placement pipeline already uses (`attractor_matching::match_cities`):
|
||||
/// Tier A (≥ 1,000,000 or `NameLocked`) settlements are `Major`, Tier B
|
||||
/// (50,000–999,999) are `Standard`, and everything else (Tier C / synthetic
|
||||
/// overflow) is `Minor`. A display bucket, not new simulation truth.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SettlementSizeClass {
|
||||
Major,
|
||||
Standard,
|
||||
Minor,
|
||||
}
|
||||
|
||||
impl SettlementSizeClass {
|
||||
/// Bucket a raw population using the D-211 Tier A/B cutoffs.
|
||||
pub fn from_population(population: i64) -> Self {
|
||||
if population >= 1_000_000 {
|
||||
SettlementSizeClass::Major
|
||||
} else if population >= 50_000 {
|
||||
SettlementSizeClass::Standard
|
||||
} else {
|
||||
SettlementSizeClass::Minor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One placed settlement in the [`SettlementLayer`] overlay (T-960 §2).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SettlementEntry {
|
||||
pub city_id: u64,
|
||||
pub name: String,
|
||||
/// Position in working-heightmap-grid coordinates `(row, col)` — the same
|
||||
/// space as `Layer1Output` attractors/rivers (T-960 §2: match the
|
||||
/// coordinate convention layer1 features already use so the client
|
||||
/// transforms identically).
|
||||
pub position: (u16, u16),
|
||||
pub size_class: SettlementSizeClass,
|
||||
/// Authored `atlas_city_names.kind == 'capital'` (not population-derived).
|
||||
pub is_capital: bool,
|
||||
/// Cheap derived flag: `true` if the settlement's anchoring attractor is
|
||||
/// water-adjacent (`CoastalAccess` / `RiverMouth` / `LakeShore`).
|
||||
pub is_port: bool,
|
||||
}
|
||||
|
||||
/// The settlement-placement overlay for one body (T-960 §2, #955, D-211).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SettlementLayer {
|
||||
pub settlements: Vec<SettlementEntry>,
|
||||
}
|
||||
|
||||
/// `true` for the water-adjacent attractor types a settlement counts as a
|
||||
/// "port" for cheaply (T-960 §2). No "foothold" flag: unlike `is_port`, there
|
||||
/// is no existing concept in the placement data this could derive from
|
||||
/// without inventing new business logic — left out (see the T-960 report).
|
||||
fn is_port_attractor(at: AttractorType) -> bool {
|
||||
matches!(
|
||||
at,
|
||||
AttractorType::CoastalAccess | AttractorType::RiverMouth | AttractorType::LakeShore
|
||||
)
|
||||
}
|
||||
|
||||
/// Build the [`SettlementLayer`] from a body's cached state (T-960 §2).
|
||||
/// Returns `None` when the Settlement layer has not placed any city yet.
|
||||
pub fn build_settlement_layer(state: &BodyWorldState) -> Option<SettlementLayer> {
|
||||
if state.placements.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let settlements = state
|
||||
.placements
|
||||
.iter()
|
||||
.map(|p| SettlementEntry {
|
||||
city_id: p.city_id,
|
||||
name: p.name.clone(),
|
||||
position: p.position,
|
||||
size_class: SettlementSizeClass::from_population(p.population),
|
||||
is_capital: p.is_capital,
|
||||
is_port: is_port_attractor(p.attractor_type),
|
||||
})
|
||||
.collect();
|
||||
Some(SettlementLayer { settlements })
|
||||
}
|
||||
|
||||
/// Serve one layer request (D-225). `current_tick` stamps the cache LRU on hit;
|
||||
/// `world_seed` derives the body's `SeedChain` for the enqueued analysis.
|
||||
///
|
||||
@@ -154,11 +334,15 @@ pub fn handle_atlas_request(
|
||||
district_basin_dirs: std::collections::BTreeMap::new(),
|
||||
};
|
||||
let district_grid = build_district_grid(state);
|
||||
let road_graph = build_road_graph_layer(state);
|
||||
let settlements = build_settlement_layer(state);
|
||||
return AtlasLayerResponse {
|
||||
body_id: req.body_id.clone(),
|
||||
status: AtlasLayerStatus::Ready,
|
||||
layer1: Some(layer1),
|
||||
district_grid,
|
||||
road_graph,
|
||||
settlements,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -229,6 +413,8 @@ pub fn handle_atlas_request(
|
||||
status: AtlasLayerStatus::Pending,
|
||||
layer1: None,
|
||||
district_grid: None,
|
||||
road_graph: None,
|
||||
settlements: None,
|
||||
}
|
||||
}
|
||||
// Unknown / no terrain → re-requesting won't help.
|
||||
@@ -238,12 +424,16 @@ pub fn handle_atlas_request(
|
||||
status: AtlasLayerStatus::NotFound,
|
||||
layer1: None,
|
||||
district_grid: None,
|
||||
road_graph: None,
|
||||
settlements: None,
|
||||
},
|
||||
Err(e) => AtlasLayerResponse {
|
||||
body_id: req.body_id.clone(),
|
||||
status: AtlasLayerStatus::Error(e.to_string()),
|
||||
layer1: None,
|
||||
district_grid: None,
|
||||
road_graph: None,
|
||||
settlements: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -317,6 +507,236 @@ mod tests {
|
||||
assert!(build_district_grid(&state).is_none());
|
||||
}
|
||||
|
||||
/// A blank `BodyWorldState` for tests that only care about one field —
|
||||
/// callers overwrite `placements`/`road_graph`/etc. as needed.
|
||||
fn blank_state(body_id: &str) -> BodyWorldState {
|
||||
BodyWorldState {
|
||||
body_id: body_id.into(),
|
||||
heightmap: vec![],
|
||||
heightmap_width: 16,
|
||||
heightmap_height: 8,
|
||||
river_network: RiverNetwork::default(),
|
||||
drainage_basins: vec![],
|
||||
attractors: vec![],
|
||||
placements: vec![],
|
||||
road_graph: crate::atlas::road_graph::RoadGraph::default(),
|
||||
quarters: std::collections::BTreeMap::new(),
|
||||
districts: std::collections::BTreeMap::new(),
|
||||
last_accessed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// T-960 §1: `build_road_graph_layer` trims the internal `RoadGraph` (drops
|
||||
/// `degree`/`parent_edge`/`length_cells`) while keeping everything a
|
||||
/// planetary-map overlay needs (positions, kind, polyline, maintenance,
|
||||
/// rail flag, named-route id).
|
||||
#[test]
|
||||
fn road_graph_layer_built_from_cached_state() {
|
||||
use crate::atlas::road_graph::{RoadEdge, RoadGraph, RoadNode};
|
||||
use crate::simulation::generator::MaintenanceAuthority;
|
||||
|
||||
let mut state = blank_state("GJ1c");
|
||||
state.road_graph = RoadGraph {
|
||||
nodes: vec![
|
||||
RoadNode {
|
||||
city_id: Some(1),
|
||||
position: (10, 20),
|
||||
kind: RoadNodeKind::Settlement,
|
||||
degree: 1,
|
||||
parent_edge: None,
|
||||
},
|
||||
RoadNode {
|
||||
city_id: None,
|
||||
position: (15, 25),
|
||||
kind: RoadNodeKind::Waypoint,
|
||||
degree: 0,
|
||||
parent_edge: Some(0),
|
||||
},
|
||||
],
|
||||
edges: vec![RoadEdge {
|
||||
from: 0,
|
||||
to: 1,
|
||||
path: vec![(10, 20), (15, 25)],
|
||||
length_cells: 20, // internal routing-grid measure — dropped
|
||||
maintenance: MaintenanceAuthority::Trade,
|
||||
named_route_id: Some("split/hwy-1".into()),
|
||||
is_rail: true,
|
||||
}],
|
||||
};
|
||||
|
||||
let layer = build_road_graph_layer(&state).expect("populated road_graph → Some");
|
||||
assert_eq!(layer.nodes.len(), 2);
|
||||
assert_eq!(layer.nodes[0].position, (10, 20));
|
||||
assert_eq!(layer.nodes[0].kind, RoadNodeKind::Settlement);
|
||||
assert_eq!(layer.nodes[0].city_id, Some(1));
|
||||
assert_eq!(layer.nodes[1].kind, RoadNodeKind::Waypoint);
|
||||
assert_eq!(layer.nodes[1].city_id, None);
|
||||
assert_eq!(layer.edges.len(), 1);
|
||||
assert_eq!(layer.edges[0].path, vec![(10, 20), (15, 25)]);
|
||||
assert_eq!(layer.edges[0].maintenance, MaintenanceAuthority::Trade);
|
||||
assert!(layer.edges[0].is_rail);
|
||||
assert_eq!(
|
||||
layer.edges[0].named_route_id.as_deref(),
|
||||
Some("split/hwy-1")
|
||||
);
|
||||
|
||||
// Layer hasn't run (or zero settlements) → None, mirroring district_grid.
|
||||
let unrun = blank_state("GJ1c");
|
||||
assert!(build_road_graph_layer(&unrun).is_none());
|
||||
}
|
||||
|
||||
/// T-960 §2: `build_settlement_layer` derives `size_class` from population
|
||||
/// using the D-211 Tier A/B cutoffs, threads `is_capital` straight through,
|
||||
/// and derives `is_port` cheaply from the anchoring attractor type.
|
||||
#[test]
|
||||
fn settlement_layer_built_from_cached_placements() {
|
||||
use crate::atlas::attractor_matching::CityPlacement;
|
||||
use crate::simulation::generator::{
|
||||
ArrangementPattern, FoundingOrientation, PoliticalArchetype,
|
||||
};
|
||||
|
||||
let mk = |city_id: u64,
|
||||
name: &str,
|
||||
pos: (u16, u16),
|
||||
population: i64,
|
||||
is_capital: bool,
|
||||
attractor_type: AttractorType| CityPlacement {
|
||||
city_id,
|
||||
name: name.to_string(),
|
||||
position: pos,
|
||||
attractor_type,
|
||||
score: 1000,
|
||||
synthetic: false,
|
||||
political_archetype: PoliticalArchetype::Commission,
|
||||
arrangement_pattern: ArrangementPattern::RadialCore,
|
||||
founding_orientation: FoundingOrientation::Cardinal,
|
||||
population,
|
||||
is_capital,
|
||||
};
|
||||
|
||||
let mut state = blank_state("GJ1c");
|
||||
state.placements = vec![
|
||||
mk(
|
||||
1,
|
||||
"Port Aldren",
|
||||
(12, 58),
|
||||
2_000_000,
|
||||
true,
|
||||
AttractorType::CoastalAccess,
|
||||
),
|
||||
mk(
|
||||
2,
|
||||
"Millbrook",
|
||||
(30, 40),
|
||||
200_000,
|
||||
false,
|
||||
AttractorType::ValleyFloor,
|
||||
),
|
||||
mk(
|
||||
3,
|
||||
"Farmstead Rell",
|
||||
(50, 60),
|
||||
8_000,
|
||||
false,
|
||||
AttractorType::PlainCenter,
|
||||
),
|
||||
];
|
||||
|
||||
let layer = build_settlement_layer(&state).expect("populated placements → Some");
|
||||
assert_eq!(layer.settlements.len(), 3);
|
||||
|
||||
let capital = layer.settlements.iter().find(|s| s.city_id == 1).unwrap();
|
||||
assert_eq!(capital.name, "Port Aldren");
|
||||
assert_eq!(capital.position, (12, 58));
|
||||
assert_eq!(capital.size_class, SettlementSizeClass::Major);
|
||||
assert!(capital.is_capital);
|
||||
assert!(capital.is_port, "CoastalAccess must read as a port");
|
||||
|
||||
let mid = layer.settlements.iter().find(|s| s.city_id == 2).unwrap();
|
||||
assert_eq!(mid.size_class, SettlementSizeClass::Standard);
|
||||
assert!(!mid.is_capital);
|
||||
assert!(!mid.is_port, "ValleyFloor is not a port attractor");
|
||||
|
||||
let small = layer.settlements.iter().find(|s| s.city_id == 3).unwrap();
|
||||
assert_eq!(small.size_class, SettlementSizeClass::Minor);
|
||||
assert!(!small.is_port);
|
||||
|
||||
// No placements → None.
|
||||
let unrun = blank_state("GJ1c");
|
||||
assert!(build_settlement_layer(&unrun).is_none());
|
||||
}
|
||||
|
||||
/// T-960: the new layers survive a MessagePack round trip inside
|
||||
/// `AtlasLayerResponse` — the same wire path the bridge uses
|
||||
/// (`rmp_serde::to_vec_named` / `from_slice`, matching `layer1`/
|
||||
/// `district_grid`'s existing serialization).
|
||||
#[test]
|
||||
fn atlas_layer_response_with_new_layers_round_trips_msgpack() {
|
||||
use crate::atlas::attractor_matching::CityPlacement;
|
||||
use crate::atlas::road_graph::{RoadEdge, RoadGraph, RoadNode};
|
||||
use crate::simulation::generator::{
|
||||
ArrangementPattern, FoundingOrientation, MaintenanceAuthority, PoliticalArchetype,
|
||||
};
|
||||
|
||||
let mut state = blank_state("GJ1c");
|
||||
state.placements = vec![CityPlacement {
|
||||
city_id: 1,
|
||||
name: "Port Aldren".into(),
|
||||
position: (12, 58),
|
||||
attractor_type: AttractorType::CoastalAccess,
|
||||
score: 1000,
|
||||
synthetic: false,
|
||||
political_archetype: PoliticalArchetype::Commission,
|
||||
arrangement_pattern: ArrangementPattern::RadialCore,
|
||||
founding_orientation: FoundingOrientation::Cardinal,
|
||||
population: 2_000_000,
|
||||
is_capital: true,
|
||||
}];
|
||||
state.road_graph = RoadGraph {
|
||||
nodes: vec![RoadNode {
|
||||
city_id: Some(1),
|
||||
position: (12, 58),
|
||||
kind: RoadNodeKind::Settlement,
|
||||
degree: 0,
|
||||
parent_edge: None,
|
||||
}],
|
||||
edges: vec![RoadEdge {
|
||||
from: 0,
|
||||
to: 0,
|
||||
path: vec![(12, 58)],
|
||||
length_cells: 0,
|
||||
maintenance: MaintenanceAuthority::Administrative,
|
||||
named_route_id: None,
|
||||
is_rail: false,
|
||||
}],
|
||||
};
|
||||
|
||||
let resp = AtlasLayerResponse {
|
||||
body_id: "GJ1c".into(),
|
||||
status: AtlasLayerStatus::Ready,
|
||||
layer1: None,
|
||||
district_grid: None,
|
||||
road_graph: build_road_graph_layer(&state),
|
||||
settlements: build_settlement_layer(&state),
|
||||
};
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&resp).expect("encode");
|
||||
let decoded: AtlasLayerResponse = rmp_serde::from_slice(&bytes).expect("decode");
|
||||
|
||||
assert_eq!(decoded.body_id, "GJ1c");
|
||||
let rg = decoded.road_graph.expect("road_graph survives round trip");
|
||||
assert_eq!(rg.nodes[0].position, (12, 58));
|
||||
assert_eq!(rg.edges[0].maintenance, MaintenanceAuthority::Administrative);
|
||||
let settlements = decoded.settlements.expect("settlements survives round trip");
|
||||
assert_eq!(settlements.settlements[0].name, "Port Aldren");
|
||||
assert_eq!(
|
||||
settlements.settlements[0].size_class,
|
||||
SettlementSizeClass::Major
|
||||
);
|
||||
assert!(settlements.settlements[0].is_capital);
|
||||
assert!(settlements.settlements[0].is_port);
|
||||
}
|
||||
|
||||
fn req(body_id: &str) -> AtlasLayerRequest {
|
||||
AtlasLayerRequest {
|
||||
body_id: body_id.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user