79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""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)
|