diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a6da89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +bin/ +obj/ +*.user +*.suo +.vs/ +*.dll +*.exe +*.pdb +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..dba6395 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# Runic Gateway — ServUO Plugin + +The **C# ServUO side** of the Runic Gateway bridge. The shard emits newline-delimited JSON +over a loopback TCP socket to the Rust **sidecar** ([RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link)), +which 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) + >>> THIS REPO <<< (RunicGateway/link) +``` + +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. + +## Documentation + +All project documentation lives in the central **[RunicGateway/docs](https://gitea.whitlocktech.com/RunicGateway/docs)** repo, +under [`link/`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link) (design docs, +integration guide, protocol spec, research — with full history preserved). + +## 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 (C# probes + PowerShell stub sidecars) and anything else that must not reach a server. | +| `deploy.ps1` | Copies `overlay/` into a server root. `-Verify` diffs instead of writing. | +| [INTEGRATION.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/INTEGRATION.md) | **Website integration guide** — the WebSocket feed, REST endpoints, auth, event catalog, and examples. | +| [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) | Implementation plan, measured performance budget, and the full data catalog. | +| [RESEARCH.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/RESEARCH.md) | Original source-level research. Partly superseded — see the corrections table in `PLAN.md` §8. | +| [SHARD_PREREQS.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/SHARD_PREREQS.md) | Repairs the target shard needed before any of this could load. | + +Anything under `overlay/` is authoritative. Do not edit files in the server tree directly — edit here and deploy. + +## Sidecar & deployment + +The Rust sidecar is the other half of the bridge and lives in **[RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link)**. +The two are deployed **together** but built **independently**: + +- **This plugin** is deployed as *source* — `deploy.ps1` copies `overlay/` into the ServUO server + root, and ServUO compiles it at boot (`Scripts.csproj`; see [Phase 0](#phase-0--what-it-fixes)). + There is no separate build artifact and no CI build — it cannot be compiled standalone without the + ServUO reference assemblies. +- **The sidecar** is a standalone Rust binary, released from its own repo. + +The **only** coupling is the loopback JSON protocol (the shard dials out to the sidecar on +`127.0.0.1`). Compatibility is a **protocol** concern, not a build-order one: keep the event/command +catalog in sync across the two repos (canonical spec: [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) +§5/§7 and [INTEGRATION.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/INTEGRATION.md)). +A wedged or absent sidecar cannot stall the shard, so the plugin can be deployed before, after, or +without the sidecar running. + +## 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 [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §11** | +| 2 — event streams (`BridgeEvents`) | **done, acceptance in [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §12** | +| 3 — sweeps (`BridgeSweeps`) | **done, acceptance in [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §13** | +| 4 — request/response (`BridgeRequests`) | **done, acceptance in [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §14** | +| 5 — `[link` account linking (`BridgeAccountLink`) | **done, acceptance in [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §15** | +| 6 — town-crier inbound (`BridgeTownCrier`) | **done, acceptance in [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §16** | +| 7 — `PlayerVendorSale` core event (`patches/` + `BridgeVendorSale`) | **done, acceptance in [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §17** | + +Every phase on the ServUO side is complete. Phases 0–6 are drop-in (`overlay/`); Phase 7 is the one +core change, shipped as `patches/`. + +Cheat-detection signals are not a separate phase — they are folded into the streams above: +`cheat.fastwalk`, `audit.set`, `audit.command`, and `vendor.sale` (buyer + owner for laundering detection). + +## 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. | +| `BridgeTownCrier.cs` | Town-crier news (Phase 6): inbound `towncrier.add` / `remove` into the global crier list, with abuse caps. | + +`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 ([RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link)) 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 [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §1. See `tools/scaffolding/README.md`.