# 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 ```bash 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 into `ShardEvent { kind, value }` and forwards them; the write half drains an mpsc of command lines. `ShardHandle::send` posts 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). `AppState` holds the `broadcast::Sender`; each `/ws` client 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 and `broadcast::send`s it to the WS feed. Later phases also persist to SQLite here and turn REST calls into shard commands via `ShardHandle`. ## 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`.