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>
89 lines
5.7 KiB
Markdown
89 lines
5.7 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 (`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`.
|