From 8f57d63d37478bd0b9277754b1184b105cadcded Mon Sep 17 00:00:00 2001 From: Natalie Date: Tue, 12 May 2026 03:40:32 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects):=20=E2=9C=A8=20update=20buildin?= =?UTF-8?q?g=20entity=20port=20status=20to=20partial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../objectives/p2-72a-building-entity-port.md | 112 +++++++++++++++--- src/game/engine/src/entities/building.gd | 11 ++ 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/.project/objectives/p2-72a-building-entity-port.md b/.project/objectives/p2-72a-building-entity-port.md index 933b7cee..0d8001b8 100644 --- a/.project/objectives/p2-72a-building-entity-port.md +++ b/.project/objectives/p2-72a-building-entity-port.md @@ -2,15 +2,38 @@ id: p2-72a-building-entity-port title: "Port NPC Building entity (lairs/villages/ruins) into Rust" priority: p2 -status: open +status: partial scope: game1 category: architecture owner: simulator-infra created: 2026-05-11 -updated_at: 2026-05-11 +updated_at: 2026-05-12 blocks: [p2-72a-save-format-migration, p2-72a, p2-72] --- +## STATUS — 2026-05-12 + +**Stage 2b (Rust surface) landed.** `BuildingEntity` lives in +`mc_core::building`, `GameState.npc_buildings: Vec` is +the canonical mirror, and `GdGameState` exposes the full accessor surface +(count / dict / all / spawn / remove / set-visited / convert-type / +clear). Round-trip serde test green; `cargo check -p +magic-civ-physics-gdext` clean. + +**Stage 2b acceptance dropped to `partial`, not `done`.** The +GDScript→Rust spawn-path wiring (per § E) is deferred to Stage 4 +(`p2-72a-gdgamestate-canonical-render-source`). Reason: no singleton +`GdGameState` autoload exists today — every current GDScript caller +instantiates a fresh `GdGameState` per op, so dual-writing spawns into +those ephemeral instances would not flow through to any persistent +mirror. The Rust spawn API is in place and ready for Stage 4 to wire, +which is the legitimate place for the singleton to land. + +`Building.gd` is annotated with a Stage-4 marker comment but otherwise +unchanged. Read-paths (renderer, AI, encyclopedia, fauna, climate-event +handlers) continue to consult `GameState.npc_buildings` (GDScript array) +unchanged. + ## STATUS — 2026-05-11 Filed as prerequisite for `p2-72a-save-format-migration` after Wall 3 of @@ -165,21 +188,78 @@ fn building_entity_round_trip_is_byte_identical() { ## Acceptance -- ☐ `BuildingEntity` Rust struct lives in `mc-core::building` (or - `mc-buildings` if Wave 1 picks crate split). -- ☐ Serialize/Deserialize round-trip unit test passes. -- ☐ `api-gdext` exposes the accessor surface in § C. -- ☐ Every Game 1 spawn path routes through `GdGameState::spawn_npc_building`. -- ☐ Every Game 1 read path routes through `GdGameState::npc_building_at` / `npc_buildings_all` / `set_npc_building_visited` / `remove_npc_building`. -- ☐ `Building.gd` deleted (option b) or thinned to a view-only wrapper (option c) — Wave 1 picks. -- ☐ `GameState.npc_buildings` and `GameState._npc_buildings_by_tile` deleted from `game_state.gd`. Spatial index lives in Rust only. -- ☐ Hex renderer / unit movement / AI tactical / city borders / encyclopedia / siege flow all continue to work; visual + GUT verification. -- ☐ `cargo check --workspace` green. -- ☐ `cargo test --workspace` green (modulo known pre-existing - `solo_dominion` errors). -- ☐ GUT headless run green. +- ✓ `BuildingEntity` Rust struct lives in `mc-core::building` (chose + option ii — extend `mc-core` rather than new crate; see Stage 2b + decision log below). +- ✓ Serialize/Deserialize round-trip unit test passes + (`cargo test -p mc-core building::` — 3 tests green: + `entity_round_trip_is_byte_identical`, + `entity_position_serializes_as_pair`, + `entity_round_trip_city_placement_defaults`). +- ✓ `api-gdext` exposes the accessor surface in § C + (`npc_building_count`, `npc_building_dict(idx)`, `npc_buildings_all`, + `spawn_npc_building`, `remove_npc_building_by_id`, + `set_npc_building_visited`, `convert_npc_building_type`, + `clear_npc_buildings` — see + `src/simulator/api-gdext/src/lib.rs` Stage 2b block). +- ☐ Every Game 1 spawn path routes through `GdGameState::spawn_npc_building` + — **deferred to Stage 4** (no singleton; dual-writes into + per-call `GdGameState` instances would be ephemeral). +- ☐ Every Game 1 read path routes through `GdGameState::npc_building_at` + / `npc_buildings_all` / `set_npc_building_visited` / + `remove_npc_building` — **Stage 3+ (save-format migration) / + Stage 4 (canonical-render-source) scope, not Stage 2b.** +- ☐ `Building.gd` deleted (option b) or thinned to a view-only wrapper + (option c) — **Stage 4 scope**. Stage 2b adds a marker comment + flagging the upcoming Stage 4 conversion. +- ☐ `GameState.npc_buildings` and `GameState._npc_buildings_by_tile` + deleted from `game_state.gd`. Spatial index lives in Rust only. + **Stage 4 scope.** +- ☐ Hex renderer / unit movement / AI tactical / city borders / + encyclopedia / siege flow all continue to work; visual + GUT + verification — Stage 2b doesn't touch read paths, so existing GDScript + flow is unaltered; Stage 4 acceptance. +- ✓ `cargo check --workspace` green (only pre-existing + `unsafe_op_in_unsafe_fn` / `missing-docs` warnings). +- ✓ `cargo test -p mc-core -p mc-turn` green + (mc-core: 243/243 + 3 golden; mc-turn: see Stage 2b verification log). + Pre-existing failures: `mc-ai` test crates (`gpu_walltime`, + `gpu_tree_integration`, `mcts_basic`, `clan_rollout_divergence`, + `ultimate_lookahead_stress`, `budget_enforcement` — all + `AbstractPlayerState._pad_fr/_pad_rel` missing-field errors); these + reproduce on the unmodified pre-Stage-2b tree (verified via + `git stash && cargo check --workspace --tests`). +- ☐ GUT headless run green — Stage 2b changes only Rust + a doc + comment in `building.gd`; GUT unchanged. Re-run at Stage 4 close. - ☐ `p2-72a-save-format-migration` `blocked_by:` updated to remove this - objective once it lands. + objective once it lands — **partial unblock**: Stage 3 can begin + consuming the `BuildingEntity` type and `GameState.npc_buildings` + field, but full unblock awaits Stage 4 wiring. + +## Stage 2b decision log — 2026-05-12 + +**Crate decision: option (ii) — extend `mc-core`.** Rationale: +`mc_core::lair` already lives in the same crate and owns adjacent +per-tile siege state; `BuildingEntity` is pure data + queries with no +turn-tick lifecycle of its own; no new crate-level surface to justify. +Spec § A explicitly prefers (ii). + +**Position serde shape: tuple `(i32, i32)` round-tripped as a JSON +array `[col, row]`** via a small `axial_pos_as_pair` adapter. Matches +`Building.gd::to_dict`'s `"position": [position.x, position.y]` exactly +so Stage 3 save-format migration gets byte-identity with existing +GDScript-emitted saves. + +**Placement serde shape: lowercase string `"map"` / `"city"`** +(`#[serde(rename_all = "lowercase")]`). Matches `Building.gd::from_dict` +shape. + +**`name` kept on the entity (not derived from data).** Spec § B +suggested dropping it, but two production sites +(`ecological_event_handlers_b.gd::_abandon_lair`, `fauna.gd::_abandon_lair`) +mutate `b.name = "Ruin"` alongside `b.type_id = "ruin"` mid-game, so the +field is mutable state, not a pure data-lookup. `convert_npc_building_type` +takes both `type_id` and `name` for parity. ## Out of scope diff --git a/src/game/engine/src/entities/building.gd b/src/game/engine/src/entities/building.gd index e33b490d..a17dead0 100644 --- a/src/game/engine/src/entities/building.gd +++ b/src/game/engine/src/entities/building.gd @@ -3,6 +3,17 @@ extends RefCounted ## Game entity for both city buildings and map-placed NPC buildings (lairs, villages, ruins). ## City buildings: placement="city", owner=player_index, position=city tile. ## NPC buildings: placement="map", owner=-1, position=map tile. +## +## p2-72a Stage 2b: The Rust source-of-truth for this entity now lives in +## `mc_core::building::BuildingEntity` and the canonical `npc_buildings` +## mirror sits on `mc_turn::GameState.npc_buildings`. This GDScript class +## stays unchanged for Stage 2b — no singleton `GdGameState` bridge exists +## yet, so dual-writing into per-call `GdGameState` instances would be +## ephemeral. Stage 4 (`p2-72a-gdgamestate-canonical-render-source`) +## introduces the singleton, at which point this script collapses to a +## thin view over `GdGameState::npc_building_dict(idx)` and the spawn +## paths in `village_lair_placer.gd` / `ecological_event_handlers_b.gd` +## start routing through `spawn_npc_building` / `convert_npc_building_type`. ## Unique instance ID (e.g. "lair_42", "building_granary_3") var id: String = ""