refactor(mc-combat): read promotions.json via ContentRegistry; fold Rail-2 gate (p3-28)
Both promotion_config() and build_registry() now pull raw bytes from content::get(Promotions) instead of a crate-local include_str! const. The embedded fallback moved to the central ContentRegistry, so the Rail-2 gate gains a registry_owned flag: Check A verifies mc-core/src/content.rs embeds the JSON, while the XP_THRESHOLDS/HEAL_ON_PROMOTE_FRACTION tombstones still guard mc-combat. Gate passes; mc-combat/mc-turn/mc-player-api tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1c256e7db4
commit
69f80189c6
2 changed files with 64 additions and 20 deletions
|
|
@ -2,9 +2,6 @@ use serde::{Deserialize, Serialize};
|
|||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const PROMOTIONS_JSON: &str =
|
||||
include_str!("../../../../../public/resources/promotions/promotions.json");
|
||||
|
||||
/// Promotion tuning loaded from the canonical content store
|
||||
/// (`public/resources/promotions/promotions.json`). Rail-2: neither Rust nor
|
||||
/// GDScript hardcodes game content — the XP thresholds and heal-on-promote
|
||||
|
|
@ -18,14 +15,16 @@ struct PromotionConfig {
|
|||
heal_on_promote_percent: f32,
|
||||
}
|
||||
|
||||
/// Load + cache the promotion tuning. The JSON is compiled in via `include_str!`
|
||||
/// (WASM- and GDExtension-safe — no filesystem access at runtime) and parsed
|
||||
/// once into a process-wide `OnceLock`, mirroring `mc_comms::config`.
|
||||
/// Load + cache the promotion tuning. The raw JSON comes from the host-fed
|
||||
/// [`mc_core::content`] registry (embedded `include_str!` fallback when no host
|
||||
/// override — WASM- and GDExtension-safe, no runtime filesystem access) and is
|
||||
/// parsed once into a process-wide `OnceLock`.
|
||||
fn promotion_config() -> &'static PromotionConfig {
|
||||
static CELL: OnceLock<PromotionConfig> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
let json = mc_core::content::get(mc_core::content::ContentKey::Promotions);
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(PROMOTIONS_JSON).expect("promotions.json must parse as valid JSON");
|
||||
serde_json::from_str(&json).expect("promotions.json must parse as valid JSON");
|
||||
let xp_thresholds = value
|
||||
.get("xp_thresholds")
|
||||
.and_then(|v| serde_json::from_value::<Vec<i32>>(v.clone()).ok())
|
||||
|
|
@ -259,8 +258,9 @@ fn promotion_registry() -> &'static HashMap<String, PromotionDef> {
|
|||
}
|
||||
|
||||
fn build_registry() -> HashMap<String, PromotionDef> {
|
||||
let json = mc_core::content::get(mc_core::content::ContentKey::Promotions);
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(PROMOTIONS_JSON).expect("promotions.json must parse as valid JSON");
|
||||
serde_json::from_str(&json).expect("promotions.json must parse as valid JSON");
|
||||
let trees = value
|
||||
.get("trees")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ from pathlib import Path
|
|||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# The central host-fed content registry (p3-28). When a content pack's embedded
|
||||
# `include_str!` fallback is centralized here, Check A is satisfied by THIS file
|
||||
# carrying the include_str! rather than each consumer module — the consumers now
|
||||
# read the bytes via `mc_core::content::get(...)`. Tombstones still apply to the
|
||||
# original consumer modules (a hardcode resurrection is caught wherever it lands).
|
||||
CONTENT_REGISTRY_MODULE = "src/simulator/crates/mc-core/src/content.rs"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ContentEntry:
|
||||
"""One canonical JSON content file and the Rust module(s) that load it."""
|
||||
|
|
@ -55,6 +63,10 @@ class ContentEntry:
|
|||
# const/static names that PREVIOUSLY hardcoded this content and were removed;
|
||||
# the gate fails if any reappears in an owning module (a divergence regression).
|
||||
tombstones: tuple[str, ...] = field(default=())
|
||||
# True once the embedded include_str! fallback for this pack has moved into the
|
||||
# central ContentRegistry (CONTENT_REGISTRY_MODULE). Check A then verifies the
|
||||
# registry loads the JSON; `modules` are kept only for the tombstone scan.
|
||||
registry_owned: bool = field(default=False)
|
||||
|
||||
|
||||
# The registry. Each entry was verified (file:line) at authoring time against the
|
||||
|
|
@ -66,6 +78,9 @@ REGISTRY: tuple[ContentEntry, ...] = (
|
|||
# Removed 2026-06-27 when promotion tuning moved to promotions.json
|
||||
# (the divergence that motivated this gate). Must not come back.
|
||||
tombstones=("XP_THRESHOLDS", "HEAL_ON_PROMOTE_FRACTION"),
|
||||
# p3-28: embedded fallback now lives in the central ContentRegistry;
|
||||
# mc-combat reads it via mc_core::content::get(Promotions).
|
||||
registry_owned=True,
|
||||
),
|
||||
ContentEntry(
|
||||
json="public/resources/diplomacy/treaty_rules.json",
|
||||
|
|
@ -121,24 +136,53 @@ def main() -> int:
|
|||
)
|
||||
continue
|
||||
|
||||
# Check A — the canonical JSON must be include_str!-loaded somewhere the
|
||||
# headless/WASM path reaches. For registry-owned packs that is the central
|
||||
# ContentRegistry; otherwise it is each owning consumer module.
|
||||
if entry.registry_owned:
|
||||
reg_path = REPO_ROOT / CONTENT_REGISTRY_MODULE
|
||||
reg_text = (
|
||||
reg_path.read_text(encoding="utf-8", errors="replace")
|
||||
if reg_path.is_file()
|
||||
else ""
|
||||
)
|
||||
if "include_str!" not in reg_text or suffix not in reg_text:
|
||||
violations.append(
|
||||
f"[A] {CONTENT_REGISTRY_MODULE} no longer embeds {entry.json}\n"
|
||||
f" this pack is registry_owned — the central ContentRegistry must\n"
|
||||
f" carry an include_str!(... {suffix}) embedded fallback (Rail-2)."
|
||||
)
|
||||
else:
|
||||
for mod in entry.modules:
|
||||
mod_path = REPO_ROOT / mod
|
||||
if not mod_path.is_file():
|
||||
violations.append(
|
||||
f"[A] owning module not found: {mod}\n"
|
||||
f" registered as loader of {entry.json}"
|
||||
)
|
||||
continue
|
||||
text = mod_path.read_text(encoding="utf-8", errors="replace")
|
||||
if "include_str!" not in text or suffix not in text:
|
||||
violations.append(
|
||||
f"[A] {mod} no longer loads {entry.json}\n"
|
||||
f" expected an include_str!(... {suffix}) — content must be\n"
|
||||
f" LOADED from the canonical JSON, not hardcoded (Rail-2)."
|
||||
)
|
||||
|
||||
# Check B — no tombstoned hardcode may reappear in any consumer module.
|
||||
for mod in entry.modules:
|
||||
mod_path = REPO_ROOT / mod
|
||||
if not mod_path.is_file():
|
||||
violations.append(
|
||||
f"[A] owning module not found: {mod}\n"
|
||||
f" registered as loader of {entry.json}"
|
||||
)
|
||||
# Only matters for tombstone scanning; a missing non-registry
|
||||
# loader was already reported above.
|
||||
if entry.registry_owned and entry.tombstones:
|
||||
violations.append(
|
||||
f"[A] tombstone-owner module not found: {mod}\n"
|
||||
f" registered for tombstone scan of {entry.json}"
|
||||
)
|
||||
continue
|
||||
text = mod_path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
# Check A — the module must include_str! its registered content.
|
||||
if "include_str!" not in text or suffix not in text:
|
||||
violations.append(
|
||||
f"[A] {mod} no longer loads {entry.json}\n"
|
||||
f" expected an include_str!(... {suffix}) — content must be\n"
|
||||
f" LOADED from the canonical JSON, not hardcoded (Rail-2)."
|
||||
)
|
||||
|
||||
# Check B — no tombstoned hardcode may reappear.
|
||||
for name in entry.tombstones:
|
||||
rx = _const_decl(name)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue