"## Deploy" led with deploy.ps1 and mentioned the installer only afterwards, which is backwards now that the installer is released. - Deploy leads with the installer, with the by-hand overlay copy (INSTALL.md Appendix A2) as the supported alternative. - deploy.ps1 gets its own subsection as the developer path: it deploys from a working tree, which is the one thing the installer cannot do, and it installs no sidecar and checks no protocol pairing. - CONTRIBUTING: note that changes reach shards through a release, so a change that only works when deploy.ps1 copies it does not ship. Co-Authored-By: Claude <noreply@anthropic.com>
217 lines
14 KiB
Markdown
217 lines
14 KiB
Markdown
# 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` | **Developer tool** — copies `overlay/` from this working tree into a server root. `-Verify` diffs instead of writing. Operators use the [installer](https://gitea.whitlocktech.com/RunicGateway/installer); see [Deploy](#deploy). |
|
||
| `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-<ver>.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** — by the
|
||
[installer](https://gitea.whitlocktech.com/RunicGateway/installer), in one run — but built
|
||
**independently**:
|
||
|
||
- **This plugin** is deployed as *source*: `overlay/` is copied 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
|
||
publishes a *source* tarball, which is what the installer fetches and syncs; see
|
||
[Releases](#releases).
|
||
- **The sidecar** is a standalone Rust binary, released from its own repo and installed from that
|
||
release.
|
||
|
||
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
|
||
|
||
**On a shard, use the [Runic Gateway installer](https://gitea.whitlocktech.com/RunicGateway/installer).**
|
||
One binary syncs this overlay from the release tarball below, offers the patch tier, installs the
|
||
uo-link sidecar as a service, and prints the values your website needs — cross-platform, with a
|
||
`doctor` afterwards to tell a copied file from a working bridge:
|
||
|
||
```bash
|
||
sudo ./runicgateway-installer-linux-x86_64 install
|
||
```
|
||
|
||
Guide: [installer/INSTALL.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md).
|
||
To place the overlay yourself instead — a host that cannot run the binary, or you want to see every
|
||
file land — [Appendix A2](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md#a2-deploy-the-plugin-overlay)
|
||
is the same copy done by hand, and stays supported.
|
||
|
||
### `deploy.ps1` — the developer path
|
||
|
||
`deploy.ps1` deploys from a **working tree**, which is what you want while writing plugin code and
|
||
is the one thing the installer cannot do (it deploys from a release):
|
||
|
||
```powershell
|
||
.\deploy.ps1 -ServerPath <servuo> -Verify # show what would change
|
||
.\deploy.ps1 -ServerPath <servuo> # write
|
||
```
|
||
|
||
It is Windows-only and stays developer-facing; it never installs the sidecar, registers a service,
|
||
or checks the protocol pairing. Nothing shipped to an operator depends on it.
|
||
|
||
## 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-<ver>.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).
|