# 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. | | `overlay.toml` | Release metadata: the wire-protocol version this overlay speaks, and its ServUO compatibility. Read by CI into the release manifest — see [Releases](#releases). | | `.gitea/workflows/release.yml` | Publishes `runicgateway-overlay-.tar.gz` on every merge to `main`. | | [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 CI build** — it cannot be compiled standalone without the ServUO reference assemblies. CI does publish a *source* tarball for the installer to fetch; see [Releases](#releases). - **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 -Verify # show what would change .\deploy.ps1 -ServerPath # write ``` `deploy.ps1` is the **developer-facing** tool and stays that way. Operators get the [Runic Gateway installer](https://gitea.whitlocktech.com/RunicGateway/installer), which does the same sync cross-platform from the release tarball below. ## Releases Every merge to `main` that carries a releasable conventional commit (`feat:`, `fix:`, `perf:`, or a breaking change — a `docs:`/`chore:`-only merge deliberately cuts nothing) publishes a Gitea release: ``` runicgateway-overlay-.tar.gz └── runicgateway-overlay/ ├── manifest.json ├── overlay/ # exactly what deploy.ps1 would copy └── patches/ # the opt-in stock-file diffs + their companion sources SHA256SUMS ``` This is a **source** tarball, not a build — nothing here is compiled. It exists so the installer can deploy the plugin onto a shard host that has no git and no Gitea credentials. `manifest.json` is what makes the tarball self-describing: ```json { "component": "servuo-plugins-overlay", "version": "0.1.0", "commit": "968b526…", "protocol": 3, "servuo": { "min_version": "57.4", "patches_verified_against": "57.4" }, "files": { "overlay/Config/Bridge.cfg": "32718424…", … } } ``` - **`protocol`** comes from `overlay.toml` and is the plugin half of the compatibility contract. The plugin announces no version on the wire and none is queryable before ServUO boots, so this declaration is the only way the installer can check it against the sidecar's `PROTOCOL_VERSION` *before* an operator installs the pair. **When the protocol changes, bump it in the same PR that changes the emitters.** - **`files`** carries a SHA256 per shipped file, so a deployment can later tell "an operator edited this" from "the overlay moved on". The version is derived from git tags — there is no version to maintain by hand and no bump commit, so this workflow never pushes to `main`. The tarball is byte-reproducible for a given tree (`tar --sort=name`, pinned mtime and ownership), so its checksum changes only when its contents do. ## 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`. --- ## License Runic Gateway is free software, licensed under the **GNU General Public License v3.0 or later** — see [LICENSE.md](LICENSE.md). Copyright (C) 2026 Runic Gateway This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. Contributions are welcome — please read [CONTRIBUTING.md](CONTRIBUTING.md) (note the **AI-usage disclosure** requirement) and our [Code of Conduct](CODE_OF_CONDUCT.md). Report vulnerabilities privately per [SECURITY.md](SECURITY.md).