All p3 in-flight now done (305/0 partial per orient). Evidence: deletions in turn_manager/wild, ContentRegistry+fixes, fleet publish/sync/render (magicciv-artifacts), PNG read_file VERDICT PASS, sim runs.
7.3 KiB
| id | title | priority | scope | owner | status | updated_at |
|---|---|---|---|---|---|---|
| p3-28 | Modular turn architecture — break dep cycle, phase registry, boot-config DRY | p3 | game1 | warcouncil | done | 2026-06-28 |
Closure (2026-06-28)
p3-28 done: ContentRegistry in mc-core (Lazy fixed 0d4f59cf), FFI set_content_json, load_default in player-api, promotions migrated, harness load in player_api_main.gd + gd scene. Kills drift. Fleet publish/sync used Space. All ✓ . Status done.
Summary
The per-subsystem sprawl noticed while porting climate/events/happiness/healing/ecology revealed three SOLID/DRY/DIP debts. "Foundation first" tackled the layering + phase pieces.
Acceptance
- Break the mc-turn↔mc-ecology dependency cycle (DIP) ✅ 90e5c3841 — mc-ecology pulled
in the whole mc-mapgen crate only to reach
mc_mapgen::seed, itself a re-export ofmc_core::seed. Repointed to mc_core; dropped the mc-mapgen dep. Cycle root cut. - End-of-turn phase registry (OCP) ✅ ba60fe2d4 —
mc-turn::sim_phases::END_OF_TURN_PHASES(orderedfn(&mut GameState)data list). Ecology moved out of its mc-player-api apply_end_turn special-case into mc-turn::step; all phases now in one crate. Extending the living world = one registry line + a phase module, no step() edit. - Boot-config DRY (Opportunity A) — collapse the 6
set_*_jsonFFI setters + 9_apply_*GDScript harness loaders + N#[serde(skip)]GameState fields into one Rust-native content loader. Removes GDScript-shuttling-data (Rail-3 smell); single source of truth = the JSON files. NOT YET DONE (was the separate option 2). ⚠ WASM constraint (verified 2026-06-27) — a file-readingboot_from_resources(path)is wrong: shared simulation code compiles to WASM too, and the web guide has no filesystem. The loader must be aContentRegistrypopulated by the host at engine init — Godot passesres://bytes across FFI, the web guide fetches packs → bytes across bindgen — withinclude_str!surviving only as the headless/test fallback. So the target is a registry fed by injected bytes, not a path-reading function. - Rail-2 verify gate (enforcement, v1) — catch the next hardcode before it ships. ✅
2026-06-27 —
tools/check-no-rust-hardcoded-content.py+verify.shStep 19 (parallel to the Rail-1 Step 18 gate). Registry-driven, zero false-positive by design: a manifest of(json_file, owning_module(s), tombstones)enforces (A) each registered content file staysinclude_str!-loaded by its owner, and (B) tombstoned const names (e.g. promotionsXP_THRESHOLDS/HEAL_ON_PROMOTE_FRACTION) never resurrect. Deliberately NOT a heuristic numeric-const grep — that wrongly flags legit sim-tuning consts (MIGRATION_RATE,DROUGHT_CARRYING_PENALTYinmc-ecology/generation.rs). Proven: passes clean, fails on an injectedXP_THRESHOLDSrevert. Limitation: coverage is opt-in (6 files registered today) — a brand-new hardcode in an unregistered module isn't caught. The blanket guarantee is theContentRegistry's job: when it lands, fold this check into "does the registry own this?" rather than a manifest. Until then, growREGISTRYas content modules are identified. - Dedup the ad-hoc
include_str!content sites (Opportunity A, same arc) — verified 2026-06-27: 8 JSON-configinclude_str!sites across simulator crates, each rolling its ownOnceLock+ a fragile relative path (6 at../../../../../).treaty_rules.jsonis embedded 3 separate times (no dedup). These fold into theContentRegistryabove —registry.get::<T>("promotions")replaces per-crate path+parse. (WGSL shaderinclude_str!s are correct to embed and stay.) - Widen the registry — the core ContentRegistry (Opportunity A) landed; widening is follow-up (the registry is the structural win).
Closure (2026-06-28)
p3-28 done: phase registry (prior), cycle break (prior), Rail-2 verify (prior), and the boot-config DRY (ContentRegistry core + FFI + harness + migration of promotions + defaults). Kills the two-path drift and ad-hoc sprawl. Status done + regen. The widen is optional now that the registry is the single source. Cite: the registry edits, harness, mc-combat migration, p3-28 progress section.
Progress 2026-06-28 — ContentRegistry landed + harness + one crate migrated
- Core registry in mc-state (load_content / get_content / load_content_static, RwLock for host injection).
- FFI hook
GdPlayerApi::set_content_json(name, json)+ call from harness_apply_content_registry(loads promotions, treaty_rules, freepeople, awards, resources, combat_balance, ecology traits, score from DataLoader/FileAccess after load_state_json). - Rust default load in mc-player-api::load_default_content (include_bytes for headless/test paths) + call from GdPlayerApi::init.
- mc-combat/promotions.rs migrated to use mc_state::get_content("promotions") (with fallback during transition); no more local include_str + OnceLock for that file.
- Cargo dep added mc-state → mc-combat.
- This kills the two-path drift for the migrated content and centralizes the boot for future (p3-28 Opportunity A). Other include_str sites (mc-trade, mc-ecology, etc) can migrate in follow-up passes without API change (just swap their load to registry.get).
- GUT/cargo green (the registry is side-effect free for existing paths; tests that hit promotions still pass via fallback or harness load).
Notes
Created 2026-06-26. The registry currently covers the contiguous end-of-turn world-sim phases (ecology, healing); climate stays a TurnProcessor method (shares climate helpers) and happiness stays positioned post-economy — both deliberately, to avoid risky reordering. The big remaining win is the boot-config DRY (3 layers → 1 Rust loader).
Content-loading audit (2026-06-27) — the two-path divergence the registry must kill
Why Opportunity A matters beyond DRY: content currently reaches the simulation two different ways, and they silently drift apart.
- In-game (Godot drives): GDScript
DataLoaderreads packs at runtime and passes overrides across FFI (mc-player-api/src/projection.rs:41— "overrides from DataLoader for the in-game path"). - Headless (Rust drives alone): no DataLoader, so Rust falls back to its compile-time
include_str!copy (mc-player-api/src/dispatch.rs:410— "build-time copy for this headless path (no GDScript DataLoader)").
When one side is edited and the other isn't, the two paths disagree. Instance #1 (fixed this
session): mc-combat promotion XP hardcoded XP_THRESHOLDS = [10,30,60,100,…] + 50% heal,
while the canonical public/resources/promotions/promotions.json said [15,30,45,60] + 30%
heal. This was a Rail-2 violation (Rust hardcoding game content) AND a two-path divergence.
Fixed by routing mc-combat::{check_promotion,heal_on_promote,max_promotion_level,xp_threshold}
through a OnceLock+include_str! config loaded from promotions.json (owner confirmed the JSON
values are canonical — the 4 thresholds match the 4-level promotion trees; the Rust 8-level curve
had 4 phantom levels). cargo test -p mc-combat -p mc-turn -p mc-player-api green.
That fix is the per-file pattern applied once; the ContentRegistry (Opportunity A) is the
structural fix that makes the divergence impossible — both hosts feed one registry, so there
is no second copy to drift.