Compare commits
1 Commits
feat/porta
...
fix/1509
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63b2d444d5 |
27
app.js
27
app.js
@@ -1534,27 +1534,6 @@ function createPortals(data) {
|
||||
});
|
||||
}
|
||||
|
||||
async function reloadPortals() {
|
||||
// Remove existing portals from scene
|
||||
portals.forEach(portal => {
|
||||
scene.remove(portal.group);
|
||||
});
|
||||
portals.length = 0;
|
||||
activePortal = null;
|
||||
|
||||
// Re-fetch and recreate
|
||||
try {
|
||||
const response = await fetch('./portals.json?' + Date.now()); // cache-bust
|
||||
const portalData = await response.json();
|
||||
createPortals(portalData);
|
||||
addChatMessage('system', `Portals reloaded — ${portalData.length} worlds online.`);
|
||||
console.log(`[portals] Reloaded ${portalData.length} portals`);
|
||||
} catch (e) {
|
||||
console.error('[portals] Reload failed:', e);
|
||||
addChatMessage('error', 'Portal reload failed. Check portals.json.');
|
||||
}
|
||||
}
|
||||
|
||||
function createPortal(config) {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(config.position.x, config.position.y, config.position.z);
|
||||
@@ -2244,12 +2223,6 @@ function connectHermes() {
|
||||
const data = JSON.parse(evt.data);
|
||||
handleHermesMessage(data);
|
||||
|
||||
// Portal hot-reload: server detected portals.json change
|
||||
if (data.type === 'portals:reload') {
|
||||
console.log('[portals] Hot-reload triggered');
|
||||
reloadPortals();
|
||||
}
|
||||
|
||||
// Store in MemPalace
|
||||
if (data.type === 'chat') {
|
||||
// Store in MemPalace with AAAK compression
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import Any, Callable, Optional
|
||||
|
||||
import websockets
|
||||
|
||||
from bannerlord_trace import BannerlordTraceLogger
|
||||
from nexus.bannerlord_trace import BannerlordTraceLogger
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# CONFIGURATION
|
||||
|
||||
@@ -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 (
|
||||
|
||||
45
server.py
45
server.py
@@ -7,10 +7,8 @@ the body (Evennia/Morrowind), and the visualization surface.
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
# Branch protected file - see POLICY.md
|
||||
@@ -81,42 +79,6 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
|
||||
clients.discard(websocket)
|
||||
logger.info(f"Client disconnected {addr}. Total clients: {len(clients)}")
|
||||
|
||||
|
||||
# Portal hot-reload state
|
||||
_portals_mtime: float = 0.0
|
||||
_portals_path: Path = Path(__file__).parent / "portals.json"
|
||||
|
||||
async def _watch_portals():
|
||||
"""Watch portals.json for changes and broadcast reload to all clients."""
|
||||
global _portals_mtime
|
||||
try:
|
||||
if _portals_path.exists():
|
||||
_portals_mtime = _portals_path.stat().st_mtime
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(2) # Check every 2 seconds
|
||||
try:
|
||||
if not _portals_path.exists():
|
||||
continue
|
||||
current_mtime = _portals_path.stat().st_mtime
|
||||
if current_mtime != _portals_mtime:
|
||||
_portals_mtime = current_mtime
|
||||
logger.info("portals.json changed — broadcasting reload")
|
||||
msg = json.dumps({"type": "portals:reload", "ts": current_mtime})
|
||||
disconnected = set()
|
||||
for client in clients:
|
||||
if client.open:
|
||||
try:
|
||||
await client.send(msg)
|
||||
except Exception:
|
||||
disconnected.add(client)
|
||||
if disconnected:
|
||||
clients.difference_update(disconnected)
|
||||
except OSError as e:
|
||||
logger.warning(f"Portal watch error: {e}")
|
||||
|
||||
async def main():
|
||||
"""Main server loop with graceful shutdown."""
|
||||
logger.info(f"Starting Nexus WS gateway on ws://{HOST}:{PORT}")
|
||||
@@ -137,13 +99,8 @@ async def main():
|
||||
pass
|
||||
|
||||
async with websockets.serve(broadcast_handler, HOST, PORT):
|
||||
# Start portal file watcher
|
||||
watcher_task = asyncio.create_task(_watch_portals())
|
||||
logger.info("Portal hot-reload watcher started.")
|
||||
|
||||
logger.info("Gateway is ready and listening.")
|
||||
logger.info("Gateway is ready and listening.")
|
||||
await stop
|
||||
watcher_task.cancel()
|
||||
|
||||
logger.info("Shutting down Nexus WS gateway...")
|
||||
# Close any remaining client connections (handlers may have already cleaned up)
|
||||
|
||||
Reference in New Issue
Block a user