feat(simulation): add Layer-2 inter-settlement road/rail graph (T-1038)
New deterministic cascade layer (CascadeLayer::RoadGraph) building the transport topology between Layer-3 settlement placements, stored on BodyWorldState.road_graph (session-cached, never serialized). - MST force-connect (strict Kruskal over squared distance) + secondary links across a 30-60% body-scale band where the MST detours >1.5x. - A* over the Layer-1 terrain-cost field on a coarse ~64-wide routing grid: ocean/lake impassable, river corridors 1/2 cost, slope >=35deg (D-210 roughness ~0.7) 10x, else the D-210 terrain_modification_cost. Columns wrap (equirectangular); integer costs + index tie-break keep A* deterministic. - MaintenanceAuthority per edge (new generator.rs enum) from endpoint political archetypes + haul length; waypoint nodes at long-edge midpoints; named-route identity join (designed-for, pool empty post-D-223); junction detection (high_connectivity_junctions, degree>=3). Wired into the cascade (gen_queue requests CascadeLayer::RoadGraph). Adds Copy to PoliticalArchetype. 10 road_graph unit tests + a cascade e2e test; full suite + clippy --all-targets -D warnings green. Deferred to T-1076 (gated on the D-242 settlement model): the RailHeadFacing junction pass (needs a D-213 FoundingOrientation amendment + Layer-4 handling) and the hub-selection refinement (needs baked population/specialization). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::atlas::attractor_matching::CityPlacement;
|
use crate::atlas::attractor_matching::CityPlacement;
|
||||||
use crate::atlas::region_profile::{RegionPos, RegionProfile};
|
use crate::atlas::region_profile::{RegionPos, RegionProfile};
|
||||||
|
use crate::atlas::road_graph::RoadGraph;
|
||||||
use crate::simulation::generator::{
|
use crate::simulation::generator::{
|
||||||
GeographicAttractor, QuarterId, QuarterWorldState, TerritorialStatus,
|
GeographicAttractor, QuarterId, QuarterWorldState, TerritorialStatus,
|
||||||
};
|
};
|
||||||
@@ -84,6 +85,10 @@ pub struct BodyWorldState {
|
|||||||
/// Settlement placements (D-211, #955). Attractor-matched city positions.
|
/// Settlement placements (D-211, #955). Attractor-matched city positions.
|
||||||
/// Empty until the Layer-3 placement task completes.
|
/// Empty until the Layer-3 placement task completes.
|
||||||
pub placements: Vec<CityPlacement>,
|
pub placements: Vec<CityPlacement>,
|
||||||
|
/// Inter-settlement road/rail graph (D-211, T-1038). Session-cached, never
|
||||||
|
/// serialized — re-derived from `(placements, terrain, seed)` on resume.
|
||||||
|
/// Empty until the Layer-2 road task completes (needs placements + terrain).
|
||||||
|
pub road_graph: RoadGraph,
|
||||||
/// Quarter-level world state, keyed by `QuarterId` (D-230).
|
/// Quarter-level world state, keyed by `QuarterId` (D-230).
|
||||||
///
|
///
|
||||||
/// Populated by `GenCompletion::SkeletonGenerated` after the plan phase
|
/// Populated by `GenCompletion::SkeletonGenerated` after the plan phase
|
||||||
@@ -216,6 +221,7 @@ mod tests {
|
|||||||
drainage_basins: vec![],
|
drainage_basins: vec![],
|
||||||
attractors: vec![],
|
attractors: vec![],
|
||||||
placements: vec![],
|
placements: vec![],
|
||||||
|
road_graph: RoadGraph::default(),
|
||||||
quarters: BTreeMap::new(),
|
quarters: BTreeMap::new(),
|
||||||
regions: BTreeMap::new(),
|
regions: BTreeMap::new(),
|
||||||
last_accessed: tick,
|
last_accessed: tick,
|
||||||
|
|||||||
+145
-18
@@ -28,6 +28,7 @@ use crate::atlas::features::TerrainAnalysis;
|
|||||||
use crate::atlas::heightmap::{self, BodyHeightmap, HeightmapLoadError};
|
use crate::atlas::heightmap::{self, BodyHeightmap, HeightmapLoadError};
|
||||||
use crate::atlas::layer1::{self, Layer1Output};
|
use crate::atlas::layer1::{self, Layer1Output};
|
||||||
use crate::atlas::region_profile::{self, BodyParams, RegionPos, RegionProfile};
|
use crate::atlas::region_profile::{self, BodyParams, RegionPos, RegionProfile};
|
||||||
|
use crate::atlas::road_graph::{self, RoadGraph};
|
||||||
use crate::seed::SeedChain;
|
use crate::seed::SeedChain;
|
||||||
use crate::simulation::generator::{CompatibilityMatrix, GeographicAttractor, TerritorialStatus};
|
use crate::simulation::generator::{CompatibilityMatrix, GeographicAttractor, TerritorialStatus};
|
||||||
|
|
||||||
@@ -49,6 +50,12 @@ pub enum CascadeLayer {
|
|||||||
/// `(seed, body_params, terrain_analysis)`. Appended after Settlement so
|
/// `(seed, body_params, terrain_analysis)`. Appended after Settlement so
|
||||||
/// declaration order (= Ord) is preserved — never reorder (D-010).
|
/// declaration order (= Ord) is preserved — never reorder (D-010).
|
||||||
RegionProfile,
|
RegionProfile,
|
||||||
|
/// Layer 2 — inter-settlement road/rail graph (D-211, T-1038). Pure function
|
||||||
|
/// of `(Layer-3 placements, Layer-1 terrain)`; RNG-free. Semantically "Layer
|
||||||
|
/// 2", but it depends only on Settlement + Topography, so it is **appended
|
||||||
|
/// last** to honour the append-only `Ord` rule (it neither needs nor blocks
|
||||||
|
/// the RegionProfile layer; requesting it runs RegionProfile first, harmlessly).
|
||||||
|
RoadGraph,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Output of the cascade for one body, up to the requested layer (#952).
|
/// Output of the cascade for one body, up to the requested layer (#952).
|
||||||
@@ -70,6 +77,9 @@ pub struct CascadeSnapshot {
|
|||||||
/// RegionProfile layer — ~1 km carriers. `Some` once
|
/// RegionProfile layer — ~1 km carriers. `Some` once
|
||||||
/// [`CascadeLayer::RegionProfile`] has run (T-1023, D-239 §1).
|
/// [`CascadeLayer::RegionProfile`] has run (T-1023, D-239 §1).
|
||||||
pub layer_region: Option<LayerRegionOutput>,
|
pub layer_region: Option<LayerRegionOutput>,
|
||||||
|
/// Layer 2 — inter-settlement road/rail graph. `Some` once
|
||||||
|
/// [`CascadeLayer::RoadGraph`] has run (D-211, T-1038).
|
||||||
|
pub road_graph: Option<RoadGraph>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RegionProfile layer output (T-1023, D-239 §1): per-region (~1 km) terrain
|
/// RegionProfile layer output (T-1023, D-239 §1): per-region (~1 km) terrain
|
||||||
@@ -98,6 +108,7 @@ impl CascadeSnapshot {
|
|||||||
};
|
};
|
||||||
let placements = self.layer3.map(|l3| l3.placements).unwrap_or_default();
|
let placements = self.layer3.map(|l3| l3.placements).unwrap_or_default();
|
||||||
let regions = self.layer_region.map(|lr| lr.regions).unwrap_or_default();
|
let regions = self.layer_region.map(|lr| lr.regions).unwrap_or_default();
|
||||||
|
let road_graph = self.road_graph.unwrap_or_default();
|
||||||
BodyWorldState {
|
BodyWorldState {
|
||||||
body_id: self.body_id,
|
body_id: self.body_id,
|
||||||
heightmap: self.heightmap.data,
|
heightmap: self.heightmap.data,
|
||||||
@@ -107,6 +118,7 @@ impl CascadeSnapshot {
|
|||||||
drainage_basins,
|
drainage_basins,
|
||||||
attractors,
|
attractors,
|
||||||
placements,
|
placements,
|
||||||
|
road_graph,
|
||||||
quarters: std::collections::BTreeMap::new(),
|
quarters: std::collections::BTreeMap::new(),
|
||||||
regions,
|
regions,
|
||||||
last_accessed: 0,
|
last_accessed: 0,
|
||||||
@@ -173,6 +185,7 @@ pub fn run_cascade_from_heightmap(
|
|||||||
layer1: None,
|
layer1: None,
|
||||||
layer3: None,
|
layer3: None,
|
||||||
layer_region: None,
|
layer_region: None,
|
||||||
|
road_graph: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TerritorialStatus is derived once per body from the system's dominant
|
// TerritorialStatus is derived once per body from the system's dominant
|
||||||
@@ -210,28 +223,35 @@ pub fn run_cascade_from_heightmap(
|
|||||||
snapshot.layer3 = Some(l3);
|
snapshot.layer3 = Some(l3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegionProfile layer (T-1023, D-239 §1) — pure derivation from body params +
|
// RegionProfile (T-1023, D-239 §1) and RoadGraph (Layer 2, T-1038) both need a
|
||||||
// terrain analysis. Needs a TerrainAnalysis, which needs a drainage pass.
|
// TerrainAnalysis, which needs a drainage pass. Layer 1 already ran drainage
|
||||||
// Layer 1 already ran drainage inside run_layer1, but neither the drainage
|
// inside run_layer1, but neither result is stored on Layer1Output, so we re-run
|
||||||
// result nor the TerrainAnalysis is stored on Layer1Output, so we re-run both
|
// both here once and share them. Pure → determinism preserved, but the drainage
|
||||||
// here. Pure → determinism preserved, but the drainage re-run is NOT free at
|
// re-run is NOT free at the ~6 000-regions/body working scale (D-203).
|
||||||
// the ~6 000-regions/body working scale (D-203).
|
|
||||||
// PERF/TODO(T-1044): cache TerrainAnalysis on Layer1Output to drop this
|
// PERF/TODO(T-1044): cache TerrainAnalysis on Layer1Output to drop this
|
||||||
// redundant drainage pass, and validate the combined cost against the D-239 §10
|
// redundant drainage pass, and validate the combined cost against the D-239 §10
|
||||||
// ~45 ms/body budget in the T-1031 verification harness. This is now a LIVE
|
// ~45 ms/body budget in the T-1031 verification harness. This is a LIVE
|
||||||
// production cost: T-1032 wired the real body_params read, so every analyzed
|
// production cost: T-1032 wired the real body_params read, so every analyzed
|
||||||
// body runs this path. If body_params is None, the region layer is skipped
|
// body runs this path.
|
||||||
// (e.g. unit tests without DB).
|
//
|
||||||
if up_to >= CascadeLayer::RegionProfile {
|
// The terrain analysis is computed only when it will actually be used:
|
||||||
|
// body_params present (RegionProfile) or RoadGraph requested. RegionProfile
|
||||||
|
// skips silently without body_params (e.g. unit tests without DB), but the
|
||||||
|
// road graph needs no body params, so RoadGraph runs regardless.
|
||||||
|
if up_to >= CascadeLayer::RegionProfile
|
||||||
|
&& (body_params.is_some() || up_to >= CascadeLayer::RoadGraph)
|
||||||
|
{
|
||||||
|
use crate::atlas::drainage;
|
||||||
|
let dr = drainage::analyze(
|
||||||
|
&snapshot.heightmap.data,
|
||||||
|
snapshot.heightmap.width,
|
||||||
|
snapshot.heightmap.height,
|
||||||
|
snapshot.heightmap.sea_level,
|
||||||
|
);
|
||||||
|
let ta = TerrainAnalysis::analyze(&snapshot.heightmap, &dr);
|
||||||
|
|
||||||
|
// RegionProfile layer — pure derivation from body params + terrain.
|
||||||
if let Some(params) = body_params {
|
if let Some(params) = body_params {
|
||||||
use crate::atlas::drainage;
|
|
||||||
let dr = drainage::analyze(
|
|
||||||
&snapshot.heightmap.data,
|
|
||||||
snapshot.heightmap.width,
|
|
||||||
snapshot.heightmap.height,
|
|
||||||
snapshot.heightmap.sea_level,
|
|
||||||
);
|
|
||||||
let ta = TerrainAnalysis::analyze(&snapshot.heightmap, &dr);
|
|
||||||
// ~8 cells per region on a 128×64 working grid → ~80×32 = ~2 560 regions;
|
// ~8 cells per region on a 128×64 working grid → ~80×32 = ~2 560 regions;
|
||||||
// at full working resolution the budget is ~6 000/body (D-203).
|
// at full working resolution the budget is ~6 000/body (D-203).
|
||||||
const CELLS_PER_REGION: usize = 8;
|
const CELLS_PER_REGION: usize = 8;
|
||||||
@@ -239,6 +259,33 @@ pub fn run_cascade_from_heightmap(
|
|||||||
region_profile::derive_all_regions(body_seed, params, &ta, CELLS_PER_REGION);
|
region_profile::derive_all_regions(body_seed, params, &ta, CELLS_PER_REGION);
|
||||||
snapshot.layer_region = Some(LayerRegionOutput { regions });
|
snapshot.layer_region = Some(LayerRegionOutput { regions });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Layer 2 — inter-settlement road/rail graph (D-211, T-1038). Pure
|
||||||
|
// function of (Layer-3 placements, Layer-1 terrain). The named-route pool
|
||||||
|
// is empty for now (atlas_roads/atlas_railroads carry no rows post-D-223),
|
||||||
|
// so the named-route identity join is a designed-for no-op.
|
||||||
|
if up_to >= CascadeLayer::RoadGraph {
|
||||||
|
let placements = snapshot
|
||||||
|
.layer3
|
||||||
|
.as_ref()
|
||||||
|
.map(|l3| l3.placements.as_slice())
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
let river_cells = snapshot
|
||||||
|
.layer1
|
||||||
|
.as_ref()
|
||||||
|
.map(|l1| l1.river_network.river_cells.as_slice())
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
let graph = road_graph::build_road_graph(
|
||||||
|
placements,
|
||||||
|
&ta,
|
||||||
|
river_cells,
|
||||||
|
snapshot.heightmap.width,
|
||||||
|
snapshot.heightmap.height,
|
||||||
|
&territorial_status,
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
snapshot.road_graph = Some(graph);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot
|
snapshot
|
||||||
@@ -371,6 +418,7 @@ mod tests {
|
|||||||
assert!(CascadeLayer::Heightmap < CascadeLayer::Topography);
|
assert!(CascadeLayer::Heightmap < CascadeLayer::Topography);
|
||||||
assert!(CascadeLayer::Topography < CascadeLayer::Settlement);
|
assert!(CascadeLayer::Topography < CascadeLayer::Settlement);
|
||||||
assert!(CascadeLayer::Settlement < CascadeLayer::RegionProfile);
|
assert!(CascadeLayer::Settlement < CascadeLayer::RegionProfile);
|
||||||
|
assert!(CascadeLayer::RegionProfile < CascadeLayer::RoadGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -511,4 +559,83 @@ mod tests {
|
|||||||
placement_count
|
placement_count
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Layer 2 — road graph runs through the full cascade, connects the placed
|
||||||
|
/// cities, is deterministic, and propagates into BodyWorldState (T-1038).
|
||||||
|
#[test]
|
||||||
|
fn road_graph_layer_connects_cities_deterministically() {
|
||||||
|
use crate::atlas::attractor_matching::CityRecord;
|
||||||
|
use crate::atlas::road_graph::RoadNodeKind;
|
||||||
|
use crate::simulation::generator::SettlementClass;
|
||||||
|
|
||||||
|
// Several inland cities (the slope heightmap is land away from the low
|
||||||
|
// corner) so the MST has real edges to route.
|
||||||
|
let cities: Vec<CityRecord> = [
|
||||||
|
(1u64, "A", 800_000i64),
|
||||||
|
(2, "B", 400_000),
|
||||||
|
(3, "C", 200_000),
|
||||||
|
(4, "D", 150_000),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|(id, name, pop)| CityRecord {
|
||||||
|
city_id: *id,
|
||||||
|
name: (*name).into(),
|
||||||
|
settlement_class: SettlementClass::PopulationBudget,
|
||||||
|
population: *pop,
|
||||||
|
economic_role: "manufacturing".into(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let run = || {
|
||||||
|
run_cascade_from_heightmap(
|
||||||
|
body_seed(),
|
||||||
|
test_heightmap(),
|
||||||
|
&cities,
|
||||||
|
Some("independent"),
|
||||||
|
None, // body_params — road graph needs none
|
||||||
|
CascadeLayer::RoadGraph,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let snap = run();
|
||||||
|
let graph = snap.road_graph.as_ref().expect("RoadGraph layer ran");
|
||||||
|
let settlements = graph
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.filter(|n| n.kind == RoadNodeKind::Settlement)
|
||||||
|
.count();
|
||||||
|
assert_eq!(settlements, cities.len(), "one road node per placed city");
|
||||||
|
assert!(
|
||||||
|
!graph.edges.is_empty(),
|
||||||
|
"placed cities on shared land must be connected"
|
||||||
|
);
|
||||||
|
// Every edge endpoint is a settlement node and the path snaps to it.
|
||||||
|
for e in &graph.edges {
|
||||||
|
assert!(e.from < e.to);
|
||||||
|
assert_eq!(graph.nodes[e.from].position, *e.path.first().unwrap());
|
||||||
|
assert_eq!(graph.nodes[e.to].position, *e.path.last().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determinism: identical inputs → identical graph.
|
||||||
|
let key = |s: &CascadeSnapshot| {
|
||||||
|
let g = s.road_graph.as_ref().unwrap();
|
||||||
|
(
|
||||||
|
g.nodes
|
||||||
|
.iter()
|
||||||
|
.map(|n| (n.city_id, n.position, n.degree))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
g.edges
|
||||||
|
.iter()
|
||||||
|
.map(|e| (e.from, e.to, e.length_cells, e.maintenance))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert_eq!(key(&snap), key(&run()), "road graph must be deterministic");
|
||||||
|
|
||||||
|
// Propagates into the hot-cache BodyWorldState.
|
||||||
|
let edge_count = graph.edges.len();
|
||||||
|
assert_eq!(
|
||||||
|
snap.into_body_world_state().road_graph.edges.len(),
|
||||||
|
edge_count
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,15 +374,13 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
|
|||||||
} else {
|
} else {
|
||||||
hm
|
hm
|
||||||
};
|
};
|
||||||
// Run through RegionProfile (T-1023, D-239 §1): includes Settlement
|
// Run the full cascade through RoadGraph (Layer 2, T-1038), the
|
||||||
// and all prior layers. RegionProfile > Settlement in CascadeLayer ord
|
// terminal layer. It subsumes Settlement, RegionProfile (T-1023),
|
||||||
// so Settlement also runs when body_params is Some. When body_params
|
// and all prior layers. RegionProfile derivation still gates on
|
||||||
// is None the cascade falls back to Settlement as the terminal layer.
|
// body_params internally (skipped when absent — e.g. a body with no
|
||||||
let up_to = if body_params.is_some() {
|
// params row), but the road graph needs no body params, so it runs
|
||||||
CascadeLayer::RegionProfile
|
// for every analyzed body.
|
||||||
} else {
|
let up_to = CascadeLayer::RoadGraph;
|
||||||
CascadeLayer::Settlement
|
|
||||||
};
|
|
||||||
let snapshot = run_cascade_from_heightmap(
|
let snapshot = run_cascade_from_heightmap(
|
||||||
*body_seed,
|
*body_seed,
|
||||||
working,
|
working,
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ mod tests {
|
|||||||
drainage_basins: vec![],
|
drainage_basins: vec![],
|
||||||
attractors: vec![],
|
attractors: vec![],
|
||||||
placements: vec![],
|
placements: vec![],
|
||||||
|
road_graph: crate::atlas::road_graph::RoadGraph::default(),
|
||||||
quarters: std::collections::BTreeMap::new(),
|
quarters: std::collections::BTreeMap::new(),
|
||||||
regions: std::collections::BTreeMap::new(),
|
regions: std::collections::BTreeMap::new(),
|
||||||
last_accessed: 0,
|
last_accessed: 0,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub mod layer1;
|
|||||||
pub mod layer_proxy;
|
pub mod layer_proxy;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod region_profile;
|
pub mod region_profile;
|
||||||
|
pub mod road_graph;
|
||||||
pub mod skeleton_gen;
|
pub mod skeleton_gen;
|
||||||
pub mod source_resolver;
|
pub mod source_resolver;
|
||||||
pub mod subbiome;
|
pub mod subbiome;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -373,7 +373,7 @@ pub enum SettlementClass {
|
|||||||
/// Dominant power structure of a settlement and its physical spatial expression.
|
/// Dominant power structure of a settlement and its physical spatial expression.
|
||||||
/// Derived from TerritorialStatus + economic_role at generation time.
|
/// Derived from TerritorialStatus + economic_role at generation time.
|
||||||
/// Source: D-214
|
/// Source: D-214
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum PoliticalArchetype {
|
pub enum PoliticalArchetype {
|
||||||
/// Top-down Commission planning; rectilinear, institutional core, radial-core arrangement.
|
/// Top-down Commission planning; rectilinear, institutional core, radial-core arrangement.
|
||||||
Commission,
|
Commission,
|
||||||
@@ -406,6 +406,29 @@ pub enum FoundingOrientation {
|
|||||||
Free { bearing_degrees: u16 },
|
Free { bearing_degrees: u16 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Who maintains an inter-settlement road/rail edge — readable in the road's
|
||||||
|
/// condition (Ozzie: "you can tell who controls territory by the state of the
|
||||||
|
/// roads"). Derived at Layer-2 road generation from the endpoint settlements'
|
||||||
|
/// political archetypes + the edge's haul length (no separate authoring).
|
||||||
|
/// Five-value vocabulary frozen by the cascade workshop (workshop-outcomes.md
|
||||||
|
/// "OQ-R3-4"); see [`crate::atlas::road_graph::maintenance_authority`].
|
||||||
|
/// Source: D-211 (Layer-2 transport), D-212 (territorial status input).
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum MaintenanceAuthority {
|
||||||
|
/// Government-built/maintained — a Commission (or strategic Military) endpoint.
|
||||||
|
Administrative,
|
||||||
|
/// Corporation-maintained — a Corporate endpoint (e.g. road to a corp mine).
|
||||||
|
Corporate,
|
||||||
|
/// Locally maintained short links between neighbouring small settlements.
|
||||||
|
Communal,
|
||||||
|
/// Long-haul inter-regional routes between distant non-state settlements.
|
||||||
|
Trade,
|
||||||
|
/// Roads that exist but are no longer maintained (a Derelict-province
|
||||||
|
/// endpoint, or runtime abandonment). Not reachable from the current
|
||||||
|
/// authored data alone (D-212 Derelict is deferred) — wired for forward use.
|
||||||
|
Abandoned,
|
||||||
|
}
|
||||||
|
|
||||||
/// Territory control status for a province (drainage basin). Priority-ordered derivation.
|
/// Territory control status for a province (drainage basin). Priority-ordered derivation.
|
||||||
/// Source: D-212 (amended 2026-06-05 — see `AutonomistHeld` + the dominant_faction
|
/// Source: D-212 (amended 2026-06-05 — see `AutonomistHeld` + the dominant_faction
|
||||||
/// mapping note on the variant docs and in `attractor_matching::territorial_status_from_faction`).
|
/// mapping note on the variant docs and in `attractor_matching::territorial_status_from_faction`).
|
||||||
|
|||||||
Reference in New Issue
Block a user