Files
link/README.md
Claude 9c02ba45dc Phase 1: loopback transport to the sidecar
BridgeLink owns a TcpClient to 127.0.0.1 and nothing else touches it. Emit() is
called from the Core thread; it enqueues onto a bounded drop-oldest queue and
returns. A link thread drains the queue and reconnects with backoff; a reader
thread parses inbound lines and marshals each to the Core thread via
Timer.DelayCall. An absent, slow, or wedged sidecar therefore cannot stall the
shard, which is the property the rest of the bridge depends on.

Outbound JSON is written by hand into a StringBuilder because it runs on the
Core thread for every event and the measured budget assumes that cost. Inbound
uses JavaScriptSerializer: commands arrive at human rates, so correctness beats
speed, and parsing happens off the Core thread anyway. That needs a
System.Web.Extensions reference.

server.hello is emitted per connection rather than once at ServerStarted. A
sidecar that restarts independently would otherwise never learn which shard it
is attached to. It carries a bootId, stable across reconnects and fresh on every
shard restart, so the sidecar can tell "I reconnected" from "the shard
restarted" and keep or discard its cache accordingly.

Two defects found by testing and fixed before commit:

  - Backoff ceiling was 30s, so a sidecar restart cost up to half a minute of
    buffering on a loopback socket. Now 5s.
  - A stale reader could kill a fresh connection: reader.Join(1s) can time out,
    and the old thread's finally block then set the shared _dead flag, possibly
    tearing down the connection that had replaced it. Connections now carry an
    epoch and a reader only marks dead the one it owned.

Acceptance evidence recorded in docs/PLAN.md §11: boots with no sidecar, buffers
through the outage and drains on connect, round-trips ping/pong on the Core
thread, survives unknown kinds and malformed JSON, and reconnects unattended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 05:03:03 -05:00

80 lines
4.3 KiB
Markdown

# uo-link
ServUO ⇄ Rust sidecar bridge. The shard emits newline-delimited JSON over a loopback TCP socket; the sidecar owns the WebSocket the website consumes.
```
ServUO plugin (C#, net48) ──loopback TCP, newline-JSON──► Rust sidecar ──WebSocket/JSON──► website
(Core-thread reads) ◄──inbound commands─────────────┘ (owns WS, auth, buffering, fan-out)
```
The shard never speaks WebSocket. Every world read happens on the Core thread; the socket is touched only by a dedicated writer thread draining a bounded queue.
## Layout
| Path | What |
|------|------|
| `overlay/` | Mirrors the ServUO server root. Everything here — and **only** this — copies over an install. |
| `patches/` | Unified diffs against stock ServUO for files we must modify rather than add. |
| `tools/` | Never deployed. Test scaffolding and anything else that must not reach a server. |
| `docs/PLAN.md` | Implementation plan, measured performance budget, and the full data catalog. |
| `docs/RESEARCH.md` | Original source-level research. Partly superseded — see the corrections table in `PLAN.md` §8. |
| `docs/SHARD_PREREQS.md` | Repairs the target shard needed before any of this could load. |
| `deploy.ps1` | Copies `overlay/` into a server root. `-Verify` diffs instead of writing. |
Anything under `overlay/` is authoritative. Do not edit files in the server tree directly — edit here and deploy.
## Deploy
```powershell
.\deploy.ps1 -ServerPath C:\Users\colby\Desktop\servuo -Verify # show what would change
.\deploy.ps1 -ServerPath C:\Users\colby\Desktop\servuo # write
```
## Status
| Phase | State |
|------:|-------|
| 0 — build fix (`Scripts.csproj`) | **done, verified end-to-end** |
| 1 — transport (`BridgeLink`) | **done, acceptance in `docs/PLAN.md` §11** |
| 2 — event streams | not started |
| 3 — sweeps | not started |
| 4 — request/response | not started |
| 5 — `[link` account linking | not started |
| 6 — town-crier inbound | not started |
| 7 — `PlayerVendorSale` core event | not started |
| 8 — cheat signals | not started |
## Phase 0 — what it fixes
`ScriptCompiler.Compile()` runs `dotnet build Scripts/Scripts.csproj -c Release`, prints the output, and **never checks the exit code**, then `Assembly.LoadFrom("Scripts.dll")` and returns `true`. Because that build passed no `Platform`, MSBuild defaulted to `AnyCPU`, and `Scripts.csproj` gated both `OutputPath` and `DefineConstants` on `Configuration|Platform == Release|x64`. So:
- the DLL landed in `Scripts/bin/Release/` while the core loads `Scripts.dll` from the base directory, and
- `TRACE;NEWTIMERS;ServUO` went undefined, so XmlSpawner compiled its non-ServUO branches.
Runtime script compilation therefore had no effect, silently. `overlay/Scripts/Scripts.csproj` conditions both property groups on `Configuration` alone.
`Server.csproj` is deliberately left alone: nothing under `Server/` uses those symbols, and giving it `OutputPath=..\` would make the boot-time build try to overwrite the running `ServUO.exe`.
## The plugin (Phase 1)
`overlay/Scripts/Custom/Bridge/`:
| File | Responsibility |
|------|----------------|
| `BridgeConfig.cs` | Reads `Config/Bridge.cfg` in `Configure()`, before `World.Load`. |
| `BridgeJson.cs` | Outbound JSON by hand (Core thread, so no reflection serializer). Inbound via `JavaScriptSerializer`. |
| `BridgeLink.cs` | The socket. Link thread owns it; a bounded drop-oldest queue fronts it; a reader thread marshals inbound lines to the Core thread. |
| `BridgeBoot.cs` | Lifecycle, inbound dispatch, `[bridge status\|reload\|ping]`. |
`Emit()` is called from the Core thread. It enqueues and returns — it never touches the socket, never blocks, never allocates a syscall. **A wedged or absent sidecar cannot stall the shard**, and that is the property everything else depends on.
## Testing
`tools/stub_sidecar.ps1` is a loopback listener that logs every line the shard sends. Run it, boot the shard, watch `server.hello` arrive.
```powershell
.\tools\stub_sidecar.ps1 -Port 7788 -Log .\sidecar.log
```
`tools/scaffolding/` holds the world seeder and the performance probe. Neither is deployed — `deploy.ps1` only copies `overlay/`. They produced the budget in `docs/PLAN.md` §1. See `tools/scaffolding/README.md`.