A TLS reverse proxy in front of the sidecar is a supported deployment already running on a real domain, not the fallback the guide framed it as. Recommend it first, keep [web] bind on loopback in that arrangement, and demote widen-the- bind-and-firewall to the trusted-LAN alternative - on that path the token and every event cross the network in the clear. Adds the four things a proxy must actually do, checked against web.rs: forward the WebSocket upgrade (/ws is the entire live feed, and losing it leaves REST working with no events - a confusing half-working state); pass headers through unmodified (auth is Authorization: Bearer or X-Api-Key, and a stripped X-UOLink-Version silently skips the 409 mismatch check); no buffering and long-lived connections (the sidecar pings every 30s, so a 60s+ read timeout is safe as it stands); and no query-string logging, since ?token= is an accepted auth form. Nothing needs X-Forwarded-For - the sidecar never uses the client IP for authorization. Includes an nginx block that satisfies all four, and notes Caddy and Traefik need no equivalent. The website gets the proxied https:// / wss:// URLs, not the pair the installer prints: those are composed from the sidecar's own bind address, which knows nothing about what fronts it. PLAN records that the installer deliberately does not try to detect a proxy - nothing visible from the sidecar's side says what is in front of it, so guessing would print a confidently wrong URL. Two new troubleshooting rows for the symptoms this causes: REST works but no events (upgrade not forwarded), and a feed that drops every minute or two (read timeout under the ping interval, or buffering). Co-Authored-By: Claude <noreply@anthropic.com>
37 KiB
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 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 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.
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 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 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, 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:
runicgateway-installer-linux-x86_64
runicgateway-installer-windows-x86_64.exe
SHA256SUMS
Linux
sha256sum -c SHA256SUMS --ignore-missing
chmod +x runicgateway-installer-linux-x86_64
Windows (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: 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
sudo ./runicgateway-installer-linux-x86_64 install
# 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
- 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/andConfig/. - 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.
- 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.1or0.0.0.0, neither of which is something to hand to a website. - 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.
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. |
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.csprojis overwritten deliberately. The stock file omitsScripts/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 previousScripts.dll. That failure mode is the reason §6 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
.patchfiles cached, so re-runs stay idempotent anduninstallcan 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 the sidecar sits behind a reverse proxy, paste your public https:// and wss:// URLs
instead of the two the installer printed — it composes those from the sidecar's own bind address,
which knows nothing about what fronts it. Everything else on the page is unchanged.
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, the recommended arrangement is a TLS reverse proxy in front of the
sidecar — this is a supported deployment and the one Runic Gateway itself runs, on a real domain
name.
Leave [web] bind on 127.0.0.1:8080 and let the proxy be the only thing that talks to it.
Widening the bind and firewalling the port is the alternative, not the default (see below).
Give the website the proxied URLs — https://link.example.com and
wss://link.example.com/ws — in place of the http:// / ws:// pair the installer prints. Those
values are the sidecar's own view of itself; the proxy is what the outside world sees.
What the proxy must do:
| Requirement | Why |
|---|---|
Forward the WebSocket upgrade (Upgrade / Connection headers, HTTP/1.1 to the upstream) |
/ws is the live event feed. Without it the site's REST calls work and events never arrive — a confusing half-working state. |
| Pass request headers through unmodified | Auth is Authorization: Bearer (or X-Api-Key), and the website sends X-UOLink-Version. A proxy that strips unknown headers turns into a 401, and a stripped version header just silently skips the mismatch check. |
| Do not buffer the WS connection, and allow long-lived ones | The feed is idle between events. The sidecar sends a WebSocket Ping every 30 s, so a read timeout of 60 s or more is safe as it stands — but a proxy that buffers responses will hold events instead of streaming them. |
| Do not log query strings | The sidecar also accepts ?token=… (for clients that cannot set headers). If anything in your stack uses that form, a default access-log format writes your auth token to disk on every request. |
Nothing needs X-Forwarded-For: the sidecar never uses the client's IP for authorization, and the
browser IP that account provisioning cares about is supplied by the website in the request body.
An nginx server block that satisfies all of the above:
server {
listen 443 ssl;
server_name link.example.com;
# your certificate directives here
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; # "upgrade" for WS, "" otherwise
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 300s;
}
}
(with the usual map $http_upgrade $connection_upgrade { default upgrade; '' close; } at http
level). Caddy and Traefik handle WebSocket upgrades automatically and need no equivalent stanza.
Without a proxy, on a trusted network only: set [web] bind to 0.0.0.0:8080 or a specific LAN
address, restart the service, and firewall the port to your website's address. The auth token is
always required, but the sidecar speaks HTTP — on that path the token and every event cross the
network in the clear. Do not do this over the public internet.
The [shard] bind line is a different matter entirely: leave it on 127.0.0.1:7788 and never proxy
it. 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:
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:
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 the sidecar — bind address, firewall, or proxy. See §5. Note that the site is designed to render normally with the shard offline, so this fails quietly by design. |
| REST reads work but no live events arrive | The classic reverse-proxy symptom: the WebSocket upgrade is not being forwarded. Confirm the proxy sets Upgrade/Connection and speaks HTTP/1.1 upstream, and that the site's WebSocket URL is wss://…/ws — not https://. |
| The event feed connects, then drops every minute or two | A proxy read timeout below the sidecar's 30 s WebSocket ping interval, or response buffering. Raise the timeout and turn buffering off. |
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. 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)
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
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):
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
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:
sudo UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db \
/usr/bin/runicgateway-link --print-config --config /etc/runicgateway/sidecar.toml
{
"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:
[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
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%:
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 and §6: 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.
[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. 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 | The installer's design of record — phases, locked decisions, the bundle model |
bundles/README.md |
The compat matrix: what a bundle is and how it is composed |
| link/INTEGRATION.md | The sidecar's HTTP/WS API — for anyone integrating something other than the website |
| link/ADMIN_CONTROLS.md | The staff write plane in detail, before you turn AdminWriteEnabled on |
| website/SHARD_VISIBILITY.md | Which shard data each audience sees, configured on the website |
| link/SHARD_PREREQS.md | A worked example of diagnosing a shard whose scripts silently stopped compiling |