From 4f52abd532dafead2243526dfb645eaee74e843b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 16 Jul 2026 18:51:43 +0200 Subject: [PATCH] feat(simulation): wire region climate layer into the cascade + proxy (T-1113, D-243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CascadeLayer::Region appended per the enum's append-only Ord rule (depends only on seed/body_params/heightmap dims — documented); BodyWorldState.regions BTreeMap per the districts precedent; RegionGridLayer dense row-major all-integer encoding (season/weather repr(u8) discriminants, mean_temp deci-degC i16 with i16::MIN airless sentinel, moisture_q u8); build_region_grid mirrors build_district_grid; protocol.gd region_grid passthrough (visual overlay deliberately out of scope); wire fixtures regenerated via make fixtures. atlas:: suite 507 green. Co-Authored-By: Claude Fable 5 --- client/scripts/protocol/protocol.gd | 12 +- .../msgpack/atlas_response_not_found.msgpack | 2 +- .../msgpack/atlas_response_pending.msgpack | 2 +- .../msgpack/atlas_response_ready.msgpack | Bin 737 -> 750 bytes server/src/atlas/body_world_state.rs | 12 ++ server/src/atlas/cascade.rs | 148 ++++++++++++++++- server/src/atlas/gen_queue.rs | 15 +- server/src/atlas/layer_proxy.rs | 153 +++++++++++++++++- server/src/atlas/plugin.rs | 1 + server/tests/gen_fixtures.rs | 3 + 10 files changed, 330 insertions(+), 18 deletions(-) diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index e0fddaebe..2e3a2ee6c 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -774,11 +774,12 @@ static func decode_atlas_layer_response(bytes: PackedByteArray) -> Variant: ## road_graph/settlements (T-960): passthrough fields for the L2 road/rail ## graph and L3 settlement placements, mirroring the district_grid precedent ## (T-1046) — raw decoded maps/arrays, no further client-side reshaping. -## Key names "road_graph"/"settlements" are the CONFIRMED wire contract — -## identical to server/src/atlas/layer_proxy.rs AtlasLayerResponse's field -## names (pinned 2026-07-14; round-tripped by test_atlas_overlays.gd and the -## server's msgpack round-trip tests). This remains the one client-side spot -## to touch if the contract ever changes. +## region_grid (T-1113): the region climate grid, same passthrough pattern. +## Key names "road_graph"/"settlements"/"region_grid" are the CONFIRMED wire +## contract — identical to server/src/atlas/layer_proxy.rs AtlasLayerResponse's +## field names (pinned 2026-07-14; round-tripped by test_atlas_overlays.gd and +## the server's msgpack round-trip tests). This remains the one client-side +## spot to touch if the contract ever changes. static func atlas_response_from_raw(raw: Variant) -> Variant: if not raw is Dictionary or not raw.has("status"): return null @@ -798,6 +799,7 @@ static func atlas_response_from_raw(raw: Variant) -> Variant: "district_grid": raw.get("district_grid"), "road_graph": raw.get("road_graph"), "settlements": raw.get("settlements"), + "region_grid": raw.get("region_grid"), } diff --git a/client/tests/fixtures/msgpack/atlas_response_not_found.msgpack b/client/tests/fixtures/msgpack/atlas_response_not_found.msgpack index ac84932a1..b45ad1b02 100644 --- a/client/tests/fixtures/msgpack/atlas_response_not_found.msgpack +++ b/client/tests/fixtures/msgpack/atlas_response_not_found.msgpack @@ -1 +1 @@ -†§body_id¥ghost¦status¨NotFound¦layer1À­district_gridÀªroad_graphÀ«settlementsÀ \ No newline at end of file +‡§body_id¥ghost¦status¨NotFound¦layer1À­district_gridÀªroad_graphÀ«settlementsÀ«region_gridÀ \ No newline at end of file diff --git a/client/tests/fixtures/msgpack/atlas_response_pending.msgpack b/client/tests/fixtures/msgpack/atlas_response_pending.msgpack index 97399184a..2ad9d65e5 100644 --- a/client/tests/fixtures/msgpack/atlas_response_pending.msgpack +++ b/client/tests/fixtures/msgpack/atlas_response_pending.msgpack @@ -1 +1 @@ -†§body_id¤GJ1c¦status§Pending¦layer1À­district_gridÀªroad_graphÀ«settlementsÀ \ No newline at end of file +‡§body_id¤GJ1c¦status§Pending¦layer1À­district_gridÀªroad_graphÀ«settlementsÀ«region_gridÀ \ No newline at end of file diff --git a/client/tests/fixtures/msgpack/atlas_response_ready.msgpack b/client/tests/fixtures/msgpack/atlas_response_ready.msgpack index ec4101d911b251af9aa0ad2183fa188ad28398c4..14c10caed35a06134ec3c18f24fc76c5d575a479 100644 GIT binary patch delta 24 fcmaFJ`i_;ceIw%oCf?OWsp*;ddGYB*nJEVVc}EHN delta 10 RcmaFI`jC~eZ6o6YCIA=P1N8s^ diff --git a/server/src/atlas/body_world_state.rs b/server/src/atlas/body_world_state.rs index a31e77077..e3284c137 100644 --- a/server/src/atlas/body_world_state.rs +++ b/server/src/atlas/body_world_state.rs @@ -15,7 +15,9 @@ use serde::{Deserialize, Serialize}; use crate::atlas::attractor_matching::CityPlacement; use crate::atlas::district_profile::{DistrictPos, DistrictProfile}; +use crate::atlas::region_profile::RegionProfile; use crate::atlas::road_graph::RoadGraph; +use crate::atlas::scale::RegionPos; use crate::simulation::generator::{ GeographicAttractor, QuarterId, QuarterWorldState, TerritorialStatus, }; @@ -100,6 +102,15 @@ pub struct BodyWorldState { /// `BTreeMap` keyed by `DistrictPos` for D-010 determinism. /// Empty until the DistrictProfile layer has run. pub districts: BTreeMap, + /// Per-region (~205 km) climate context — season/weather/temperature + /// baseline cells (D-243 §3, T-1113). + /// + /// Populated by the background cascade's Region layer; the covering grid + /// only (no blend-padding ring — see `cascade::LayerRegionOutput`). + /// `BTreeMap` keyed by `RegionPos` for D-010 determinism. Empty until the + /// Region layer has run. Footprint is trivial (~195×98 cells at the D-243 + /// true-scale ceiling; a handful on today's working grids). + pub regions: BTreeMap, /// Last sim tick this entry was read. Used for LRU eviction. pub last_accessed: SimTick, } @@ -224,6 +235,7 @@ mod tests { road_graph: RoadGraph::default(), quarters: BTreeMap::new(), districts: BTreeMap::new(), + regions: BTreeMap::new(), last_accessed: tick, } } diff --git a/server/src/atlas/cascade.rs b/server/src/atlas/cascade.rs index 33aedccca..6ff67192f 100644 --- a/server/src/atlas/cascade.rs +++ b/server/src/atlas/cascade.rs @@ -28,8 +28,9 @@ use crate::atlas::district_profile::{self, BodyParams, DistrictPos, DistrictProf use crate::atlas::features::TerrainAnalysis; use crate::atlas::heightmap::{self, BodyHeightmap, HeightmapLoadError}; use crate::atlas::layer1::{self, Layer1Output}; +use crate::atlas::region_profile::{self, RegionProfile}; use crate::atlas::road_graph::{self, RoadGraph}; -use crate::atlas::scale; +use crate::atlas::scale::{self, RegionPos}; use crate::seed::SeedChain; use crate::simulation::generator::{CompatibilityMatrix, GeographicAttractor, TerritorialStatus}; @@ -57,6 +58,16 @@ pub enum CascadeLayer { /// last** to honour the append-only `Ord` rule (it neither needs nor blocks /// the DistrictProfile layer; requesting it runs DistrictProfile first, harmlessly). RoadGraph, + /// Region climate layer (~205 km cells, D-243 §3, T-1113). Pure function of + /// `(seed, body_params, heightmap dims)` — the region baselines the district + /// layer already derives internally (and discards) are RETAINED here as + /// their own layer output for the Atlas. Semantically the climate context + /// *above* districts, but **appended last** per the append-only `Ord` rule + /// (the RoadGraph precedent): it depends on no other layer, so requesting + /// it runs the earlier layers first, harmlessly. The cheap double-derive + /// (district blend cache + this layer) is deliberate — one layer, one + /// concern, no cache plumbing between layers. + Region, } /// Output of the cascade for one body, up to the requested layer (#952). @@ -81,6 +92,9 @@ pub struct CascadeSnapshot { /// Layer 2 — inter-settlement road/rail graph. `Some` once /// [`CascadeLayer::RoadGraph`] has run (D-211, T-1038). pub road_graph: Option, + /// Region climate layer — ~205 km climate-context cells. `Some` once + /// [`CascadeLayer::Region`] has run (D-243 §3, T-1113). + pub layer_region: Option, /// **Transient** — the `TerrainAnalysis` produced by the Layer-1 drainage /// pass (T-1044). Populated when Layer 1 runs; consumed (and freed) once /// both `DistrictProfile` and `RoadGraph` have consumed it. @@ -98,6 +112,19 @@ pub struct LayerDistrictOutput { pub districts: std::collections::BTreeMap, } +/// Region climate layer output (T-1113, D-243 §3): per-region (~205 km) climate +/// context covering the body's district grid. Stored in `BodyWorldState.regions`. +/// +/// The set is the **covering grid only** — the regions whose districts tile the +/// body, with no ±1 neighbour padding. (The district layer's internal region +/// cache pads a neighbour ring because its edge-fuzz blend samples across +/// boundaries; that padding is a blend implementation detail, not part of the +/// body's own region grid, and a dense Atlas wire encoding wants exact dims.) +#[derive(Debug, Clone, Default)] +pub struct LayerRegionOutput { + pub regions: std::collections::BTreeMap, +} + /// Layer 3 output (#955, D-211): attractor-matched settlement placements for the /// body. Re-derivable from (Layer-1 attractors + settlement records + seed). #[derive(Debug, Clone, Default)] @@ -123,6 +150,7 @@ impl CascadeSnapshot { .layer_district .map(|lr| lr.districts) .unwrap_or_default(); + let regions = self.layer_region.map(|lr| lr.regions).unwrap_or_default(); let road_graph = self.road_graph.unwrap_or_default(); // terrain_analysis (transient) is intentionally dropped here. let _ = self.terrain_analysis; @@ -138,6 +166,7 @@ impl CascadeSnapshot { road_graph, quarters: std::collections::BTreeMap::new(), districts, + regions, last_accessed: 0, } } @@ -203,6 +232,7 @@ pub fn run_cascade_from_heightmap( layer3: None, layer_district: None, road_graph: None, + layer_region: None, terrain_analysis: None, }; @@ -330,6 +360,42 @@ pub fn run_cascade_from_heightmap( snapshot.terrain_analysis = None; } + // Region climate layer (D-243 §3, T-1113) — the ~205 km climate-context + // cells the district blend already derives internally, retained as their + // own layer output. Pure function of (seed, body_params, heightmap dims): + // no TerrainAnalysis needed, so it runs outside the transient-borrow block + // above. Gates on body_params like the DistrictProfile layer (no params → + // no climate inputs → the layer skips, `regions` stays empty). + if up_to >= CascadeLayer::Region { + if let Some(params) = body_params { + // The covering region grid: the same district dims the district + // layer computes (heightmap dims ÷ cells-per-district), mapped up + // to region cells — WITHOUT the ±1 neighbour padding the district + // blend cache adds (see LayerRegionOutput's doc). + let gcpr = scale::HEIGHTMAP_CELLS_PER_DISTRICT.max(1); + let district_cols = (snapshot.heightmap.width as usize).div_ceil(gcpr) as i32; + let district_rows = (snapshot.heightmap.height as usize).div_ceil(gcpr) as i32; + let max_region = scale::district_to_region(( + district_cols.saturating_sub(1), + district_rows.saturating_sub(1), + )); + let mut region_positions: Vec = Vec::new(); + for ry in 0..=max_region.1 { + for rx in 0..=max_region.0 { + region_positions.push((rx, ry)); + } + } + let climate = district_profile::ClimateConstants::default(); + let regions = region_profile::derive_regions_for_body( + body_seed, + params, + &climate, + region_positions, + ); + snapshot.layer_region = Some(LayerRegionOutput { regions }); + } + } + snapshot } @@ -461,6 +527,7 @@ mod tests { assert!(CascadeLayer::Topography < CascadeLayer::Settlement); assert!(CascadeLayer::Settlement < CascadeLayer::DistrictProfile); assert!(CascadeLayer::DistrictProfile < CascadeLayer::RoadGraph); + assert!(CascadeLayer::RoadGraph < CascadeLayer::Region); } #[test] @@ -794,4 +861,83 @@ mod tests { // Silence unused-import warning when the filter above changes. let _ = RoadNodeKind::Settlement; } + + /// Region climate layer (T-1113, D-243 §3): runs as the cascade terminal, + /// populates `BodyWorldState.regions` with the covering region grid, gates + /// on body_params like the DistrictProfile layer, and is deterministic. + #[test] + fn region_layer_populates_regions_deterministically() { + use crate::atlas::district_profile::BodyParams; + + let params = BodyParams { + hydrosphere: Some("ocean".into()), + atmosphere: Some("breathable".into()), + planet_class: Some("temperate".into()), + ..Default::default() + }; + let run = || { + run_cascade_from_heightmap( + body_seed(), + test_heightmap(), + &[], + None, + Some(¶ms), + CascadeLayer::Region, + ) + }; + let snap = run(); + let lr = snap.layer_region.as_ref().expect("Region layer ran"); + // 64×32 working grid → 8×4 districts → a single covering region at + // (0,0) (100 districts per region side — the working grid is far + // inside one region cell today; the D-243 elastic seam grows this). + assert_eq!(lr.regions.len(), 1, "one covering region on the test grid"); + let profile = lr.regions.get(&(0, 0)).expect("region (0,0) present"); + assert!( + profile.clock.mean_temp_c.is_some(), + "breathable temperate body derives a temperature baseline" + ); + assert!((0..=100).contains(&profile.moisture_q)); + + // Determinism: identical inputs → bit-identical region output. + let key = |s: &CascadeSnapshot| { + s.layer_region + .as_ref() + .unwrap() + .regions + .iter() + .map(|(pos, p)| { + ( + *pos, + p.clock.season as u8, + p.clock.weather as u8, + p.clock.mean_temp_c.map(f32::to_bits), + p.moisture_q, + ) + }) + .collect::>() + }; + assert_eq!( + key(&snap), + key(&run()), + "region layer must be deterministic" + ); + + // The regions propagate into the hot-cache BodyWorldState. + let state = snap.into_body_world_state(); + assert_eq!(state.regions.len(), 1); + assert!(state.regions.contains_key(&(0, 0))); + + // No body params → the layer skips and regions stays empty (mirrors + // the DistrictProfile gate). + let no_params = run_cascade_from_heightmap( + body_seed(), + test_heightmap(), + &[], + None, + None, + CascadeLayer::Region, + ); + assert!(no_params.layer_region.is_none()); + assert!(no_params.into_body_world_state().regions.is_empty()); + } } diff --git a/server/src/atlas/gen_queue.rs b/server/src/atlas/gen_queue.rs index 15fda3c69..614a28b6f 100644 --- a/server/src/atlas/gen_queue.rs +++ b/server/src/atlas/gen_queue.rs @@ -407,13 +407,14 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion { } else { hm }; - // Run the full cascade through RoadGraph (Layer 2, T-1038), the - // terminal layer. It subsumes Settlement, DistrictProfile (T-1023), - // and all prior layers. DistrictProfile derivation still gates on - // body_params internally (skipped when absent — e.g. a body with no - // params row), but the road graph needs no body params, so it runs - // for every analyzed body. - let up_to = CascadeLayer::RoadGraph; + // Run the full cascade through Region (T-1113), the terminal + // layer. It subsumes RoadGraph (T-1038), Settlement, + // DistrictProfile (T-1023), and all prior layers. + // DistrictProfile and Region derivation both gate on + // body_params internally (skipped when absent — e.g. a body + // with no params row), but the road graph needs no body + // params, so it runs for every analyzed body. + let up_to = CascadeLayer::Region; let snapshot = run_cascade_from_heightmap( *body_seed, working, diff --git a/server/src/atlas/layer_proxy.rs b/server/src/atlas/layer_proxy.rs index 253221afd..83d88513d 100644 --- a/server/src/atlas/layer_proxy.rs +++ b/server/src/atlas/layer_proxy.rs @@ -70,13 +70,13 @@ 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), or -/// a non-ready status. +/// (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. /// /// 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) and T-1113 (region climate) as the only remaining candidates. +/// footprints) as the only remaining candidate. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AtlasLayerResponse { pub body_id: String, @@ -94,6 +94,9 @@ pub struct AtlasLayerResponse { /// 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, + /// 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, } /// Build the coarse [`DistrictGridLayer`] from a body's cached state (T-1046). @@ -129,6 +132,83 @@ pub fn build_district_grid( }) } +// --------------------------------------------------------------------------- +// RegionGridLayer (T-1113, D-243 §3) +// --------------------------------------------------------------------------- + +/// The ~205 km region climate grid for the Atlas overlay (T-1113), dense +/// row-major like [`DistrictGridLayer`] (the T-1046 encoding precedent). +/// Serves the **mean-state** `RegionClock` fields only — the Q-105 tick-phase +/// callbacks are deferred, so what ships is the static climate context. +/// +/// Wire encoding is all-integer (D-010 wire discipline): +/// - `season[i]` / `weather[i]` — the `repr(u8)` discriminants of +/// `SeasonPhase` / `WeatherState` (pinned, append-only). +/// - `mean_temp_dc[i]` — mean-annual temperature baseline in **deci-°C** +/// (×10, `round`ed; 0.1 °C is ample for a map overlay). `i16::MIN` is the +/// sentinel for "no atmosphere → no temperature" (airless bodies carry +/// `mean_temp_c: None`); real values are class-band-clamped far inside +/// i16 range. +/// - `moisture_q[i]` — the 0–100 region moisture primitive. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegionGridLayer { + pub cols: u32, + pub rows: u32, + pub season: Vec, + pub weather: Vec, + pub mean_temp_dc: Vec, + pub moisture_q: Vec, +} + +/// Sentinel for "airless body — no temperature baseline" in +/// [`RegionGridLayer::mean_temp_dc`]. +pub const REGION_TEMP_NONE_DC: i16 = i16::MIN; + +/// Build the [`RegionGridLayer`] from a body's cached state (T-1113). +/// Returns `None` when the Region layer has not run (empty `regions`). +/// The stored region set is the dense covering grid `[0, cols) × [0, rows)` +/// (see `cascade::LayerRegionOutput` — no blend-padding ring), so the extent +/// comes from the maximum `RegionPos`, mirroring [`build_district_grid`]. +pub fn build_region_grid( + state: &crate::atlas::body_world_state::BodyWorldState, +) -> Option { + if state.regions.is_empty() { + return None; + } + let cols = state.regions.keys().map(|(x, _)| *x).max().unwrap_or(0) as u32 + 1; + let rows = state.regions.keys().map(|(_, y)| *y).max().unwrap_or(0) as u32 + 1; + let n = (cols * rows) as usize; + let mut season = vec![0u8; n]; + let mut weather = vec![0u8; n]; + let mut mean_temp_dc = vec![REGION_TEMP_NONE_DC; n]; + let mut moisture_q = vec![0u8; n]; + for (&(x, y), profile) in &state.regions { + if x < 0 || y < 0 { + continue; + } + let i = (y as u32 * cols + x as u32) as usize; + if i < n { + season[i] = profile.clock.season as u8; + weather[i] = profile.clock.weather as u8; + mean_temp_dc[i] = match profile.clock.mean_temp_c { + Some(t) => { + ((t * 10.0).round() as i32).clamp(i16::MIN as i32 + 1, i16::MAX as i32) as i16 + } + None => REGION_TEMP_NONE_DC, + }; + moisture_q[i] = profile.moisture_q.clamp(0, 100) as u8; + } + } + Some(RegionGridLayer { + cols, + rows, + season, + weather, + mean_temp_dc, + moisture_q, + }) +} + // --------------------------------------------------------------------------- // RoadGraphLayer (T-960 §1, T-1038) // --------------------------------------------------------------------------- @@ -341,6 +421,7 @@ pub fn handle_atlas_request( let district_grid = build_district_grid(state); let road_graph = build_road_graph_layer(state); let settlements = build_settlement_layer(state); + let region_grid = build_region_grid(state); return AtlasLayerResponse { body_id: req.body_id.clone(), status: AtlasLayerStatus::Ready, @@ -348,6 +429,7 @@ pub fn handle_atlas_request( district_grid, road_graph, settlements, + region_grid, }; } @@ -420,6 +502,7 @@ pub fn handle_atlas_request( district_grid: None, road_graph: None, settlements: None, + region_grid: None, } } // Unknown / no terrain → re-requesting won't help. @@ -431,6 +514,7 @@ pub fn handle_atlas_request( district_grid: None, road_graph: None, settlements: None, + region_grid: None, }, Err(e) => AtlasLayerResponse { body_id: req.body_id.clone(), @@ -439,6 +523,7 @@ pub fn handle_atlas_request( district_grid: None, road_graph: None, settlements: None, + region_grid: None, }, } } @@ -487,6 +572,7 @@ mod tests { road_graph: crate::atlas::road_graph::RoadGraph::default(), quarters: std::collections::BTreeMap::new(), districts: std::collections::BTreeMap::new(), + regions: std::collections::BTreeMap::new(), last_accessed: 0, }; // 3×2 grid with two distinct zones at the corners. @@ -512,6 +598,64 @@ mod tests { assert!(build_district_grid(&state).is_none()); } + /// T-1113: the region climate grid mirrors the district-grid encoding — + /// `None` when the Region layer hasn't run; dense row-major with the + /// integer wire quantization (deci-°C temp, `i16::MIN` airless sentinel) + /// when it has. + #[test] + fn build_region_grid_encodes_dense_quantized_climate() { + use crate::atlas::region_profile::{RegionClock, RegionProfile, SeasonPhase, WeatherState}; + + let mut state = blank_state("GJ1c"); + // Empty regions → None (the Region layer hasn't run). + assert!(build_region_grid(&state).is_none()); + + // A 2×1 covering grid: one temperate region, one airless-style region + // (mean_temp_c = None → the sentinel). + state.regions.insert( + (0, 0), + RegionProfile { + pos: (0, 0), + clock: RegionClock { + season: SeasonPhase::Summer, + weather: WeatherState::Clear, + mean_temp_c: Some(12.34), + }, + latitude_deg: 45.0, + moisture_q: 80, + }, + ); + state.regions.insert( + (1, 0), + RegionProfile { + pos: (1, 0), + clock: RegionClock { + season: SeasonPhase::Winter, + weather: WeatherState::Snow, + mean_temp_c: None, + }, + latitude_deg: -10.0, + moisture_q: 5, + }, + ); + + let grid = build_region_grid(&state).expect("regions present → Some grid"); + assert_eq!((grid.cols, grid.rows), (2, 1)); + assert_eq!(grid.season.len(), 2); + assert_eq!(grid.season[0], SeasonPhase::Summer as u8); + assert_eq!(grid.weather[0], WeatherState::Clear as u8); + // 12.34 °C → 123 deci-°C (rounded). + assert_eq!(grid.mean_temp_dc[0], 123); + assert_eq!(grid.moisture_q[0], 80); + assert_eq!(grid.season[1], SeasonPhase::Winter as u8); + assert_eq!(grid.weather[1], WeatherState::Snow as u8); + assert_eq!( + grid.mean_temp_dc[1], REGION_TEMP_NONE_DC, + "airless None maps to the sentinel" + ); + assert_eq!(grid.moisture_q[1], 5); + } + /// 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 { @@ -527,6 +671,7 @@ mod tests { road_graph: crate::atlas::road_graph::RoadGraph::default(), quarters: std::collections::BTreeMap::new(), districts: std::collections::BTreeMap::new(), + regions: std::collections::BTreeMap::new(), last_accessed: 0, } } @@ -728,6 +873,7 @@ mod tests { district_grid: None, road_graph: build_road_graph_layer(&state), settlements: build_settlement_layer(&state), + region_grid: build_region_grid(&state), }; let bytes = rmp_serde::to_vec_named(&resp).expect("encode"); @@ -829,6 +975,7 @@ mod tests { road_graph: crate::atlas::road_graph::RoadGraph::default(), quarters: std::collections::BTreeMap::new(), districts: std::collections::BTreeMap::new(), + regions: std::collections::BTreeMap::new(), last_accessed: 0, }); let (_db, resolver) = empty_resolver(); diff --git a/server/src/atlas/plugin.rs b/server/src/atlas/plugin.rs index 69551e4a0..2124d8f1c 100644 --- a/server/src/atlas/plugin.rs +++ b/server/src/atlas/plugin.rs @@ -113,6 +113,7 @@ fn serve_atlas_requests( district_grid: None, road_graph: None, settlements: None, + region_grid: None, }, }; responses.0.push(resp); diff --git a/server/tests/gen_fixtures.rs b/server/tests/gen_fixtures.rs index bc88579d3..9a60a9808 100644 --- a/server/tests/gen_fixtures.rs +++ b/server/tests/gen_fixtures.rs @@ -624,6 +624,7 @@ fn generate_atlas_layer_response_fixtures() { district_grid: None, road_graph: Some(road_graph), settlements: Some(settlements), + region_grid: None, }; write_fixture( "atlas_response_ready", @@ -637,6 +638,7 @@ fn generate_atlas_layer_response_fixtures() { district_grid: None, road_graph: None, settlements: None, + region_grid: None, }; write_fixture( "atlas_response_pending", @@ -650,6 +652,7 @@ fn generate_atlas_layer_response_fixtures() { district_grid: None, road_graph: None, settlements: None, + region_grid: None, }; write_fixture( "atlas_response_not_found",