feat(simulation): T-1119 QuarterFootprintLayer — server half (touch points 1-2) + populated wire fixtures

ZoningType gains PartialOrd/Ord (DistrictType T-994 mirror) for the
dominant_zoning declaration-order tie-break. layer_proxy:
QuarterFootprintEntry/QuarterFootprintLayer (BTreeMap keyed by
city_id; density_avg_pct floor-mean, dominant_* mode with
declaration-order tie-break, landmark/corridor counts) +
build_quarter_footprint_layer gated None on empty state.quarters;
sixth AtlasLayerResponse field wired into all construction sites;
growth-ceiling doc updated to say the whole-body budget is consumed
(what carries a seventh is T-1124's design, resolved in the same
branch's amendment). Placements whose derived Layer4Quarter id is not
yet in state.quarters are SKIPPED (skeleton gen is async Low-priority
— entries may be a proper subset; client decodes defensively). Stale
TODO(#957) removed (premise predates T-957 landing).

Tie-break bug caught by its own test: Iterator::max_by_key over a
BTreeMap returns the LAST max on ties — backwards from
declaration-order-first; replaced with an explicit forward walk that
replaces only on strictly-greater count.

gen_fixtures: populated RegionGridLayer (T-1118 replay coverage) +
QuarterFootprintLayer literals in the ready fixture; msgpack fixtures
regenerated via make fixtures. Note: landmark_count is inert on real
data today (BlockSkeleton.landmark hardcoded None until landmark
placement lands); fixtures use non-zero counts to exercise the logic.

5 new unit tests + populated round-trip; atlas:: 549 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:03:28 +02:00
co-authored by Claude Fable 5
parent 67bd0ec294
commit 8165c61186
7 changed files with 599 additions and 18 deletions
@@ -1 +1 @@
§body_id¥ghost¦status¨NotFound¦layer1À­district_gridÀªroad_graphÀ«settlementsÀ«region_gridÀ
ˆ§body_id¥ghost¦status¨NotFound¦layer1À­district_gridÀªroad_graphÀ«settlementsÀ«region_gridÀ²quarter_footprintsÀ
@@ -1 +1 @@
𣇪body_id二J1c存tatus判ending奸ayer1嶺district_grid尷road_graph屨settlements屨region_grid
êbody_id二J1c存tatus判ending奸ayer1嶺district_grid尷road_graph屨settlements屨region_grid徽quarter_footprints
Binary file not shown.
+530 -8
View File
@@ -21,8 +21,8 @@ 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};
use crate::seed::{SeedChain, SeedDomain};
use crate::simulation::generator::{AttractorType, DistrictType, MaintenanceAuthority, ZoningType};
/// Fallback sea level when the heightmap PNG carries no `sea_level` tEXt chunk
/// (the loader prefers the chunk; this is only the floor).
@@ -71,12 +71,16 @@ pub struct DistrictGridLayer {
/// A layer response: the computed `Layer1Output` + the coarse district grid
/// (D-225, T-1046) + the road-graph and settlement overlays (T-960 §1/§2) +
/// the region climate grid (T-1113), or a non-ready status.
/// the region climate grid (T-1113) + the quarter-footprint overlay (T-1112,
/// T-1119), or a non-ready status.
///
/// Growth ceiling (governance-bounded): the one-`Option`-field-per-layer
/// pattern tops out around six fields — D-226's 2026-07-13 amendment (d)
/// rules out any L5/tile Atlas layer ever, leaving T-1112 (quarter
/// footprints) as the only remaining candidate.
/// pattern tops out at six fields — D-226's 2026-07-13 amendment (d) rules
/// out any L5/tile Atlas layer ever, and `quarter_footprints` below is the
/// last candidate the 2026-07-16 T-1112 amendment named. **The budget is now
/// consumed** — a seventh field is not a naming exercise like the six before
/// it; what (if anything) carries a future generation-layer addition is a
/// T-1124 design-pass question, not something to resolve here.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AtlasLayerResponse {
pub body_id: String,
@@ -97,6 +101,13 @@ pub struct AtlasLayerResponse {
/// The region climate grid for the Atlas overlay (D-243 §3, T-1113).
/// `Some` on a cache hit once the Region layer has run; `None` otherwise.
pub region_grid: Option<RegionGridLayer>,
/// The quarter-footprint overlay (D-226 T-1112 amendment, T-1119). `Some`
/// on a cache hit once at least one settlement's quarter skeleton has been
/// generated (`state.quarters` non-empty); `None` otherwise, including a
/// body with placed settlements whose quarters haven't finished the async
/// `GenerateSkeleton` pass yet (skeleton generation runs at `Low` priority
/// after the body's own `Ready` snapshot is cached — see `plugin.rs`).
pub quarter_footprints: Option<QuarterFootprintLayer>,
}
/// Build the coarse [`DistrictGridLayer`] from a body's cached state (T-1046).
@@ -209,6 +220,148 @@ pub fn build_region_grid(
})
}
// ---------------------------------------------------------------------------
// QuarterFootprintLayer (D-226 T-1112 amendment, T-1119)
// ---------------------------------------------------------------------------
/// Per-settlement aggregate over one quarter's 4×4 `BlockSkeleton` grid, for
/// the Atlas quarter-footprint overlay (D-226 T-1112 amendment §1). Five
/// scalar fields earn their place per the amendment's hard ceiling (§2): no
/// per-block zoning/street/tag detail ever reaches the wire, and no
/// chunk/tile/voxel data is touched.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuarterFootprintEntry {
pub city_id: u64,
/// Basis-point mean of `BlockSkeleton.density_pct` across the 16 blocks
/// (integer division, D-010 — no `f32` on the wire).
pub density_avg_pct: u8,
/// Mode `DistrictType` across the 16 blocks; ties resolve to the lowest
/// declaration-order variant (the `Ord` derive on `DistrictType`, T-994).
pub dominant_district_type: DistrictType,
/// Mode `ZoningType` across the 16 blocks; same tie rule (the `Ord`
/// derive added on `ZoningType` for this ticket, T-1119).
pub dominant_zoning: ZoningType,
/// Count of blocks with `landmark: Some(_)` across the 16 blocks (max 16).
/// Tooltip/sidebar-only per the D-226(d) ceiling — never a map-visible
/// channel (§2).
pub landmark_count: u8,
/// `QuarterSkeleton.corridors.len()`, clamped to `u8`. Tooltip/sidebar-only,
/// same ceiling as `landmark_count`.
pub corridor_count: u8,
}
/// The quarter-footprint overlay for one body (D-226 T-1112 amendment §1),
/// keyed by `city_id` — a quarter carries no independent spatial position of
/// its own (`QuarterId` is a content-addressable hash, not a coordinate), so
/// the layer anchors at the existing L3 settlement position client-side and
/// this map only needs to answer "does this settlement have quarter data, and
/// if so what does it aggregate to".
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuarterFootprintLayer {
/// `BTreeMap` for D-010 determinism, matching `RegionGridLayer`'s and the
/// source `QuarterWorldState.block_tags`' own `BTreeMap` precedent.
pub entries: std::collections::BTreeMap<u64, QuarterFootprintEntry>,
}
/// Aggregate one quarter's 16 `BlockSkeleton`s into a [`QuarterFootprintEntry`]
/// for `city_id`.
fn aggregate_quarter_footprint(
city_id: u64,
skeleton: &crate::simulation::generator::QuarterSkeleton,
) -> QuarterFootprintEntry {
let blocks: Vec<&crate::simulation::generator::BlockSkeleton> =
skeleton.blocks.iter().flatten().collect();
let n = blocks.len() as u32; // always 16 (the fixed 4×4 grid) — computed
// rather than hardcoded so the mean formula
// stays correct if the grid shape ever changes.
let density_sum: u32 = blocks.iter().map(|b| b.density_pct as u32).sum();
let density_avg_pct = if n == 0 { 0 } else { (density_sum / n) as u8 };
let dominant_district_type = mode_by_declaration_order(blocks.iter().map(|b| &b.district_type))
.cloned()
.unwrap_or_default();
let dominant_zoning = mode_by_declaration_order(blocks.iter().map(|b| &b.zoning))
.cloned()
.unwrap_or_default();
let landmark_count = blocks.iter().filter(|b| b.landmark.is_some()).count() as u8;
let corridor_count = skeleton.corridors.len().min(u8::MAX as usize) as u8;
QuarterFootprintEntry {
city_id,
density_avg_pct,
dominant_district_type,
dominant_zoning,
landmark_count,
corridor_count,
}
}
/// Mode of an `Ord` value over an iterator, tie-broken by lowest declaration
/// order (i.e. the `Ord`-smallest value among the tied-for-max-count values).
/// `None` for an empty iterator.
///
/// **Not** `counts.into_iter().max_by_key(...)`: `Iterator::max_by_key`
/// returns the *last* maximum on a tie (documented behaviour), which is the
/// opposite of what's needed here. `BTreeMap` iterates keys in ascending
/// `Ord` order (= declaration order for these enums), so walking forward and
/// only replacing the running best on a *strictly greater* count keeps the
/// first-seen — i.e. lowest-declaration-order — winner on every tie.
fn mode_by_declaration_order<'a, T: Ord + 'a>(
values: impl Iterator<Item = &'a T>,
) -> Option<&'a T> {
let mut counts: std::collections::BTreeMap<&'a T, u32> = std::collections::BTreeMap::new();
for v in values {
*counts.entry(v).or_insert(0) += 1;
}
let mut best: Option<(&'a T, u32)> = None;
for (v, count) in counts {
match best {
Some((_, best_count)) if count <= best_count => {}
_ => best = Some((v, count)),
}
}
best.map(|(v, _)| v)
}
/// Build the [`QuarterFootprintLayer`] from a body's cached state (T-1119).
/// Returns `None` when the Quarter-skeleton layer has not run for any
/// settlement (empty `state.quarters`).
///
/// `state.quarters` carries no independent spatial position — the only
/// spatial anchor a quarter has is the `city_id` it was generated for
/// (D-226 T-1112 amendment §1). So this recomputes the same deterministic
/// `QuarterId` derivation the L3→L4 dispatch path uses
/// (`SeedChain::for_body(world_seed, body_id).derive(SeedDomain::Layer4Quarter,
/// city_id).seed()`, `plugin.rs::build_skeleton_work_item`) for every placed
/// settlement and looks it up in `state.quarters`. A placement whose derived
/// id isn't found (skeleton generation is async, dispatched at `Low` priority
/// after the body's `Ready` snapshot is already cached — `plugin.rs`) is
/// skipped, not defaulted: an absent quarter is not a zero-footprint quarter.
pub fn build_quarter_footprint_layer(
state: &BodyWorldState,
world_seed: u64,
) -> Option<QuarterFootprintLayer> {
if state.quarters.is_empty() {
return None;
}
let body_chain = SeedChain::for_body(world_seed, &state.body_id);
let mut entries = std::collections::BTreeMap::new();
for placement in &state.placements {
let quarter_id = body_chain
.derive(SeedDomain::Layer4Quarter, placement.city_id)
.seed();
if let Some(quarter_state) = state.quarters.get(&quarter_id) {
entries.insert(
placement.city_id,
aggregate_quarter_footprint(placement.city_id, &quarter_state.skeleton),
);
}
}
Some(QuarterFootprintLayer { entries })
}
// ---------------------------------------------------------------------------
// RoadGraphLayer (T-960 §1, T-1038)
// ---------------------------------------------------------------------------
@@ -422,6 +575,7 @@ pub fn handle_atlas_request(
let road_graph = build_road_graph_layer(state);
let settlements = build_settlement_layer(state);
let region_grid = build_region_grid(state);
let quarter_footprints = build_quarter_footprint_layer(state, world_seed);
return AtlasLayerResponse {
body_id: req.body_id.clone(),
status: AtlasLayerStatus::Ready,
@@ -430,6 +584,7 @@ pub fn handle_atlas_request(
road_graph,
settlements,
region_grid,
quarter_footprints,
};
}
@@ -503,6 +658,7 @@ pub fn handle_atlas_request(
road_graph: None,
settlements: None,
region_grid: None,
quarter_footprints: None,
}
}
// Unknown / no terrain → re-requesting won't help.
@@ -515,6 +671,7 @@ pub fn handle_atlas_request(
road_graph: None,
settlements: None,
region_grid: None,
quarter_footprints: None,
},
Err(e) => AtlasLayerResponse {
body_id: req.body_id.clone(),
@@ -524,6 +681,7 @@ pub fn handle_atlas_request(
road_graph: None,
settlements: None,
region_grid: None,
quarter_footprints: None,
},
}
}
@@ -656,6 +814,338 @@ mod tests {
assert_eq!(grid.moisture_q[1], 5);
}
/// T-1119: `build_quarter_footprint_layer` returns `None` when no
/// settlement's quarter skeleton has been generated (`state.quarters`
/// empty), mirroring `build_district_grid`/`build_region_grid`'s
/// "unrun layer → None" contract.
#[test]
fn quarter_footprint_layer_none_when_quarters_empty() {
let state = blank_state("GJ1c");
assert!(build_quarter_footprint_layer(&state, 42).is_none());
}
/// A `BlockSkeleton` fixture builder for quarter-footprint tests — only
/// the fields the aggregate reads are wired; the rest default.
fn block(
zoning: crate::simulation::generator::ZoningType,
district_type: DistrictType,
density_pct: u8,
landmark: Option<crate::simulation::generator::LandmarkSlot>,
) -> crate::simulation::generator::BlockSkeleton {
crate::simulation::generator::BlockSkeleton {
zoning,
district_type,
density_pct,
landmark,
..Default::default()
}
}
/// T-1119: a populated quarter aggregates correctly — the density mean,
/// the dominant-mode fields (including a tie resolving to the lowest
/// declaration-order variant per the D-226 T-1112 amendment §1), the
/// landmark count, and the corridor count.
#[test]
fn quarter_footprint_layer_aggregates_populated_quarter() {
use crate::atlas::attractor_matching::CityPlacement;
use crate::simulation::generator::{
ArrangementPattern, FoundingOrientation, PoliticalArchetype, ZoningType,
};
let mut state = blank_state("GJ1c");
let placement = CityPlacement {
city_id: 7,
name: "Millbrook".into(),
position: (30, 40),
attractor_type: AttractorType::ValleyFloor,
score: 500,
synthetic: false,
political_archetype: PoliticalArchetype::Commission,
arrangement_pattern: ArrangementPattern::RadialCore,
founding_orientation: FoundingOrientation::Cardinal,
population: 200_000,
is_capital: false,
is_standalone_hq: false,
};
state.placements = vec![placement.clone()];
let world_seed = 42;
let quarter_id = SeedChain::for_body(world_seed, "GJ1c")
.derive(SeedDomain::Layer4Quarter, placement.city_id)
.seed();
// 16 blocks: 10 Commercial/Commercial, 6 Industrial/Industrial — a
// clean (non-tied) mode on both district_type and zoning, plus a
// known density mean and landmark/corridor counts.
let mut blocks: [[crate::simulation::generator::BlockSkeleton; 4]; 4] = Default::default();
let mut flat: Vec<&mut crate::simulation::generator::BlockSkeleton> =
blocks.iter_mut().flatten().collect();
for (i, b) in flat.iter_mut().enumerate() {
if i < 10 {
**b = block(ZoningType::Commercial, DistrictType::Commercial, 60, None);
} else {
**b = block(
ZoningType::Industrial,
DistrictType::Industrial,
20,
Some("landmark".to_string()),
);
}
}
// 3 landmarks among the Industrial blocks (indices 10, 11, 12).
*flat[10] = block(
ZoningType::Industrial,
DistrictType::Industrial,
20,
Some("A".to_string()),
);
*flat[11] = block(
ZoningType::Industrial,
DistrictType::Industrial,
20,
Some("B".to_string()),
);
*flat[12] = block(
ZoningType::Industrial,
DistrictType::Industrial,
20,
Some("C".to_string()),
);
for b in flat.iter_mut().skip(13) {
**b = block(ZoningType::Industrial, DistrictType::Industrial, 20, None);
}
// (10 * 60 + 6 * 20) / 16 = 720 / 16 = 45.
state.quarters.insert(
quarter_id,
crate::simulation::generator::QuarterWorldState {
skeleton: crate::simulation::generator::QuarterSkeleton {
quarter_id,
blocks,
corridors: vec![
crate::simulation::generator::CorridorSpine {
from: 0,
to: 1,
path: vec![(0, 0), (4, 4)],
},
crate::simulation::generator::CorridorSpine {
from: 1,
to: 2,
path: vec![(4, 4), (8, 8)],
},
],
..Default::default()
},
block_tags: Default::default(),
},
);
let layer =
build_quarter_footprint_layer(&state, world_seed).expect("populated quarters → Some");
let entry = layer.entries.get(&7).expect("city_id 7 entry present");
assert_eq!(entry.city_id, 7);
assert_eq!(entry.density_avg_pct, 45);
assert_eq!(entry.dominant_district_type, DistrictType::Commercial);
assert_eq!(entry.dominant_zoning, ZoningType::Commercial);
assert_eq!(entry.landmark_count, 3);
assert_eq!(entry.corridor_count, 2);
}
/// T-1119: the mode tie-break resolves to the lowest declaration-order
/// variant (the `Ord` derive), per the D-226 T-1112 amendment §1's
/// explicit tie rule — this is the reason `ZoningType` gained
/// `PartialOrd`/`Ord` in this same ticket.
#[test]
fn quarter_footprint_layer_tie_breaks_by_declaration_order() {
use crate::atlas::attractor_matching::CityPlacement;
use crate::simulation::generator::{
ArrangementPattern, FoundingOrientation, PoliticalArchetype, ZoningType,
};
let mut state = blank_state("GJ1c");
let placement = CityPlacement {
city_id: 3,
name: "Farmstead Rell".into(),
position: (50, 60),
attractor_type: AttractorType::PlainCenter,
score: 100,
synthetic: false,
political_archetype: PoliticalArchetype::Commission,
arrangement_pattern: ArrangementPattern::RadialCore,
founding_orientation: FoundingOrientation::Cardinal,
population: 8_000,
is_capital: false,
is_standalone_hq: false,
};
state.placements = vec![placement.clone()];
let world_seed = 99;
let quarter_id = SeedChain::for_body(world_seed, "GJ1c")
.derive(SeedDomain::Layer4Quarter, placement.city_id)
.seed();
// 8 blocks Industrial, 8 blocks Commercial — an exact tie. Declaration
// order on both DistrictType and ZoningType lists Commercial before
// Industrial, so the tie-broken dominant must be Commercial on both.
let mut blocks: [[crate::simulation::generator::BlockSkeleton; 4]; 4] = Default::default();
for (i, b) in blocks.iter_mut().flatten().enumerate() {
*b = if i < 8 {
block(ZoningType::Industrial, DistrictType::Industrial, 50, None)
} else {
block(ZoningType::Commercial, DistrictType::Commercial, 50, None)
};
}
state.quarters.insert(
quarter_id,
crate::simulation::generator::QuarterWorldState {
skeleton: crate::simulation::generator::QuarterSkeleton {
quarter_id,
blocks,
..Default::default()
},
block_tags: Default::default(),
},
);
let layer =
build_quarter_footprint_layer(&state, world_seed).expect("populated quarters → Some");
let entry = layer.entries.get(&3).expect("city_id 3 entry present");
assert_eq!(
entry.dominant_district_type,
DistrictType::Commercial,
"tie resolves to Commercial (declared before Industrial)"
);
assert_eq!(
entry.dominant_zoning,
ZoningType::Commercial,
"tie resolves to Commercial (declared before Industrial)"
);
}
/// T-1119: a placement whose deterministically-derived `quarter_id` is
/// NOT yet in `state.quarters` (skeleton generation is async, dispatched
/// after the body's own snapshot is cached — D-226 T-1112 amendment §1)
/// is skipped, not defaulted. `entries` is a subset of `placements`.
#[test]
fn quarter_footprint_layer_skips_placement_without_matching_quarter() {
use crate::atlas::attractor_matching::CityPlacement;
use crate::simulation::generator::{
ArrangementPattern, FoundingOrientation, PoliticalArchetype, ZoningType,
};
let mut state = blank_state("GJ1c");
let has_quarter = 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,
is_standalone_hq: false,
};
let no_quarter_yet = CityPlacement {
city_id: 2,
name: "Farmstead Rell".into(),
..has_quarter.clone()
};
state.placements = vec![has_quarter.clone(), no_quarter_yet];
let world_seed = 42;
let quarter_id = SeedChain::for_body(world_seed, "GJ1c")
.derive(SeedDomain::Layer4Quarter, has_quarter.city_id)
.seed();
let mut blocks: [[crate::simulation::generator::BlockSkeleton; 4]; 4] = Default::default();
for b in blocks.iter_mut().flatten() {
*b = block(ZoningType::Mixed, DistrictType::MixedUse, 10, None);
}
state.quarters.insert(
quarter_id,
crate::simulation::generator::QuarterWorldState {
skeleton: crate::simulation::generator::QuarterSkeleton {
quarter_id,
blocks,
..Default::default()
},
block_tags: Default::default(),
},
);
let layer =
build_quarter_footprint_layer(&state, world_seed).expect("populated quarters → Some");
assert_eq!(
layer.entries.len(),
1,
"only the placement with a matching quarter gets an entry"
);
assert!(layer.entries.contains_key(&1));
assert!(
!layer.entries.contains_key(&2),
"city_id 2 has no generated quarter yet — must be absent, not defaulted"
);
}
/// T-1119 (D-010): building the layer twice from identical state produces
/// byte-identical output — the `city_id → quarter_id` derivation and the
/// mode aggregation are pure functions of their inputs.
#[test]
fn quarter_footprint_layer_is_deterministic() {
use crate::atlas::attractor_matching::CityPlacement;
use crate::simulation::generator::{
ArrangementPattern, FoundingOrientation, PoliticalArchetype, ZoningType,
};
let mut state = blank_state("GJ1c");
let placement = CityPlacement {
city_id: 5,
name: "Groombridge".into(),
position: (1, 1),
attractor_type: AttractorType::PlainCenter,
score: 300,
synthetic: false,
political_archetype: PoliticalArchetype::Commission,
arrangement_pattern: ArrangementPattern::RadialCore,
founding_orientation: FoundingOrientation::Cardinal,
population: 60_000,
is_capital: false,
is_standalone_hq: false,
};
state.placements = vec![placement.clone()];
let world_seed = 7;
let quarter_id = SeedChain::for_body(world_seed, "GJ1c")
.derive(SeedDomain::Layer4Quarter, placement.city_id)
.seed();
let mut blocks: [[crate::simulation::generator::BlockSkeleton; 4]; 4] = Default::default();
for (i, b) in blocks.iter_mut().flatten().enumerate() {
*b = block(
ZoningType::Residential,
DistrictType::Residential,
(i as u8) * 5,
None,
);
}
state.quarters.insert(
quarter_id,
crate::simulation::generator::QuarterWorldState {
skeleton: crate::simulation::generator::QuarterSkeleton {
quarter_id,
blocks,
..Default::default()
},
block_tags: Default::default(),
},
);
let a = build_quarter_footprint_layer(&state, world_seed);
let b = build_quarter_footprint_layer(&state, world_seed);
assert_eq!(a, b, "identical state must produce identical output");
}
/// 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 {
@@ -819,7 +1309,7 @@ mod tests {
assert!(build_settlement_layer(&unrun).is_none());
}
/// T-960: the new layers survive a MessagePack round trip inside
/// T-960 / T-1119: 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).
@@ -828,7 +1318,9 @@ mod tests {
use crate::atlas::attractor_matching::CityPlacement;
use crate::atlas::road_graph::{RoadEdge, RoadGraph, RoadNode};
use crate::simulation::generator::{
ArrangementPattern, FoundingOrientation, MaintenanceAuthority, PoliticalArchetype,
ArrangementPattern, BlockSkeleton, DistrictType, FoundingOrientation,
MaintenanceAuthority, PoliticalArchetype, QuarterSkeleton, QuarterWorldState,
ZoningType,
};
let mut state = blank_state("GJ1c");
@@ -865,6 +1357,28 @@ mod tests {
is_rail: false,
}],
};
let world_seed = 42;
let quarter_id = SeedChain::for_body(world_seed, "GJ1c")
.derive(SeedDomain::Layer4Quarter, 1)
.seed();
let mut block = BlockSkeleton {
zoning: ZoningType::Commercial,
district_type: DistrictType::Commercial,
density_pct: 40,
..Default::default()
};
block.position = (0, 0);
state.quarters.insert(
quarter_id,
QuarterWorldState {
skeleton: QuarterSkeleton {
quarter_id,
blocks: std::array::from_fn(|_| std::array::from_fn(|_| block.clone())),
..Default::default()
},
block_tags: Default::default(),
},
);
let resp = AtlasLayerResponse {
body_id: "GJ1c".into(),
@@ -874,6 +1388,7 @@ mod tests {
road_graph: build_road_graph_layer(&state),
settlements: build_settlement_layer(&state),
region_grid: build_region_grid(&state),
quarter_footprints: build_quarter_footprint_layer(&state, world_seed),
};
let bytes = rmp_serde::to_vec_named(&resp).expect("encode");
@@ -886,6 +1401,13 @@ mod tests {
rg.edges[0].maintenance,
MaintenanceAuthority::Administrative
);
let qf = decoded
.quarter_footprints
.expect("quarter_footprints survives round trip");
let entry = qf.entries.get(&1).expect("city_id 1 entry present");
assert_eq!(entry.density_avg_pct, 40);
assert_eq!(entry.dominant_district_type, DistrictType::Commercial);
assert_eq!(entry.dominant_zoning, ZoningType::Commercial);
let settlements = decoded
.settlements
.expect("settlements survives round trip");
+2 -3
View File
@@ -121,6 +121,7 @@ fn serve_atlas_requests(
road_graph: None,
settlements: None,
region_grid: None,
quarter_footprints: None,
},
};
responses.0.push((conn_id, resp));
@@ -358,9 +359,7 @@ fn drain_generation_completions(
// Key by state.skeleton.quarter_id (D-194/D-230): a city has many
// quarters, each with its own QuarterId. `city_id` is only the
// dispatch key used in the work item — the canonical insert key is
// the quarter's own stable id. TODO(#957): the stub GenerateSkeleton
// returns a default skeleton with quarter_id=0; real gen (#957) will
// populate it from CityGenerationContext.
// the quarter's own stable id.
let _ = city_id; // used as dispatch key only; quarter_id is the map key
body_state
.quarters
+7 -1
View File
@@ -231,7 +231,13 @@ pub enum DistrictLayoutMode {
}
/// Zoning classification for a block or floor zone.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
///
/// `Ord`/`PartialOrd` (T-1119, mirroring `DistrictType`'s T-994 precedent above):
/// lets the quarter-footprint aggregate (`layer_proxy::build_quarter_footprint_layer`)
/// resolve a `dominant_zoning` mode-tie by lowest declaration-order variant.
/// Declaration order is not a stability-pinned wire format on this enum (unlike
/// `MorphologyZone` or `SeedDomain`), so this is a safe additive derive.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum ZoningType {
Commercial,
Residential,
+58 -4
View File
@@ -4,16 +4,20 @@
use settled_reach_server::atlas::body_world_state::{DrainageBasin, RiverNetwork};
use settled_reach_server::atlas::layer1::Layer1Output;
use settled_reach_server::atlas::layer_proxy::{
AtlasLayerResponse, AtlasLayerStatus, RoadGraphEdge, RoadGraphLayer, RoadGraphNode,
SettlementEntry, SettlementLayer, SettlementSizeClass,
AtlasLayerResponse, AtlasLayerStatus, QuarterFootprintEntry, QuarterFootprintLayer,
RegionGridLayer, RoadGraphEdge, RoadGraphLayer, RoadGraphNode, SettlementEntry,
SettlementLayer, SettlementSizeClass,
};
use settled_reach_server::atlas::region_profile::{SeasonPhase, WeatherState};
use settled_reach_server::atlas::road_graph::RoadNodeKind;
use settled_reach_server::bridge::types::*;
use settled_reach_server::simulation::generator::{
AttractorType, GeographicAttractor, MaintenanceAuthority, SubBiomeVariant,
AttractorType, DistrictType, GeographicAttractor, MaintenanceAuthority, SubBiomeVariant,
ZoningType,
};
use settled_reach_server::simulation::poi::PoiCategory;
use settled_reach_server::simulation::time::{DayPhase, TickRate};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
@@ -616,6 +620,53 @@ fn generate_atlas_layer_response_fixtures() {
},
],
};
// T-1113: a small populated RegionGridLayer — a 2×1 covering grid, one
// temperate region and one airless-style region (mean_temp_dc sentinel),
// mirroring the values exercised by
// `build_region_grid_encodes_dense_quantized_climate` in layer_proxy.rs.
let region_grid = RegionGridLayer {
cols: 2,
rows: 1,
season: vec![SeasonPhase::Summer as u8, SeasonPhase::Winter as u8],
weather: vec![WeatherState::Clear as u8, WeatherState::Snow as u8],
mean_temp_dc: vec![
123,
settled_reach_server::atlas::layer_proxy::REGION_TEMP_NONE_DC,
],
moisture_q: vec![80, 5],
};
// T-1119 (D-226 T-1112 amendment): a populated QuarterFootprintLayer
// covering both fixture settlements — city_id 1 (Port Aldren, a dense
// Commercial-dominant quarter with landmarks/corridors) and city_id 2
// (Farmstead Rell, a sparse Residential-dominant quarter with neither),
// so the fixture exercises both a "rich" entry and a "minimal" entry
// rather than only one shape.
let quarter_footprints = QuarterFootprintLayer {
entries: BTreeMap::from([
(
1,
QuarterFootprintEntry {
city_id: 1,
density_avg_pct: 62,
dominant_district_type: DistrictType::Commercial,
dominant_zoning: ZoningType::Commercial,
landmark_count: 3,
corridor_count: 4,
},
),
(
2,
QuarterFootprintEntry {
city_id: 2,
density_avg_pct: 18,
dominant_district_type: DistrictType::Residential,
dominant_zoning: ZoningType::Residential,
landmark_count: 0,
corridor_count: 1,
},
),
]),
};
let ready = AtlasLayerResponse {
body_id: "GJ1c".into(),
@@ -624,7 +675,8 @@ fn generate_atlas_layer_response_fixtures() {
district_grid: None,
road_graph: Some(road_graph),
settlements: Some(settlements),
region_grid: None,
region_grid: Some(region_grid),
quarter_footprints: Some(quarter_footprints),
};
write_fixture(
"atlas_response_ready",
@@ -639,6 +691,7 @@ fn generate_atlas_layer_response_fixtures() {
road_graph: None,
settlements: None,
region_grid: None,
quarter_footprints: None,
};
write_fixture(
"atlas_response_pending",
@@ -653,6 +706,7 @@ fn generate_atlas_layer_response_fixtures() {
road_graph: None,
settlements: None,
region_grid: None,
quarter_footprints: None,
};
write_fixture(
"atlas_response_not_found",