docs(installer): add the operator install guide (Phase 0.4) #87

Merged
whitlocktech merged 2 commits from docs/installer-phase-0.4 into main 2026-08-04 17:11:44 +00:00
3 changed files with 753 additions and 17 deletions

View File

@@ -7,10 +7,11 @@ so they live in one place, independent of either codebase.
## Layout
```
website/ docs from the shard website (Node/Express + MariaDB + React/Vite)
link/ docs from the ServUO bridge (C# plugin + Rust sidecar + Node WS)
android/ docs from the native Android client (Kotlin + Jetpack Compose)
ci/ cross-cutting CI/quality notes
website/ docs from the shard website (Node/Express + MariaDB + React/Vite)
link/ docs from the ServUO bridge (C# plugin + Rust sidecar + Node WS)
android/ docs from the native Android client (Kotlin + Jetpack Compose)
installer/ docs for the installer that deploys a shard's bridge components
ci/ cross-cutting CI/quality notes
```
### `website/`
@@ -50,6 +51,12 @@ ci/ cross-cutting CI/quality notes
| [TRUSTED_DEVICES_APP_HANDOFF.md](android/TRUSTED_DEVICES_APP_HANDOFF.md) | Trusted-devices app handoff notes |
| [PROJECT_TREE.md](android/PROJECT_TREE.md) | Auto-generated snapshot of the repo's tracked file layout |
### `installer/`
| Doc | What it covers |
|---|---|
| [INSTALL.md](installer/INSTALL.md) | **Operator guide** — installing Runic Gateway on a ServUO shard, connecting it to the website, and diagnosing it. Includes the by-hand path, which works today |
| [PLAN.md](installer/PLAN.md) | Installer design of record — phases, locked decisions, the bundle/compat-matrix model |
## Provenance
- `website/*` was extracted from `RunicGateway/website` via `git filter-repo`.

692
installer/INSTALL.md Normal file
View File

@@ -0,0 +1,692 @@
# 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)* | The base install works on any reasonably current ServUO. The patch tier is verified against stock 57.4 only, and is skipped with a warning on anything else. |
| 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, and not offered at all if your
ServUO is not 57.4. 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` still refuses on a non-57.4 tree. |
| `--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 (`git apply --check`) before anything is
applied, and reported per patch. Most real shards are hand-modified; a patch that does not apply
is expected, not alarming.
- **All or nothing per feature.** The two vendor-sale patches are one unit and are applied together
or not at all.
- **Skipped entirely on a ServUO that is not 57.4**, with a warning. Unverified diffs are never
applied to an unknown tree.
- **Recorded, and the `.patch` files cached**, so re-runs stay idempotent and `uninstall` can print
the exact hunks to revert.
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 — 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 |

View File

