diff --git a/nexus/bannerlord_harness.py b/nexus/bannerlord_harness.py index 6450b75b..90356de0 100644 --- a/nexus/bannerlord_harness.py +++ b/nexus/bannerlord_harness.py @@ -29,7 +29,7 @@ from typing import Any, Callable, Optional import websockets -from bannerlord_trace import BannerlordTraceLogger +from nexus.bannerlord_trace import BannerlordTraceLogger # ═══════════════════════════════════════════════════════════════════════════ # CONFIGURATION diff --git a/nexus/evennia_ws_bridge.py b/nexus/evennia_ws_bridge.py index c578cfae..99ce5fc7 100644 --- a/nexus/evennia_ws_bridge.py +++ b/nexus/evennia_ws_bridge.py @@ -181,6 +181,63 @@ async def live_bridge(log_dir: str, ws_url: str, reconnect_delay: float = 5.0): await asyncio.gather(*tasks) + + +def clean_lines(text: str) -> list[str]: + """Strip ANSI, normalize line endings, return non-empty lines.""" + text = strip_ansi(text).replace("\r", "") + return [line.strip() for line in text.split("\n") if line.strip()] + + +def parse_room_output(text: str) -> dict: + """Parse Evennia room text into structured data (title, desc, exits, objects).""" + lines = clean_lines(text) + if len(lines) < 2: + return {"title": lines[0] if lines else "", "desc": "", "exits": [], "objects": []} + title = lines[0] + desc = lines[1] + exits = [] + objects = [] + for line in lines[2:]: + if line.startswith("Exits:"): + raw = line.split(":", 1)[1].strip().replace(" and ", ", ") + exits = [{"key": t.strip(), "destination_id": t.strip().title(), "destination_key": t.strip().title()} for t in raw.split(",") if t.strip()] + elif line.startswith("You see:"): + raw = line.split(":", 1)[1].strip().replace(" and ", ", ") + parts = [t.strip() for t in raw.split(",") if t.strip()] + objects = [{"id": p.removeprefix("a ").removeprefix("an "), "key": p.removeprefix("a ").removeprefix("an "), "short_desc": p} for p in parts] + return {"title": title, "desc": desc, "exits": exits, "objects": objects} + + +def normalize_event(raw: dict, hermes_session_id: str) -> list[dict]: + """Convert raw Evennia event dict into normalized Nexus events.""" + from nexus.evennia_event_adapter import ( + actor_located, command_issued, command_result, + room_snapshot, session_bound, + ) + out = [] + event = raw.get("event") + actor = raw.get("actor", "Timmy") + timestamp = raw.get("timestamp") + if event == "connect": + out.append(session_bound(hermes_session_id, evennia_account=actor, evennia_character=actor, timestamp=timestamp)) + parsed = parse_room_output(raw.get("output", "")) + if parsed: + out.append(actor_located(actor, parsed["title"], parsed["title"], timestamp=timestamp)) + out.append(room_snapshot(parsed["title"], parsed["title"], parsed["desc"], exits=parsed["exits"], objects=parsed["objects"], timestamp=timestamp)) + elif event == "command": + cmd = raw.get("command", "") + output = raw.get("output", "") + out.append(command_issued(hermes_session_id, actor, cmd, timestamp=timestamp)) + success = not output.startswith("Command '") and not output.startswith("Could not find") + out.append(command_result(hermes_session_id, actor, cmd, strip_ansi(output), success=success, timestamp=timestamp)) + parsed = parse_room_output(output) + if parsed: + out.append(actor_located(actor, parsed["title"], parsed["title"], timestamp=timestamp)) + out.append(room_snapshot(parsed["title"], parsed["title"], parsed["desc"], exits=parsed["exits"], objects=parsed["objects"], timestamp=timestamp)) + return out + + async def playback(log_path: Path, ws_url: str): """Legacy mode: replay a telemetry JSONL file.""" from nexus.evennia_event_adapter import (