Files
link/README.md
colby be05ea912c Phase 5: [link account linking
BridgeAccountLink ties a game account to a website account. [link mints a
one-time, 5-minute code from an unambiguous alphabet (no O/0/I/1), holds it in a
Core-thread dict keyed to the account, and emits link.request. The website
relays the code back through the sidecar as link.confirm; the shard validates,
writes the WebsiteUserId account tag, and replies link.ok. A bad or expired code
gets link.error.

The tag persists to accounts.xml in ServUO's standard <tags> format, read by
LoadTags at boot, so a link survives restarts with no new persistence layer.
mob.login now carries webId when the account is linked, so the sidecar can
attribute a session to a site user without a lookup.

Safeguards: one-time codes; only the newest code per account is valid; per-account
30s rate limit against code spam; a 1-minute purge bounds the code table; the
websiteUserId is trusted only because the socket is loopback-only. The tag reaches
memory on confirm but disk only on the next save — a hard crash between loses it,
and the player just re-runs [link.

Verified end to end with a smart stub that reads the emitted code and confirms
it: link.request -> link.confirm -> link.ok, a bad code -> link.error, and the
tag observed in accounts.xml after a save. Evidence in docs/PLAN.md §15.

The [link command body is exposed as RequestLink(Mobile) so it can be driven in
tests without a client. Adds tools/stub_sidecar_link.ps1 and
tools/scaffolding/BridgeLinkProbe.cs.

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

89 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (`BridgeEvents`) | **done, acceptance in `docs/PLAN.md` §12** |
| 3 — sweeps (`BridgeSweeps`) | **done, acceptance in `docs/PLAN.md` §13** |
| 4 — request/response (`BridgeRequests`) | **done, acceptance in `docs/PLAN.md` §14** |
| 5 — `[link` account linking (`BridgeAccountLink`) | **done, acceptance in `docs/PLAN.md` §15** |
| 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]`. |
| `BridgeEvents.cs` | EventSink subscriptions (Phase 2). Read-only, player-filtered, never emits secrets. |
| `BridgeSweeps.cs` | Polled streams (Phase 3): vitals, house decay on transition, economy supply. Core-thread timers. |
| `BridgeProfile.cs` | Read-model builders (Phase 4): full character profile, account roster. Core-thread reads. |
| `BridgeRequests.cs` | Inbound request handlers (Phase 4): `char.request`, `account.roster`, `vendor.snapshot`, with `bridge.error` replies. |
| `BridgeAccountLink.cs` | `[link` account linking (Phase 5): one-time code, `link.confirm`, `WebsiteUserId` account tag. |
`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. It survives a just-killed instance (SO_REUSEADDR) and won't die on a transient error.
```powershell
.\tools\stub_sidecar.ps1 -Port 7788 -Log .\sidecar.log
```
`tools/stub_sidecar_request.ps1` additionally *sends* inbound requests (`char.request`, `account.roster`, `vendor.snapshot`, plus an error case) right after the shard connects, and logs the replies — the harness used to validate Phase 4.
Note: the throwaway PowerShell sidecars are fragile — they get reaped and contend on their log file. The real Rust sidecar replaces them; don't read their flakiness as a shard problem. The shard buffers non-perishable events through any outage and reconnects on its own (observed reconnecting 5× unattended in one session).
`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`.