@@ -1,19 +1,19 @@
# Runic Gateway Installer — plan
Status: **Phase 0 all but complete** — every prerequisite in another repo has landed, and the
installer repo now publishes the bundle manifest, so *what* the installer will install is already
released and composed ahead of the binary that installs it. No installer code exists yet; `0.4`
(`INSTALL.md`) is the remaining item, then Phase 1. This document is the design of record;
it supersedes the informal overview it grew out of, which described a ServUO integration that does
not match how `servuo-plugins` actually ships (see
Status: **Phase 0 complete.** Every prerequisite in another repo has landed, the installer repo
publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) now specifies the operator-facing run
— so *what* the installer installs and *what using it looks like* both exist ahead of the binary.
No installer code exists yet; **Phase 1 is next.** This document is the design of record; it
supersedes the informal overview it grew out of, which described a ServUO integration that does not
match how `servuo-plugins` actually ships (see
[Corrections](#corrections-to-the-original-overview)).
| Phase 0 item | State |
|---|---|
| 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) |
| 0.2 `link` installable (data paths + `--print-config`) | ✅ Merged — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) (docs half [docs#84](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/84)); released as [`v1.1.0`](https://gitea.whitlocktech.com/RunicGateway/link/releases/tag/v1.1.0) |
| 0.3 Bundle CI in the installer repo | 🟨 In review — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: `2026.08.04` |
| 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` is **next** — the shape has now settled |
| 0.3 Bundle CI in the installer repo | ✅ Merged — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: [`2026.08.04`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json) |
| 0.4 This file + `INSTALL.md` | 🟨 In review — [docs#87](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/87). [`INSTALL.md`](INSTALL.md) is the operator guide, written before the binary because it *is* the specification of the run |
| — Repo bootstrap (governance + CI) | ✅ [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows merged ([installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1), [#2](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/2)) |
---
@@ -180,7 +180,7 @@ Runic Gateway Installer v1.0.0
├── runicgateway-installer-linux-x86_64
└── SHA256SUMS
uo-link v0.x.y (existing release, extended)
uo-link v1.1.0 (existing release, extended)
├── uo-link-sidecar-windows-x86_64.exe
├── uo-link-sidecar-linux-x86_64
├── runicgateway-link_<ver>_amd64.deb (Phase 5)
@@ -337,12 +337,48 @@ Repo work that must land before an installer can exist.
4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the
shape is settled.
As built ([`INSTALL.md`](INSTALL.md)) — written *before* the binary on purpose. Everything it
installs is already released (items 13), so the guide is not speculation about a tool that
might exist; it is the specification of what the run asks, where it writes, what it prints, and
what the operator does next. Phase 14 implement it.
- **It is useful before the installer exists.** Appendix A is the same deployment done by hand —
bundle fetch, tarball verify + overlay copy, the optional patch tier, `--print-config`
provisioning, and a systemd unit / `sc create` service — composed from the released artifacts'
actual contents and the sidecar's config and CLI source rather than from memory. That appendix
doubles as **Phase 1's acceptance test**: walking it end to end on a real shard is what proves
the automated path has nothing left to discover.
- **The installer does not install itself.** §5's `runicgateway doctor` sketch implied a name on
`PATH`; nothing places one there, and adding self-installation would give the tool a second
lifecycle to manage. The guide names the downloaded artifact, says to keep it, and shortens it
in later examples.
- **The flag surface got fixed here**, because a guide cannot describe a run in the abstract:
`--verify`, `--bundle`, `--purge` were already named by §5/§7; `--servuo`, `--patches` /
`--no-patches`, `--host`, `--site-url` and `--yes` are the remainder, chosen so every prompt
in §6's handoff has a non-interactive equivalent and an unattended install is expressible.
- **A modified `Bridge.cfg` must survive an update** — see Phase 1, where this changes the sync
rule inherited from `deploy.ps1`.
- **Remote-website deployments needed an answer.** `[web] bind` defaults to `127.0.0.1`, which
only works when the site runs on the shard host. The guide says to widen it, firewall it to
the website's address, and front it with TLS or a VPN off a trusted network — because the
token is always required but travels as a plain bearer token over HTTP. `[shard] bind` stays
on loopback, since that socket carries inbound commands *into* the game.
### Phase 1 — installer core
- ServUO root detection and validation (`ServUO.exe`, `Scripts/`, `Config/`), with version detection
and an explicit refusal when the ServUO process is running.
- Overlay sync: fetch tarball → verify SHA256 → hash-compare against the server tree → add/change,
**never delete**. Port of `deploy.ps1` semantics including its `-Verify` dry run (`--verify`).
- **One deviation from `deploy.ps1`: an operator-modified `Config/Bridge.cfg` is reported, not
overwritten.** `deploy.ps1` overwrites every file whose hash differs, which is right for a
developer redeploying their own tree and wrong for an operator who has set `LinkUrl`,
`PublicConnectAddress` and sweep intervals — an `update` would silently revert the shard's entire
configuration. `install.json` records the hash deployed, so the installer can distinguish "the
operator edited this" from "the overlay moved on" (§7.0) and act only on the second. The rule is
specific to `Bridge.cfg`: it is the only file in the overlay that is *meant* to be edited in
place, and it carries no code, so a stale copy cannot break the build. Every `.cs` file and
`Scripts.csproj` still overwrite unconditionally.
- Write `install.json`: component, version, source commit, per-file hashes, applied patches,
timestamp.
- Idempotent re-runs; a second run with no upstream change reports "unchanged" and writes nothing.
@@ -370,9 +406,9 @@ degrade loudly rather than silently.
```
✓ ServUO found /opt/ServUO (57.4)
✓ Overlay in sync 30 files, all hashes match install.json
✓ Overlay in sync 24 files, all hashes match install.json
⚠ Patch tier 1 of 3 applied — vendor.sale unavailable
✓ uo-link installed 0.3.0
✓ uo-link installed 1.1.0
✓ Service running, enabled
✓ Sidecar reachable 127.0.0.1:8080 /health ok
✓ Protocol sidecar 3 = overlay manifest 3
@@ -393,8 +429,9 @@ deliberately:
- **uo-link**: compare the bundle's version against what is installed → download → verify checksum →
replace binary → restart service.
- **plugin overlay**: download the bundle's overlay tarball → verify → re-sync → record commit →
tell the operator ServUO must restart (the installer does not restart the shard).
- **plugin overlay**: download the bundle's overlay tarball → verify → re-sync (leaving a modified
`Bridge.cfg` alone — Phase 1) → record commit → tell the operator ServUO must restart (the
installer does not restart the shard).
Because both come from one bundle, an update always moves to a combination whose protocol versions
were checked together, rather than to two independently-latest artifacts that may disagree.