web.rs adds the website-facing HTTP surface (axum): GET /health and GET /ws. Every shard event is broadcast to all connected WebSocket clients as a JSON text frame. main.rs's event loop now broadcast::sends each event after logging it. A lagging client is warned and kept live (misses events) rather than stalling the others; a dead-but-not-closed socket is caught by a 30s server ping. The feed is live-only -- no replay -- since history belongs to REST + SQLite. This side may be exposed beyond loopback (it is the gatekeeper); it defaults to 127.0.0.1:8080 and wants auth before going public. Verified end to end: a WebSocket client connected to /ws, received ws.hello, then live pong events relayed from the shard through the shard-link -> broadcast -> ws path -- the same path a login, sale, or IDOC alert will take to a browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.3 KiB
uo-link sidecar
The Rust half of the bridge. It terminates the loopback link to the ServUO shard and (as it grows) exposes WebSocket + REST to the website.
website ──WS (live feed) / REST (queries)──► sidecar ──loopback TCP 127.0.0.1:7788──► shard
(this) newline-JSON, bidirectional
The sidecar is the TCP listener; the shard dials out to it. That is what keeps the game unreachable from the website — the game exposes no port of its own. See ../docs/PLAN.md §2.
Run
cargo run # info logging
RUST_LOG=debug cargo run # see every event, incl. pong heartbeats
Binds 127.0.0.1:7788 and waits for the shard to connect. Boot the shard (or it will reconnect on its own) and watch server.hello arrive.
Status
| Piece | State |
|---|---|
Shard link (shard.rs) |
done — accepts the shard, reads events, sends commands, re-accepts on disconnect. Verified against the live shard: received server.hello, round-tripped a ping→pong, and reconnected after a sidecar restart. |
WebSocket feed (web.rs) |
done — /ws fans every shard event out to connected clients via a broadcast. Verified: a WS client received ws.hello then live pong events relayed from the shard. Live-only, no replay. |
| REST queries (char profile, roster, vendor snapshot, link submit) | not started |
| SQLite persistence (event history, economy, cached profiles, link map) | not started |
The web server binds 127.0.0.1:8080 by default (WEB_ADDR in main.rs). Routes: GET /health → ok, GET /ws → the live feed. Widen the bind and add auth before exposing it off-host.
Design
shard.rs—serve()binds the listener and accepts shard connections in a loop. Each connection splits into read/write halves: the read half parses newline-JSON intoShardEvent { kind, value }and forwards them; the write half drains an mpsc of command lines.ShardHandle::sendposts a command to whichever shard is currently connected, and drops with a warning if none is — a website query during a shard outage should fail fast and retry, not queue behind a reconnect. Live events that must survive an outage are buffered by the shard, not here.web.rs— the website-facing HTTP surface (axum).AppStateholds thebroadcast::Sender<String>; each/wsclient subscribes and forwards every event as a text frame. A client that lags past the broadcast buffer is warned and kept live (it just misses events) rather than stalling the others. This side may be exposed beyond loopback — it is the gatekeeper, so add auth when you do.main.rs— wires it together: the shard event loop logs each event andbroadcast::sends it to the WS feed. Later phases also persist to SQLite here and turn REST calls into shard commands viaShardHandle.
Wire protocol
Every line is one JSON object with t (epoch ms) and kind. The shard→sidecar events and sidecar→shard commands are catalogued in ../docs/PLAN.md (§5 data catalog, §7 protocol) and were all validated end-to-end while building the plugin. Notable inbound commands the sidecar will issue: char.request, account.roster, vendor.snapshot, link.confirm, towncrier.add/remove, ping.