"""Load authored world content and cross-check origin references. Language-neutral content integrity: every id an origin references (start location, start quest, granted items, seeded npc dispositions) must resolve in world content. New-game construction (client-side, Plan B) relies on this holding; catching it here fails a broken origin at authoring time, not three scenes into play. """ import json import os from pathlib import Path def _load_ids(dir_path: Path) -> set[str]: ids: set[str] = set() for f in dir_path.glob("*.json"): with open(f) as fh: ids.add(json.load(fh)["id"]) return ids def load_world(content_root: Path) -> dict[str, set[str]]: world = content_root / "world" return { "locations": _load_ids(world / "locations"), "npcs": _load_ids(world / "npcs"), "quests": _load_ids(world / "quests"), "items": _load_ids(world / "items"), } def load_origin(path: Path) -> dict: with open(path) as f: return json.load(f) def unresolved_refs(origin: dict, world: dict[str, set[str]]) -> list[str]: missing: list[str] = [] if origin["start_location_id"] not in world["locations"]: missing.append(f"location:{origin['start_location_id']}") quest = origin.get("start_quest_id") if quest is not None and quest not in world["quests"]: missing.append(f"quest:{quest}") for grant in origin["inventory_grants"]: if grant["item_id"] not in world["items"]: missing.append(f"item:{grant['item_id']}") for npc_id in origin["disposition_overrides"]: if npc_id not in world["npcs"]: missing.append(f"npc:{npc_id}") return missing def _content_root() -> Path: """Locate the authored content root. Honours CONTENT_ROOT, else walks up from this file for a dir containing world/ — repo-root/content in a local checkout, /app/content when bundled into the Docker image. Mirrors canon_log._schema_dir(). """ env = os.environ.get("CONTENT_ROOT") if env: return Path(env) here = Path(__file__).resolve() for parent in here.parents: candidate = parent / "content" if (candidate / "world").is_dir(): return candidate raise RuntimeError("content root not found") def load_npc(npc_id: str) -> dict | None: """Load one NPC's authored file (persona + knowledge + capabilities), or None if no file resolves. Server-only fields never travel to the client.""" path = _content_root() / "world" / "npcs" / f"{npc_id}.json" if not path.is_file(): return None with open(path) as f: return json.load(f)