The patch tier refused on a whole-file hash mismatch, which is the wrong
question: the three patches touch three small regions of three large files, so
an operator who edited Logging.cs somewhere else entirely was handed a manual
patch job they did not need. Hand-modified shards are the norm, so that refusal
covered most of the audience.
Replace the single hash test with a four-rung ladder (PLAN §2.2.1), cheapest and
safest first:
0 post-patch text already present -> no-op, keeps re-runs idempotent
1 whole file matches the pre-image -> apply verbatim
2 file differs, patched region is still byte-identical -> apply at the
matched offset
3 anything else -> do not touch the file; print the hunk to apply by hand
Rung 2 needs no new metadata: a unified diff already carries the stock text of
the region it edits (context lines plus the '-' lines). Guardrails keep it from
becoming a fuzzy apply -- exact match with only CRLF/trailing-whitespace
normalisation, exactly one occurrence or it fails, line numbers advisory only,
and all-or-nothing per patch file so a half-patched EventSink.cs cannot happen.
install.json records which rung applied each patch, and doctor and uninstall
report it.
This retires the blanket 57.4-only version gate, so PLAN gains §2.2.2 to draw
the line the ladder does not: content matching is a mechanical guarantee about
where text lands, not a support commitment. 57.4 stays the only supported
version. A non-57.4 tree may attempt the tier, but unsupported, untested and not
guaranteed -- behind a loud banner, a prompt defaulted to no, and its own
--patches-unsupported-servuo flag, because a bare --patches can be hit by
accident in a copied script. The unsupported marker persists into install.json,
every later doctor run, and the uninstall report.
INSTALL.md gets the operator-facing half: a block-quoted warning naming the
silent-script-build failure mode, the updated prerequisite row, prompts and flag
table, and a sample run showing all three outcomes.
Co-Authored-By: Claude <noreply@anthropic.com>
742 lines
37 KiB
Markdown
742 lines
37 KiB
Markdown
# Installing Runic Gateway on your shard
|
||
|
||
Operator guide for the **Runic Gateway installer** — the tool that takes a working ServUO
|
||
installation and connects it to a Runic Gateway website.
|
||
|
||
> **Status: the installer binary is not released yet.**
|
||
>
|
||
> Everything it installs *is* released and published — the sidecar, the plugin overlay, and the
|
||
> [bundle manifest](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json)
|
||
> that names the checked combination of the two. This guide is the operator-facing contract those
|
||
> phases build to, and it is written first on purpose: it is the specification of what the run
|
||
> looks like, what it asks, where it writes, and what it prints.
|
||
>
|
||
> **You can install today without it** — [Appendix A](#appendix-a--installing-by-hand) is the same
|
||
> deployment done by hand, with the commands verified against the current releases. When the binary
|
||
> ships, Appendix A stays as the reference for what it does under the hood.
|
||
>
|
||
> Design of record: [PLAN.md](PLAN.md).
|
||
|
||
---
|
||
|
||
## What this installs
|
||
|
||
Three things, on the machine that runs your shard:
|
||
|
||
| # | Component | Where it comes from |
|
||
|---|---|---|
|
||
| 1 | **The plugin overlay** — C# source that ServUO compiles at boot, copied into your server tree | [`RunicGateway/servuo-plugins`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) release tarball |
|
||
| 2 | **The uo-link sidecar** — a small Rust service that the shard dials out to, and that your website reads from | [`RunicGateway/link`](https://gitea.whitlocktech.com/RunicGateway/link) release binary |
|
||
| 3 | **A record of what it did** — `install.json`, plus a cached copy of any patches it applied | Written by the installer |
|
||
|
||
```
|
||
ServUO shard ──loopback TCP 127.0.0.1:7788──► uo-link sidecar ──HTTP + WebSocket──► website
|
||
(1) overlay (2) binary + service (yours)
|
||
```
|
||
|
||
The shard **dials out**; it never listens for the website and is never reachable from the internet.
|
||
Only the sidecar is exposed, and only to your website.
|
||
|
||
### What it deliberately does not do
|
||
|
||
- **It never restarts or manages ServUO.** Your shard keeps starting the way it always has. The
|
||
installer refuses to run while ServUO is up, and tells you when a restart is required.
|
||
- **It never deletes anything from your server tree.** The overlay sync only adds and overwrites.
|
||
- **It never contacts your website.** It prints four values for you to paste into Admin → Shard.
|
||
- **It never edits stock ServUO files without asking.** That is the opt-in
|
||
[patch tier](#4-the-patch-tier-optional), and skipping it still leaves you with a working bridge.
|
||
|
||
---
|
||
|
||
## Before you begin
|
||
|
||
| Requirement | Detail |
|
||
|---|---|
|
||
| A working ServUO install | It must currently boot and compile scripts cleanly. The installer deploys onto a healthy shard; it does not repair a broken one. |
|
||
| ServUO **57.4** *(patch tier only)* | **57.4 is the only supported version.** The base install works on any reasonably current ServUO. The patch tier is written and tested against stock 57.4; on any other version it is **unsupported and untested** — you can still choose to run it, behind an explicit opt-in, and it applies only where the exact lines it patches are unchanged. See [§4](#4-the-patch-tier-optional). |
|
||
| ServUO **stopped** | `ServUO.exe` holds a lock on `Scripts.dll` and writes `Saves/` on exit. The installer refuses to deploy under a running shard. |
|
||
| Administrator / root | It writes into system directories and registers a service. |
|
||
| Outbound HTTPS | To `gitea.whitlocktech.com`, to fetch the bundle and the two artifacts. Nothing inbound is needed, and no Gitea account or git client is required. |
|
||
| The sidecar on the **same host** as the shard | The shard connects to `127.0.0.1:7788`. Splitting them is not supported — the loopback socket *is* the trust boundary for inbound commands. |
|
||
| Admin access to your Runic Gateway site | The last step is pasting four values into Admin → Shard. |
|
||
|
||
**Back up first.** The overlay overwrites `Scripts/Scripts.csproj` (a stock file), and the patch
|
||
tier edits stock sources. A copy of `Scripts/` and `Config/` before you start costs nothing.
|
||
|
||
---
|
||
|
||
## 1. Download and verify
|
||
|
||
Releases are **unsigned**. There is no code-signing certificate and no notarization, so the
|
||
`SHA256SUMS` file published beside every artifact is the whole trust anchor — check it.
|
||
|
||
Download the installer for your OS, plus `SHA256SUMS`, from the
|
||
[installer releases page](https://gitea.whitlocktech.com/RunicGateway/installer/releases):
|
||
|
||
```
|
||
runicgateway-installer-linux-x86_64
|
||
runicgateway-installer-windows-x86_64.exe
|
||
SHA256SUMS
|
||
```
|
||
|
||
**Linux**
|
||
|
||
```bash
|
||
sha256sum -c SHA256SUMS --ignore-missing
|
||
chmod +x runicgateway-installer-linux-x86_64
|
||
```
|
||
|
||
**Windows** (PowerShell)
|
||
|
||
```powershell
|
||
(Get-FileHash .\runicgateway-installer-windows-x86_64.exe -Algorithm SHA256).Hash
|
||
Get-Content .\SHA256SUMS # compare the line for this file, case-insensitively
|
||
```
|
||
|
||
Windows will show a **SmartScreen "Windows protected your PC"** prompt on first run, because the
|
||
binary is unsigned and unknown. Once you have verified the checksum above: *More info* →
|
||
*Run anyway*. If you would rather not, Appendix A's manual path uses no unsigned binary except the
|
||
sidecar itself, which you verify the same way.
|
||
|
||
The installer applies the same standard to everything **it** downloads: each artifact's SHA256 is
|
||
checked against the value recorded in the bundle manifest — which CI computed after verifying it
|
||
against the publishing repo's own `SHA256SUMS` — and a mismatch aborts the run.
|
||
|
||
### What it installs is a bundle, not "latest"
|
||
|
||
The three components version independently but must agree on one wire protocol, so CI publishes a
|
||
[**bundle**](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/README.md):
|
||
one exact, protocol-checked pair of sidecar + overlay versions. The installer resolves that at run
|
||
time rather than hardcoding versions or blindly taking each repo's newest release.
|
||
|
||
Consequences worth knowing:
|
||
|
||
- A sidecar patch release does **not** mean re-downloading the installer. The bundle is data.
|
||
- `--bundle <tag>` (e.g. `--bundle 2026.08.04`) pins an exact past combination, so a reinstall six
|
||
months from now reproduces today's install rather than tomorrow's.
|
||
|
||
---
|
||
|
||
## 2. Run it
|
||
|
||
```bash
|
||
sudo ./runicgateway-installer-linux-x86_64 install
|
||
```
|
||
|
||
```powershell
|
||
# Windows: from an elevated PowerShell
|
||
.\runicgateway-installer-windows-x86_64.exe install
|
||
```
|
||
|
||
Run `install --verify` first if you want to see exactly what would change and write nothing — the
|
||
same idea as `deploy.ps1 -Verify`, which developers of the plugin use.
|
||
|
||
The installer **does not install itself.** Keep the binary somewhere sensible on the host (it is
|
||
one file); `doctor`, `update` and `uninstall` are run from it later. Examples below shorten it to
|
||
`runicgateway`.
|
||
|
||
### What it asks
|
||
|
||
1. **Your ServUO root** — detected if the installer is run from inside it or from an obvious
|
||
sibling, otherwise prompted. A directory qualifies only if it contains `ServUO.exe`, `Scripts/`
|
||
and `Config/`.
|
||
2. **Whether to apply the patch tier** — off unless you say yes. On a ServUO that is not 57.4 the
|
||
prompt defaults to **no** and carries an unsupported-version warning you have to answer past.
|
||
See [§4](#4-the-patch-tier-optional).
|
||
3. **The hostname your website should use to reach this machine** — used only to compose the two
|
||
URLs it prints at the end. The sidecar's bind address is frequently `127.0.0.1` or `0.0.0.0`,
|
||
neither of which is something to hand to a website.
|
||
4. **Your site's URL** — used only to print a clickable link to its Admin → Shard page. The
|
||
installer never contacts your website.
|
||
|
||
### An illustrative run
|
||
|
||
```
|
||
Runic Gateway installer — bundle 2026.08.04 (protocol 3)
|
||
|
||
ServUO /opt/ServUO (57.4)
|
||
Shard process not running
|
||
Overlay servuo-plugins v0.1.1 protocol 3
|
||
Sidecar uo-link v1.1.0 protocol 3
|
||
|
||
✓ overlay tarball verified sha256 75dc6d6c…
|
||
✓ sidecar binary verified sha256 27d491ef…
|
||
|
||
Overlay sync
|
||
ADD Config/Bridge.cfg
|
||
ADD Scripts/Custom/Bridge/*.cs (22 files)
|
||
CHANGE Scripts/Scripts.csproj
|
||
deployed. add=23 change=1 unchanged=0
|
||
|
||
Patch tier skipped (not selected)
|
||
Without it: no vendor.sale events, no in-game moderation audit forwarding.
|
||
|
||
uo-link
|
||
binary /usr/bin/runicgateway-link
|
||
config /etc/runicgateway/sidecar.toml (created)
|
||
database /var/lib/runicgateway/uo-link.db
|
||
service runicgateway-link.service enabled, running
|
||
|
||
Recorded /etc/runicgateway/install.json
|
||
|
||
Scripts.csproj changed — ServUO rebuilds Scripts.dll on next boot.
|
||
Start your shard when ready; the installer does not start it for you.
|
||
```
|
||
|
||
Then the [token handoff](#5-connect-the-website).
|
||
|
||
### Commands and flags
|
||
|
||
The surface this guide specifies. Each command is idempotent: a second run with nothing new to do
|
||
reports "unchanged" and writes nothing.
|
||
|
||
| Command | What it does |
|
||
|---|---|
|
||
| `install` | The full run above. |
|
||
| `doctor` | Diagnoses an existing deployment end to end — see [§7](#7-day-two). |
|
||
| `update` | Re-resolves the bundle; updates the sidecar (replace + restart) and the overlay (re-sync + tell you to restart ServUO). |
|
||
| `uninstall` | Removes only what the installer exclusively owns; prints — never performs — anything inside your ServUO tree. |
|
||
|
||
| Flag | Applies to | Meaning |
|
||
|---|---|---|
|
||
| `--verify` | `install`, `update` | Dry run. Report every change that would be made; write nothing. |
|
||
| `--servuo <path>` | `install`, `doctor`, `update` | Name the ServUO root instead of detecting or prompting. |
|
||
| `--bundle <tag>` | `install`, `update` | Pin an exact published bundle instead of the current one. |
|
||
| `--patches` / `--no-patches` | `install` | Decide the patch tier non-interactively. `--patches` never loosens the region check: patches whose target lines are not stock are reported for you to apply by hand, not forced. |
|
||
| `--patches-unsupported-servuo` | `install` | Required *in addition to* `--patches` to run the patch tier on a ServUO that is not 57.4. Unsupported and untested — see [§4](#4-the-patch-tier-optional). Ignored on 57.4. |
|
||
| `--host <name>` | `install` | The hostname to print in the website URLs. |
|
||
| `--site-url <url>` | `install` | Your site's base URL, for the Admin → Shard link. |
|
||
| `--yes` | all | Assume the default answer to every prompt. Combine with the flags above for an unattended run. |
|
||
| `--purge` | `uninstall` | Also delete `sidecar.toml` and `uo-link.db`, which are otherwise kept. |
|
||
|
||
---
|
||
|
||
## 3. Where everything lands
|
||
|
||
**Linux**
|
||
|
||
| Path | What |
|
||
|---|---|
|
||
| `/usr/bin/runicgateway-link` | The sidecar binary |
|
||
| `/etc/runicgateway/sidecar.toml` | Sidecar config, including the auth token |
|
||
| `/etc/runicgateway/install.json` | What the installer deployed: versions, commit, per-file hashes, applied patches, timestamps |
|
||
| `/etc/runicgateway/patches/` | Copies of any patches applied, so `uninstall` can print the exact hunks long after the release tarball is gone |
|
||
| `/var/lib/runicgateway/uo-link.db` | The sidecar's SQLite store (event history, cached profiles, link map) |
|
||
| `/etc/systemd/system/runicgateway-link.service` | The service unit, running as a dedicated user |
|
||
|
||
**Windows**
|
||
|
||
| Path | What |
|
||
|---|---|
|
||
| `%ProgramFiles%\RunicGateway\uo-link-sidecar.exe` | The sidecar binary |
|
||
| `%ProgramData%\RunicGateway\sidecar.toml` | Sidecar config, including the auth token |
|
||
| `%ProgramData%\RunicGateway\install.json` | As above |
|
||
| `%ProgramData%\RunicGateway\patches\` | As above |
|
||
| `%ProgramData%\RunicGateway\uo-link.db` | The sidecar's SQLite store |
|
||
| Service `RunicGatewayLink` | Automatic start, restart on failure |
|
||
|
||
**Inside your ServUO tree** (added by the overlay sync — 24 files):
|
||
|
||
```
|
||
Config/Bridge.cfg every bridge setting, heavily commented
|
||
Scripts/Custom/Bridge/*.cs 22 files: the plugin itself
|
||
Scripts/Scripts.csproj OVERWRITES a stock file (see below)
|
||
```
|
||
|
||
Both service definitions pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly. The sidecar's own
|
||
defaults are relative to its working directory, and a service manager's working directory is not
|
||
somewhere you want a database — on Windows it can be `%SystemRoot%\System32` or, under
|
||
`C:\Program Files\`, a silently redirected VirtualStore copy.
|
||
|
||
> **`Scripts.csproj` is overwritten deliberately.** The stock file omits `Scripts/Custom/`, so the
|
||
> plugin would sit in the tree and never compile — and ServUO would not tell you, because it
|
||
> ignores the script build's exit code and silently reloads the previous `Scripts.dll`. That
|
||
> failure mode is the reason [§6](#6-start-servuo-and-verify) exists.
|
||
|
||
---
|
||
|
||
## 4. The patch tier (optional)
|
||
|
||
Most of the plugin ships as **added** files, which is why the base install is a safe file copy. Two
|
||
features cannot: they need edits to stock ServUO sources, because the events they depend on do not
|
||
exist.
|
||
|
||
| Patch | Edits | Gives you | Rebuild needed |
|
||
|---|---|---|---|
|
||
| `playervendor-sale-eventsink.patch` + `playervendor-sale-gump.patch` | `Server/EventSink.cs`, `Scripts/Gumps/PlayerVendorGumps.cs` | `vendor.sale` events — player-vendor purchases with buyer, owner, price and commission, which is what cheat detection needs | **Core solution rebuild** (`dotnet build ServUO.sln`) — the dynamic script build is not enough |
|
||
| `commandlogging-event.patch` | `Scripts/Commands/Logging.cs` | In-game moderation actions (`[ban`, `[kick`, `[bcast`) forwarded to the website's moderation log as `admin.audit` | Script build only — a shard restart is enough |
|
||
|
||
Each patch has a companion `.cs` file that is copied **only after** its patch applies, because it
|
||
references symbols the patch introduces. That is why they are not in the base overlay: shipping them
|
||
unconditionally would break the build on every unpatched install.
|
||
|
||
How the installer handles it:
|
||
|
||
- **Opt-in.** The base install completes without it, and declining is a supported outcome, not a
|
||
degraded one.
|
||
- **Dry-run first, always.** Every patch is checked before anything is applied, and reported per
|
||
patch. Most real shards are hand-modified; a patch that does not apply is expected, not alarming.
|
||
- **A modified file is not automatically a refusal.** These patches touch three small regions of
|
||
three large files. If you have edited `Logging.cs` somewhere else entirely, the installer says so
|
||
and still applies the patch — it checks whether *the lines the patch edits* are still stock, not
|
||
whether the whole file is. It applies only where the surrounding lines match the patch exactly and
|
||
appear exactly once; anything less and it stops and hands you the hunk to apply by hand. It never
|
||
force-fits a patch by loosening the match.
|
||
- **All or nothing per feature.** The two vendor-sale patches are one unit and are applied together
|
||
or not at all — and within a patch, if one hunk cannot be placed safely, none are.
|
||
- **Your ServUO version is reported, not decisive** — but see the warning below before running this
|
||
on anything other than 57.4.
|
||
- **Recorded, and the `.patch` files cached**, so re-runs stay idempotent and `uninstall` can print
|
||
the exact hunks to revert — along with how each was applied, since a patch placed into a file you
|
||
had already modified is one to look at more carefully when reverting.
|
||
|
||
### ⚠ On any ServUO that is not 57.4: unsupported, untested, no guarantees
|
||
|
||
> **Runic Gateway is designed, built and tested against stock ServUO 57.4.** That is the only
|
||
> supported version.
|
||
>
|
||
> On any other version — a newer release, an older one, or a fork — the patch tier is
|
||
> **UNSUPPORTED, UNTESTED, and NOT GUARANTEED TO WORK.** You may run it. If you do, you are on your
|
||
> own: it is not covered by support, and a bad outcome may not show up until your shard is live,
|
||
> because ServUO's script build reports success even when it failed and quietly keeps running the
|
||
> previous `Scripts.dll`.
|
||
>
|
||
> The installer will still refuse to place a patch anywhere the exact lines it edits have changed —
|
||
> but matching text is not the same as matching behaviour. A hunk can land correctly and still be
|
||
> wrong for a tree that has diverged around it.
|
||
>
|
||
> **Back up your ServUO tree first, and verify your shard boots and compiles afterwards.**
|
||
|
||
Because of that, on a non-57.4 tree the tier is off by default and takes a deliberate yes:
|
||
|
||
- the interactive prompt defaults to **no** and prints the warning above;
|
||
- `--patches` on its own is **not** enough — an unattended run must also pass
|
||
`--patches-unsupported-servuo`;
|
||
- the choice is recorded, and `doctor` keeps showing an unsupported-version row for the life of the
|
||
install — so whoever looks after this shard next can see it without being told.
|
||
|
||
A run where the tier is selected on a shard that has been worked on looks like this:
|
||
|
||
```
|
||
Patch tier 2 of 3 applied
|
||
✓ playervendor-sale-eventsink Server/EventSink.cs stock file
|
||
✓ playervendor-sale-gump Scripts/Gumps/PlayerVendorGumps.cs
|
||
file modified, patched region stock — applied at line 1180
|
||
✗ commandlogging-event Scripts/Commands/Logging.cs
|
||
patched region has been modified — not applied
|
||
apply this hunk by hand, then re-run install:
|
||
/etc/runicgateway/patches/commandlogging-event.patch
|
||
|
||
⚠ Server/EventSink.cs changed — rebuild the core: dotnet build ServUO.sln
|
||
Without commandlogging-event: no in-game moderation audit forwarding.
|
||
```
|
||
|
||
If it is skipped or fails, you lose exactly two things — **`vendor.sale` events** and **in-game
|
||
moderation audit forwarding**. Everything else works. You can apply the patches later by hand (see
|
||
`patches/README.md` in the tarball) and re-run `install` to record it.
|
||
|
||
---
|
||
|
||
## 5. Connect the website
|
||
|
||
The installer ends a successful run by printing the one manual step it cannot do for you:
|
||
|
||
```
|
||
Runic Gateway is installed.
|
||
|
||
One manual step remains — connect the website to this sidecar:
|
||
|
||
Base URL http://shard.example.com:8080
|
||
WebSocket URL ws://shard.example.com:8080/ws
|
||
Protocol version 3
|
||
Auth token 4f9c… (also in /etc/runicgateway/sidecar.toml)
|
||
|
||
Paste these into Admin → Shard on your Runic Gateway site:
|
||
https://your-site.example/admin/shard
|
||
|
||
The token is write-only once saved — the site will never show it back to you.
|
||
```
|
||
|
||
Every value there comes from asking the installed sidecar itself (`--print-config`), not from a
|
||
log file or a guess, so it cannot drift from what the service actually runs.
|
||
|
||
On your site, sign in as an administrator and open **Admin → Shard (uo-link)**:
|
||
|
||
| Field on the page | Paste |
|
||
|---|---|
|
||
| Enable the shard integration | ✔ on |
|
||
| Base URL (REST) | the **Base URL** line |
|
||
| WebSocket URL (feed) | the **WebSocket URL** line |
|
||
| Auth token | the **Auth token** line |
|
||
| Protocol | the **Protocol version** line (`3`) |
|
||
|
||
Saving restarts the site's ingest client, so the change takes effect immediately. The token is
|
||
AES-GCM encrypted at rest and **never returned to any client** — losing it means reading it back
|
||
from `sidecar.toml` on the shard host, not from the website.
|
||
|
||
### If your website is on a different machine
|
||
|
||
The sidecar binds `127.0.0.1:8080` by default, which is reachable only from the shard host. If your
|
||
website runs elsewhere, you must widen the bind — and then narrow the access:
|
||
|
||
1. Set `[web] bind` in `sidecar.toml` to `0.0.0.0:8080` (or a specific LAN address) and restart the
|
||
service.
|
||
2. **Firewall port 8080 to your website's address only.** The auth token is always on, but it
|
||
travels as a plain bearer token — the sidecar speaks HTTP, not HTTPS.
|
||
3. If the two hosts are not on a trusted network, put the sidecar behind a TLS reverse proxy or a
|
||
VPN/WireGuard link, and give the website the proxied `https://` / `wss://` URLs.
|
||
|
||
The `[shard] bind` line is a different matter: leave it on `127.0.0.1:7788`. That socket accepts
|
||
*inbound commands* to the game, and being loopback-only is what makes that safe.
|
||
|
||
---
|
||
|
||
## 6. Start ServUO and verify
|
||
|
||
Start your shard the way you always do. Then confirm the bridge is actually live — not merely
|
||
installed. **A successful file copy is not a working bridge**: ServUO shells out to `dotnet build`,
|
||
prints the output, ignores the exit code, and reloads the existing `Scripts.dll`, so a broken script
|
||
build looks exactly like a clean boot.
|
||
|
||
**a. Watch the boot output.** You want to see the build succeed *and* the bridge announce itself:
|
||
|
||
```
|
||
Core: Compiling scripts...
|
||
Build succeeded.
|
||
[Bridge] enabled=True endpoint=127.0.0.1:7788 queueCap=10000 sweeps(stat=30s decay=60s …
|
||
```
|
||
|
||
If you scrolled past it, force the question:
|
||
|
||
```bash
|
||
cd <servuo root>
|
||
dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64 # must be 0 errors
|
||
```
|
||
|
||
**b. Ask the shard, in game.** As an Administrator:
|
||
|
||
```
|
||
[bridge status
|
||
```
|
||
|
||
It reports the config plus `connected=True depth=0 sent=… dropped=0 …`. `connected=False` means the
|
||
shard cannot reach the sidecar; `dropped` climbing means the sidecar is wedged and the shard is
|
||
shedding events rather than stalling — which it is designed to do. `[bridge reload` re-reads
|
||
`Bridge.cfg` without a restart; `[bridge sweepnow` forces one pass of every stream.
|
||
|
||
**c. Ask the sidecar.** `/health` needs no auth, so it is safe to curl from a terminal:
|
||
|
||
```bash
|
||
curl -s http://127.0.0.1:8080/health
|
||
{"status":"ok","protocol":3,"plugin_connected":true,"database":"ok","uptime":"2m","last_event":"2026-08-04T18:22:10.412Z"}
|
||
```
|
||
|
||
`plugin_connected: true` is the one that matters — it is the only value in this whole guide that
|
||
distinguishes "files copied" from "the bridge works".
|
||
|
||
**d. Ask the website.** The public site should stop showing the shard as offline, and live events
|
||
should appear on the admin dashboard within seconds.
|
||
|
||
---
|
||
|
||
## 7. Day two
|
||
|
||
### `runicgateway doctor`
|
||
|
||
The command that makes this supportable. Run it before asking anyone for help — its output is the
|
||
first thing a maintainer will want.
|
||
|
||
```
|
||
✓ ServUO found /opt/ServUO (57.4)
|
||
✓ Overlay in sync 24 files, all hashes match install.json
|
||
⚠ Patch tier 1 of 3 applied (region-match) — vendor.sale unavailable
|
||
✓ uo-link installed 1.1.0
|
||
✓ Service running, enabled
|
||
✓ Sidecar reachable 127.0.0.1:8080 /health ok
|
||
✓ Protocol sidecar 3 = overlay manifest 3
|
||
✗ Shard connected no shard has dialed in since boot
|
||
```
|
||
|
||
Three of those rows come from asking the installed sidecar (`--version`, `--print-config`) rather
|
||
than from reading `install.json`, so `doctor` reports what the binary would actually do — including
|
||
which config and database file the *service* resolves — rather than what the installer believes it
|
||
was told. The overlay row compares live file hashes against both `install.json` and the release
|
||
manifest, which is how it tells "you edited a deployed file" from "the overlay moved on".
|
||
|
||
### `runicgateway update`
|
||
|
||
Re-resolves the bundle and moves both halves to a combination whose protocol versions were checked
|
||
together — never to two independently-latest artifacts that may disagree.
|
||
|
||
- **Sidecar**: download → verify → replace binary → restart service. No shard downtime.
|
||
- **Overlay**: download → verify → re-sync → record the new commit → **tell you to restart ServUO.**
|
||
It does not restart your shard.
|
||
|
||
Your `sidecar.toml`, your `Bridge.cfg` edits and your database are not touched. `Bridge.cfg` is
|
||
overwritten only if you have not changed it; a modified copy is reported, not clobbered.
|
||
|
||
### `runicgateway uninstall`
|
||
|
||
Removes what it exclusively owns, and **prints** everything else. The installer cannot know what you
|
||
have changed in your own server tree since deployment, so an automatic revert risks silently eating
|
||
your work.
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Removed** | The sidecar binary, its service entry, `install.json`, the cached patch set |
|
||
| **Kept** | `sidecar.toml` and `uo-link.db` — config and history survive (`--purge` drops them) |
|
||
| **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete |
|
||
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs` and `Logging.cs`, for you to revert |
|
||
|
||
The report is also written to a file, so it survives the scrollback.
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Cause and fix |
|
||
|---|---|
|
||
| **"ServUO is running — stop it before installing"** | Correct, and not overridable. `ServUO.exe` locks `Scripts.dll` and rewrites `Saves/` on exit; deploying underneath it corrupts one or both. Stop the shard, install, start it again. |
|
||
| Shard boots clean but nothing reaches the site | The classic silent failure: ServUO ignores the script build's exit code and reloaded a **stale `Scripts.dll`**. Run `dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64` and read the errors it prints. |
|
||
| `[bridge status` says `connected=False` | The sidecar is not listening on `127.0.0.1:7788`. Check the service is running, and that `[shard] bind` in `sidecar.toml` matches `Host`/`Port` in `Bridge.cfg`. |
|
||
| `[bridge` is not a command | The plugin did not compile, or `Bridge.cfg` has the bridge disabled. See the row above. |
|
||
| Website says the shard is offline; `/health` is fine locally | The website cannot reach port 8080 — bind address or firewall. See [§5](#if-your-website-is-on-a-different-machine). Note that the site is *designed* to render normally with the shard offline, so this fails quietly by design. |
|
||
| Website logs `409` from the sidecar | Protocol mismatch: the number in Admin → Shard does not match the sidecar's. The sidecar rejects rather than mis-parsing. Set the field to what `/health` reports (`protocol`). If the *sidecar* and *overlay* disagree, you have a hand-assembled pair — reinstall from a bundle. |
|
||
| `401` from the sidecar | Wrong or missing auth token. Read the live one back with `uo-link-sidecar --print-config --config <path>`; do not retype it from a screenshot. |
|
||
| A patch will not apply | Expected on a hand-modified shard. The base install is unaffected; you lose only the two features in [§4](#4-the-patch-tier-optional). Apply the hunks by hand if you want them. |
|
||
| `vendor.sale` events never arrive despite patching | The `EventSink.cs` patch is a **core** change. A shard restart is not enough — rebuild the solution (`dotnet build ServUO.sln`). |
|
||
| Sidecar writes its database somewhere unexpected | A relative `[store] path` resolves against the directory holding `sidecar.toml` — not the working directory. Run `--print-config` to see the absolute path it will actually use. |
|
||
| Token leaked into a log or a screenshot | Clear `[web] auth_token` in `sidecar.toml`, restart the service (a new token is generated and saved), read it back with `--print-config`, and re-save it in Admin → Shard. |
|
||
|
||
---
|
||
|
||
## Appendix A — installing by hand
|
||
|
||
This is what the installer automates. It works today, on the current releases, and is the fallback
|
||
whenever you would rather not run an unsigned binary.
|
||
|
||
Throughout: `<servuo>` is your ServUO root, and **the shard is stopped**.
|
||
|
||
### A1. Fetch the bundle (so you install a checked pair)
|
||
|
||
```bash
|
||
curl -s https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/current.json
|
||
```
|
||
|
||
It names the sidecar tag, the overlay tag, their agreed `protocol`, and the SHA256 of every asset.
|
||
Use those versions together; that pairing is the only thing CI has verified.
|
||
|
||
### A2. Deploy the plugin overlay
|
||
|
||
```bash
|
||
curl -LO https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/download/v0.1.1/runicgateway-overlay-0.1.1.tar.gz
|
||
curl -LO https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/download/v0.1.1/SHA256SUMS
|
||
sha256sum -c SHA256SUMS --ignore-missing # must say: OK
|
||
|
||
tar xzf runicgateway-overlay-0.1.1.tar.gz # → runicgateway-overlay/
|
||
cd runicgateway-overlay
|
||
cat manifest.json # version, commit, protocol, per-file hashes
|
||
|
||
cp -r overlay/. <servuo>/ # adds files; overwrites Scripts/Scripts.csproj
|
||
```
|
||
|
||
On Windows, `Expand-Archive` does not read `.tar.gz`; use `tar.exe` (shipped with Windows 10+) and
|
||
`Copy-Item -Recurse -Force`. Plugin developers have `deploy.ps1` in the source repo, which does the
|
||
same copy with a hash diff and a `-Verify` dry run — it is not shipped in the tarball.
|
||
|
||
The overlay only ever **adds or overwrites**. Nothing in your tree is deleted.
|
||
|
||
*Optional — the patch tier* (stock ServUO 57.4 only; see `patches/README.md` in the tarball for the
|
||
full explanation):
|
||
|
||
```bash
|
||
cd <servuo>
|
||
git apply --check patches/playervendor-sale-eventsink.patch patches/playervendor-sale-gump.patch
|
||
git apply patches/playervendor-sale-eventsink.patch patches/playervendor-sale-gump.patch
|
||
cp patches/BridgeVendorSale.cs Scripts/Custom/Bridge/
|
||
dotnet build ServUO.sln # REQUIRED — EventSink.cs is a core file
|
||
|
||
git apply --check patches/commandlogging-event.patch
|
||
git apply patches/commandlogging-event.patch
|
||
cp patches/BridgeModerationAudit.cs Scripts/Custom/Bridge/
|
||
```
|
||
|
||
`git apply` works in a plain directory — the shard does not need to be a git repo. If you use
|
||
`patch` instead, note the core files are CRLF: use `patch --binary`.
|
||
|
||
### A3. Install the sidecar
|
||
|
||
```bash
|
||
curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v1.1.0/uo-link-sidecar-linux-x86_64
|
||
curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v1.1.0/SHA256SUMS
|
||
sha256sum -c SHA256SUMS --ignore-missing
|
||
|
||
sudo install -m 0755 uo-link-sidecar-linux-x86_64 /usr/bin/runicgateway-link
|
||
sudo mkdir -p /etc/runicgateway /var/lib/runicgateway
|
||
```
|
||
|
||
Provision the config and read back the token in one step. `--print-config` writes the file if it is
|
||
missing, generates the auth token if there is none, and prints the resolved settings as JSON — it is
|
||
the supported alternative to scraping the startup log:
|
||
|
||
```bash
|
||
sudo UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db \
|
||
/usr/bin/runicgateway-link --print-config --config /etc/runicgateway/sidecar.toml
|
||
```
|
||
|
||
```json
|
||
{
|
||
"component": "uo-link-sidecar",
|
||
"version": "1.1.0",
|
||
"protocol": 3,
|
||
"config_path": "/etc/runicgateway/sidecar.toml",
|
||
"config_created": true,
|
||
"token_generated": true,
|
||
"shard": { "bind": "127.0.0.1:7788" },
|
||
"web": {
|
||
"bind": "127.0.0.1:8080",
|
||
"ws_path": "/ws",
|
||
"auth_required": true,
|
||
"auth_token": "4f9c…"
|
||
},
|
||
"store": { "path": "/var/lib/runicgateway/uo-link.db" }
|
||
}
|
||
```
|
||
|
||
`config_created` and `token_generated` tell you whether *this* run provisioned anything — the values
|
||
alone cannot distinguish a fresh install from a re-read. **The output contains the auth token in
|
||
clear text**: keep it out of shell transcripts, logs and support bundles.
|
||
|
||
### A4. Register the service
|
||
|
||
**Linux** — `/etc/systemd/system/runicgateway-link.service`:
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=Runic Gateway uo-link sidecar
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=runicgateway
|
||
Environment=UOLINK_CONFIG=/etc/runicgateway/sidecar.toml
|
||
Environment=UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db
|
||
ExecStart=/usr/bin/runicgateway-link
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
```bash
|
||
sudo useradd --system --no-create-home runicgateway
|
||
sudo chown -R runicgateway /var/lib/runicgateway /etc/runicgateway
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now runicgateway-link
|
||
systemctl status runicgateway-link
|
||
```
|
||
|
||
**Windows** (elevated PowerShell) — binary under `%ProgramFiles%`, data under `%ProgramData%`:
|
||
|
||
```powershell
|
||
New-Item -ItemType Directory -Force "$env:ProgramFiles\RunicGateway", "$env:ProgramData\RunicGateway" | Out-Null
|
||
Copy-Item .\uo-link-sidecar-windows-x86_64.exe "$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe"
|
||
|
||
& "$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe" --print-config --config "$env:ProgramData\RunicGateway\sidecar.toml"
|
||
|
||
sc.exe create RunicGatewayLink binPath= "\"$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe\"" start= auto
|
||
sc.exe failure RunicGatewayLink reset= 86400 actions= restart/5000
|
||
[Environment]::SetEnvironmentVariable('UOLINK_CONFIG', "$env:ProgramData\RunicGateway\sidecar.toml", 'Machine')
|
||
[Environment]::SetEnvironmentVariable('UOLINK_DB_PATH', "$env:ProgramData\RunicGateway\uo-link.db", 'Machine')
|
||
sc.exe start RunicGatewayLink
|
||
```
|
||
|
||
Machine environment variables are read at service start, so set them before starting — and never
|
||
leave the config path to the default, which is relative to the service's working directory.
|
||
|
||
### A5. Connect the website, start the shard, verify
|
||
|
||
Exactly as in [§5](#5-connect-the-website) and [§6](#6-start-servuo-and-verify): paste the four
|
||
values into Admin → Shard, start ServUO, then check `[bridge status` in game and `/health` on the
|
||
sidecar.
|
||
|
||
### A6. Updating by hand
|
||
|
||
Re-read `current.json`, and if either version moved: replace the sidecar binary and restart its
|
||
service; re-extract the overlay tarball over your tree and restart ServUO. Keep the two in step —
|
||
`current.json` is the only statement that a given pair speaks the same protocol.
|
||
|
||
---
|
||
|
||
## Appendix B — `sidecar.toml` reference
|
||
|
||
Written on first run with a generated token. Environment variables override the file; the file
|
||
overrides these defaults.
|
||
|
||
```toml
|
||
[shard]
|
||
bind = "127.0.0.1:7788" # where the SHARD dials in. Keep this on loopback.
|
||
|
||
[web]
|
||
bind = "127.0.0.1:8080" # where the WEBSITE connects. Widen only with a firewall in front.
|
||
auth_token = "…" # generated if blank; the website's Admin → Shard "Auth token"
|
||
|
||
[store]
|
||
path = "uo-link.db" # relative paths resolve against this file's directory, not the CWD
|
||
```
|
||
|
||
| Environment variable | Overrides |
|
||
|---|---|
|
||
| `UOLINK_CONFIG` | Which config file to read (`--config <PATH>` outranks it) |
|
||
| `UOLINK_SHARD_BIND` | `[shard] bind` |
|
||
| `UOLINK_WEB_BIND` | `[web] bind` |
|
||
| `UOLINK_WEB_TOKEN` | `[web] auth_token` |
|
||
| `UOLINK_DB_PATH` | `[store] path` |
|
||
|
||
| Sidecar command | Output |
|
||
|---|---|
|
||
| `uo-link-sidecar --version` | `uo-link-sidecar 1.1.0 (protocol 3)` |
|
||
| `uo-link-sidecar --print-config [--config PATH]` | The JSON in [A3](#a3-install-the-sidecar). Provisions on first run. **Contains the token.** |
|
||
| `uo-link-sidecar --help` | Usage. An unrecognized argument exits `2` rather than starting a sidecar you did not ask for. |
|
||
|
||
Authentication is **always on**: a blank token is generated and written back, so the web surface is
|
||
never unauthenticated. `/health` is the one unauthenticated route, so monitoring can reach it.
|
||
|
||
---
|
||
|
||
## Appendix C — `Config/Bridge.cfg` settings worth reviewing
|
||
|
||
The file is deployed heavily commented and every setting has a working default — you can leave it
|
||
entirely alone. These are the ones most shards want to look at once. Run `[bridge reload` after
|
||
editing; endpoint changes take effect on the next reconnect.
|
||
|
||
| Setting | Default | Why you might change it |
|
||
|---|---|---|
|
||
| `LinkUrl` | `https://yoursite/link` | Shown in game when a player runs `[link` to connect their account. **Set this to your site.** |
|
||
| `PublicConnectAddress` | *(blank)* | The one connection detail the bridge will publish, e.g. `play.myshard.com,2593`. Blank omits it; `Server.cfg`'s address is **never** published automatically. |
|
||
| `AdminWriteEnabled` | `false` | Opt-in staff write plane: kick/ban/broadcast from the website. Authorization is enforced on the website; `AdminAccessFloor` is the shard-side floor that even a compromised sidecar cannot cross. |
|
||
| `MarketEnabled`, `MarketSweepSeconds`, `MarketSweepBatch` | `true`, `60`, `25` | The player-vendor index. Coverage takes `ceil(vendors / batch) × seconds` — 500 vendors is one full pass every 20 minutes at the defaults. |
|
||
| `PointsLeaderboardEnabled`, `PointsTopN`, `PointsSystems` | `true`, `10`, *(all shown on the loyalty gump)* | Standings boards. One frame **per system**, and ServUO carries ~25 of them, so a large `TopN` multiplies. |
|
||
| `RulesetEnabled`, `RulesetIncludeSchedule` | `true`, `true` | Publishes your ruleset (expansion, caps, systems on/off) to the site's rules page. Turn the schedule off if you would rather not advertise a predictable restart window. |
|
||
| `SignupMode` | `hybrid` | Which side may mint accounts — `website`, `game`, or `hybrid`. Pair `website` with `Accounts.AutoCreateAccounts=false`, or an in-game login still creates accounts. |
|
||
| `QueueCap` | `10000` | Outbound queue cap. On overflow the plugin **drops oldest** and counts drops, because a stalled sidecar must never take the shard down with it. |
|
||
|
||
Sweep intervals (`StatSweepSeconds`, `DecaySweepSeconds`, `EconomySweepSeconds`, and the rest) trade
|
||
freshness against Core-thread time. The measured cost is small — a vitals sweep is 0.0015 ms per
|
||
character, so 1000 online players is ~1.5 ms per pass — but there is rarely anything to gain by
|
||
hurrying them.
|
||
|
||
---
|
||
|
||
## Where to go next
|
||
|
||
| Doc | What |
|
||
|---|---|
|
||
| [PLAN.md](PLAN.md) | The installer's design of record — phases, locked decisions, the bundle model |
|
||
| [`bundles/README.md`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/README.md) | The compat matrix: what a bundle is and how it is composed |
|
||
| [link/INTEGRATION.md](../link/INTEGRATION.md) | The sidecar's HTTP/WS API — for anyone integrating something other than the website |
|
||
| [link/ADMIN_CONTROLS.md](../link/ADMIN_CONTROLS.md) | The staff write plane in detail, before you turn `AdminWriteEnabled` on |
|
||
| [website/SHARD_VISIBILITY.md](../website/SHARD_VISIBILITY.md) | Which shard data each audience sees, configured on the website |
|
||
| [link/SHARD_PREREQS.md](../link/SHARD_PREREQS.md) | A worked example of diagnosing a shard whose scripts silently stopped compiling |
|