diff --git a/server/Cargo.lock b/server/Cargo.lock index 0211a4fc1..eac2ab22c 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1492,7 +1492,6 @@ dependencies = [ "serde_json", "serde_norway", "sha2", - "smallvec", "sysinfo", "thiserror", "toml", @@ -1552,9 +1551,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "smol_str" diff --git a/server/Cargo.toml b/server/Cargo.toml index 13ecd556c..d8f6dc1bc 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -31,7 +31,6 @@ bytemuck = "1" png = "0.17" toml = "0.8" aho-corasick = "1" -smallvec = { version = "1", features = ["serde"] } # Economics simulation — Leontief + tâtonnement + D-180 event port (#821) econ-sim = { path = "../tooling/econ-sim" } diff --git a/server/src/atlas/gen_queue.rs b/server/src/atlas/gen_queue.rs index 28fd621d0..eac45a142 100644 --- a/server/src/atlas/gen_queue.rs +++ b/server/src/atlas/gen_queue.rs @@ -103,8 +103,9 @@ pub enum GenCompletion { /// The body this skeleton belongs to — used to route state into /// `BodyWorldState.districts` (D-230). body_id: String, - /// District-level world state (block tags) produced by the plan phase (D-230). - state: DistrictWorldState, + /// District-level world state (skeleton + block tags) produced by the plan phase (D-230). + /// Boxed to keep `GenCompletion` variant sizes balanced (D-230 skeleton is ~2.7 KB). + state: Box, }, ChunkFilled { district_id: u64, @@ -344,7 +345,7 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion { GenCompletion::SkeletonGenerated { city_id: *city_id, body_id: String::new(), - state: DistrictWorldState::default(), + state: Box::new(DistrictWorldState::default()), } } GenWorkItem::FillChunk { diff --git a/server/src/atlas/plugin.rs b/server/src/atlas/plugin.rs index 7cbc67189..a2fbe6d72 100644 --- a/server/src/atlas/plugin.rs +++ b/server/src/atlas/plugin.rs @@ -88,9 +88,16 @@ fn drain_generation_completions( } => { if !body_id.is_empty() { if let Some(body_state) = cache.peek_mut(&body_id) { - // TODO(#957): keying by city_id is a stub — a city has multiple districts; - // use DistrictSkeleton.district_id as the DistrictId key when real skeleton gen lands. - body_state.districts.insert(city_id, state); + // Key by state.skeleton.district_id (D-194/D-230): a city has many + // districts, each with its own DistrictId. `city_id` is only the + // dispatch key used in the work item — the canonical insert key is + // the district's own stable id. TODO(#957): the stub GenerateSkeleton + // returns a default skeleton with district_id=0; real gen (#957) will + // populate it from CityGenerationContext. + let _ = city_id; // used as dispatch key only; district_id is the map key + body_state + .districts + .insert(state.skeleton.district_id, *state); } else { tracing::warn!( city_id, diff --git a/server/src/simulation/generator.rs b/server/src/simulation/generator.rs index 0f4cc6f4f..867a792c4 100644 --- a/server/src/simulation/generator.rs +++ b/server/src/simulation/generator.rs @@ -103,7 +103,7 @@ pub type PlacedObject = String; /// Determines simulation fidelity budget and NPC complexity ceiling. /// /// Source: D-218, workshop-outcomes.md -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum WorldTier { /// Hub system. Full simulation, high faction pressure. Epicenter, @@ -114,12 +114,13 @@ pub enum WorldTier { /// Transit stop. Pass-through node. Moderate complexity ceiling. Passage, /// Not simulated until player approaches. Minimal complexity ceiling. + #[default] Waypoint, } /// Generator content budget for a district. /// Derived from WorldTier + SettingType at Phase 1. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub enum ComplexityTier { /// Full social architecture. All Tier 1+2 guarantees. 20-80+ NPCs. Full, @@ -128,26 +129,34 @@ pub enum ComplexityTier { /// Minimal social architecture. Tier 1 only. 1-8 NPCs. Minimal, /// No social architecture. Pure terrain. 0 NPCs. No guarantees. + #[default] Empty, } /// Physical setting type for a district. /// Merged from Gestalt's SettingGeometry and Tyre's TerrainType. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub enum SettingType { Station, + #[default] Urban, Agricultural, Maritime, - Wilderness { biome: Biome }, - Water { water_type: WaterType }, + Wilderness { + biome: Biome, + }, + Water { + water_type: WaterType, + }, Transitional, Orbital, - Specialized { function: SpecializedFunction }, + Specialized { + function: SpecializedFunction, + }, } /// Classification of a district (high-level function). -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub enum DistrictType { LogisticsHub, Residential, @@ -155,15 +164,17 @@ pub enum DistrictType { Industrial, Administrative, Entertainment, + #[default] MixedUse, Transit, Specialized, } /// How blocks are placed within the district's 512×512 footprint. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] pub enum DistrictLayoutMode { /// Standard Cartesian grid — perpendicular streets. + #[default] Grid, /// Organic placement with per-block offsets and rotations. Organic { @@ -172,7 +183,7 @@ pub enum DistrictLayoutMode { } /// Zoning classification for a block or floor zone. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub enum ZoningType { Commercial, Residential, @@ -181,11 +192,12 @@ pub enum ZoningType { Transit, Recreational, Restricted, + #[default] Mixed, } /// Reservation function — what purpose a multi-block reservation serves. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum ReservationFunction { Skyscraper, Park, @@ -217,7 +229,7 @@ pub enum ZoneAccessTier { } /// Vertical corridor type (how floors connect in a multi-level reservation). -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum VerticalCorridorType { Stairwell, Elevator, @@ -286,7 +298,7 @@ pub enum WallBackside { } /// Era cause — why a block has the era tag it has. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum EraCause { Original, CorporateMerger, @@ -338,7 +350,7 @@ pub enum PoliticalArchetype { /// Primary spatial axis of a city's original street grid. /// Derived from the matched attractor type (D-211). Controls district grid rotation. /// Source: D-213 -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum FoundingOrientation { /// Street grid perpendicular to coastline. `facing_degrees`: compass bearing toward water (0–359). Coastal { facing_degrees: u16 }, @@ -534,7 +546,7 @@ impl Default for FloorHeightProfile { /// Floor/basement extent for a building, bridging D-110 floor-index addressing /// and D-227 physical voxel-z (D-229, resolves Q-104). -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct FloorExtent { /// Index of the bottom floor (negative = basement, per D-110). pub base_floor: i8, @@ -682,7 +694,7 @@ pub struct ArchitectureFlavorRef { /// /// Written once inside `GenerateSkeleton`; read-only thereafter by FillChunk /// (D-230), the guarantee audit (D-097), and the Phase-6 interior generator (D-231). -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct BuildingPropertyTag { /// What the building is — matches a D-142 RON zone-type `id`. pub zone_type_id: ZoneTypeId, @@ -744,7 +756,7 @@ pub enum DoorInitialState { } /// Door credential requirement (D-231). -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum DoorCredential { None, /// Door is only accessible during specified hours (0–23, inclusive range). @@ -769,7 +781,7 @@ pub enum DoorCredential { } /// What a door connects to on the other side (D-231). -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum DoorConnectsTo { /// Opens onto a named street. Street { street_id: String }, @@ -783,7 +795,7 @@ pub enum DoorConnectsTo { /// /// A future interior generator produces a deterministic floor plan from this /// descriptor + `SeedChain` with **no other system queried**. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct InteriorDescriptor { pub zone_type_id: ZoneTypeId, pub entry_class: BuildingEntryClass, @@ -805,7 +817,7 @@ pub enum LayoutMode { } /// A single door on a building — the step3→step4 boundary (D-231). -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct DoorSpec { /// Which face of the building footprint this door is on. pub facing: CardinalDirection, @@ -897,8 +909,12 @@ pub enum ProductionUbiquity { /// Output of `GenerateSkeleton` extended to include building-property tags. /// Stored in `BodyWorldState.districts: BTreeMap`. /// `BTreeMap` for D-010 determinism. -#[derive(Serialize, Deserialize, Clone, Debug, Default)] +/// +/// D-230: `{ skeleton: DistrictSkeleton, block_tags: BTreeMap<(u8,u8), Vec> }` +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] pub struct DistrictWorldState { + /// Phase 1 skeleton produced by `GenerateSkeleton` (D-230). + pub skeleton: DistrictSkeleton, /// Building property tags keyed by block position (row, col) within the 4×4 grid. /// Each entry is a Vec of one tag per building footprint placed in that block. /// `BTreeMap` for D-010 determinism (no HashMap non-determinism). @@ -912,7 +928,7 @@ pub struct DistrictWorldState { /// /// Amended by D-229/D-232/D-233: adds `morphology_zone`, `trait_selection`, /// `dominant_bulk_class`, `dominant_production_ubiquity`. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct CityGenerationContext { /// Foreign key into atlas_city_names.id pub city_id: u64, @@ -945,7 +961,7 @@ pub struct CityGenerationContext { // --------------------------------------------------------------------------- /// Organic layout placement for a single block. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub struct BlockPlacement { /// Offset from grid-aligned position (±16 sim tiles per axis max). pub offset: (i16, i16), @@ -958,7 +974,7 @@ pub struct BlockPlacement { /// Single block within a district's 4×4 block grid. /// Each block = 128×128 sim tiles = 2×2 chunks. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] pub struct BlockSkeleton { /// Grid position (0–3, 0–3). pub position: (u8, u8), @@ -981,7 +997,7 @@ pub struct BlockSkeleton { /// **D-110:** `z_level` is `i8` — negative values represent basements /// (e.g. `z_level = -1` for a sub-basement). This differs from `z_levels: u8` /// on the parent reservation which counts total floors (always ≥ 1). -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct FloorZone { /// Absolute z-level of this floor. Negative for sub-ground levels (D-110). pub z_level: i8, @@ -991,7 +1007,7 @@ pub struct FloorZone { } /// Vertical connection spec within a multi-level reservation. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct VerticalCorridorSpec { /// Which blocks this vertical corridor passes through. pub block_coords: Vec<(u8, u8)>, @@ -1003,14 +1019,14 @@ pub struct VerticalCorridorSpec { } /// Visual palette for a zone — base material + contextual modifiers. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] pub struct ZonePalette { pub base: BasePalette, pub modifiers: Vec, } /// Contextual modifier applied on top of a base palette. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum PaletteModifier { EconomicFunction(EconomicModifier), Era(Era), @@ -1020,7 +1036,7 @@ pub enum PaletteModifier { } /// Social site placement within a district. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct SocialSitePlacement { pub site_id: SocialSiteId, /// Which blocks this site spans. @@ -1033,7 +1049,7 @@ pub struct SocialSitePlacement { } /// Triangle assignment within a social site. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct TriangleAssignment { pub template: TriangleTemplate, pub purposes: Vec, @@ -1086,7 +1102,7 @@ pub struct StructuralChange { /// `base_z: -3, z_levels: 24` /// A deep mine shaft descending 30 floors below ground: /// `base_z: -30, z_levels: 30` -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct MultiBlockReservation { /// Which blocks (grid positions) are part of this reservation. pub blocks: Vec<(u8, u8)>, @@ -1128,7 +1144,7 @@ pub struct MultiBlockReservation { /// /// All `Vec<_>` fields are stable-ordered at generation time (sorted by a /// deterministic key). Generation reproduces identical output for the same seed. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] pub struct DistrictSkeleton { // ── Identity ────────────────────────────────────────── pub district_id: DistrictId, @@ -1178,6 +1194,106 @@ pub struct DistrictSkeleton { mod tests { use super::*; + // ── FloorExtent: floor_at_voxel_z and voxel_range_for_floor ───────────── + + /// (a) Uniform building at ground level — round-trip voxel↔floor. + /// + /// Uniform(3), base_floor=0, floor_count=2 → floors 0,1; heights 3 each. + /// Floor 0 occupies voxels 0–2; floor 1 occupies voxels 3–5. + #[test] + fn floor_extent_uniform_ground_round_trip() { + let extent = FloorExtent { + base_floor: 0, + floor_count: 2, + heights: FloorHeightProfile::Uniform(3), + }; + // Range for each floor. + assert_eq!(extent.voxel_range_for_floor(0), Some((0, 2))); + assert_eq!(extent.voxel_range_for_floor(1), Some((3, 5))); + // Round-trip: voxel → floor → range contains voxel. + assert_eq!(extent.floor_at_voxel_z(0), Some(0)); + assert_eq!(extent.floor_at_voxel_z(2), Some(0)); + assert_eq!(extent.floor_at_voxel_z(3), Some(1)); + assert_eq!(extent.floor_at_voxel_z(5), Some(1)); + } + + /// (b) Basement building — base_floor=-1, floor_count=3 (floors -1, 0, 1). + /// + /// Uniform(3): floor -1 → voxels 0–2, floor 0 → 3–5, floor 1 → 6–8. + #[test] + fn floor_extent_basement_building() { + let extent = FloorExtent { + base_floor: -1, + floor_count: 3, + heights: FloorHeightProfile::Uniform(3), + }; + assert_eq!(extent.voxel_range_for_floor(-1), Some((0, 2))); + assert_eq!(extent.voxel_range_for_floor(0), Some((3, 5))); + assert_eq!(extent.voxel_range_for_floor(1), Some((6, 8))); + assert_eq!(extent.floor_at_voxel_z(0), Some(-1)); + assert_eq!(extent.floor_at_voxel_z(3), Some(0)); + assert_eq!(extent.floor_at_voxel_z(6), Some(1)); + assert_eq!(extent.floor_at_voxel_z(8), Some(1)); + } + + /// (c) Variable([5,3,3]) floor heights — confirm each floor's voxel range. + /// + /// base_floor=0, floor_count=3: floor 0 → 5 voxels (0–4), floor 1 → 3 voxels (5–7), + /// floor 2 → 3 voxels (8–10). + #[test] + fn floor_extent_variable_heights() { + let extent = FloorExtent { + base_floor: 0, + floor_count: 3, + heights: FloorHeightProfile::Variable(vec![5, 3, 3]), + }; + assert_eq!(extent.voxel_range_for_floor(0), Some((0, 4))); + assert_eq!(extent.voxel_range_for_floor(1), Some((5, 7))); + assert_eq!(extent.voxel_range_for_floor(2), Some((8, 10))); + assert_eq!(extent.floor_at_voxel_z(0), Some(0)); + assert_eq!(extent.floor_at_voxel_z(4), Some(0)); + assert_eq!(extent.floor_at_voxel_z(5), Some(1)); + assert_eq!(extent.floor_at_voxel_z(7), Some(1)); + assert_eq!(extent.floor_at_voxel_z(8), Some(2)); + assert_eq!(extent.floor_at_voxel_z(10), Some(2)); + } + + /// (d) Boundary — one voxel above the top floor returns None; one below the base returns None. + /// + /// Uniform(3), base_floor=0, floor_count=2: valid range [0, 5]. + /// voxel_z=6 is one above the top; voxel_z=-1 is one below the base. + #[test] + fn floor_extent_boundary_returns_none() { + let extent = FloorExtent { + base_floor: 0, + floor_count: 2, + heights: FloorHeightProfile::Uniform(3), + }; + // One voxel above the top floor (top floor ends at voxel 5). + assert_eq!( + extent.floor_at_voxel_z(6), + None, + "one voxel above top floor must return None" + ); + // One voxel below the base floor (base maps to voxel 0). + assert_eq!( + extent.floor_at_voxel_z(-1), + None, + "one voxel below base floor must return None" + ); + // Floor index outside extent also returns None from voxel_range_for_floor. + assert_eq!( + extent.voxel_range_for_floor(2), + None, + "floor index beyond floor_count must return None" + ); + assert_eq!( + extent.voxel_range_for_floor(-1), + None, + "floor index below base_floor must return None" + ); + } + /// D-110: FloorZone.z_level must be i8 (signed) to support negative sub-levels. #[test] fn floor_zone_z_level_is_signed() {