The spec for PLAN_FIXES §6 step 2, as built (D181-D185): - PROTOCOL.md §19.4 world.expired's `what` and the website recording an expiry (amends §15's "maps it to nothing"); §19.5 plugin.loaded / plugin.unloaded with the permission diff; §19.6 the ZoneManager helper and `zoneHelper` at hello; §19.7 what the tally counts (F1, F3, F4); §19.8 the website (F7 hold, F5/F6 link fleet, F2 names). - MODULE_API.md 1.11.0 and ctx.events.expired; EVENTS.md §L the `expired` status, terminal and green, and `resource.expired`. - rust-link/INSTALL.md: the helper in the tarball, by hand, and in doctor. - PLAYER_WALK.md: events step 6 can pass now; a step-2 section whose rows 1-2 were walked on both rigs without a player and 3-9 need one. - PLAN_FIXES §6: step 2 as built, with its PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
239 lines
14 KiB
Markdown
239 lines
14 KiB
Markdown
# Installing the Rust bridge
|
|
|
|
This connects **Rust servers** — running Oxide or Carbon — to a Runic Gateway website that has the
|
|
Rust module. It is the Rust counterpart of [`../installer/INSTALL.md`](../installer/INSTALL.md), and
|
|
the design of record is [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §34.
|
|
|
|
## What this installs
|
|
|
|
Two components per Rust server, released together as a **bundle** — an exact pair CI has checked
|
|
speaks one protocol, never "the latest of each":
|
|
|
|
| Component | What it is | Released from |
|
|
|---|---|---|
|
|
| **The plugin** | `RunicGateway.cs`, one file that runs unchanged on Oxide and Carbon — and beside it, from protocol 13, the optional **ZoneManager helper** `RunicGatewayZones.cs` (PLAN_FIXES D181, D182), installed by default | [Rust-Plugins](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins/releases) |
|
|
| **The sidecar** | `rust-link-sidecar`, which the plugin dials on loopback and the website reaches over HTTP | [Rust-Link](https://gitea.whitlocktech.com/RunicGateway/Rust-Link/releases) |
|
|
|
|
The game server opens no port for the bridge: the plugin is the client and the sidecar the
|
|
listener, on `127.0.0.1`. **One sidecar serves one Rust server.** A community running six servers
|
|
runs six sidecars, each with its own port, database and token, and the website holds six entries.
|
|
|
|
There are three ways to set a server up. They produce the same result:
|
|
|
|
| | Use it when |
|
|
|---|---|
|
|
| [**A. The installer**](#a-the-installer) | The server runs on a Linux host with systemd, or on Windows |
|
|
| [**B. The Pterodactyl egg**](#b-the-pterodactyl-egg) | The server runs on a Pterodactyl panel |
|
|
| [**C. By hand**](#c-by-hand) | Neither fits, or you want to place every file yourself |
|
|
|
|
## Before you begin
|
|
|
|
- **Oxide or Carbon is installed, and the server has started once** with it, so its directories
|
|
exist. The bridge is a plugin; a vanilla server has nothing to load it.
|
|
- **Kits and ZoneManager** from uMod are what the reward and zone features use. The bridge works
|
|
without them and names what is missing; nothing here installs them.
|
|
- **The website has the Rust module**, and you are an administrator on it. Servers are added at
|
|
**Admin → Rust → Servers** (`/admin/rust/servers`).
|
|
|
|
> **Until module-rust phase 19**, the released Rust module (Module-Rust v0.3.0) speaks protocol 2
|
|
> while the released bridge speaks 12, and a site pairing them answers `409`. This is expected and
|
|
> closes with phase 19's release; it is not a fault in your install.
|
|
|
|
---
|
|
|
|
## A. The installer
|
|
|
|
The [Runic Gateway installer](https://gitea.whitlocktech.com/RunicGateway/installer/releases) —
|
|
the same binary that sets up ServUO shards — with `--game rust`. Run it as root, or from an
|
|
elevated PowerShell:
|
|
|
|
```bash
|
|
sudo ./runicgateway-installer-linux-x86_64 install --game rust --rust /srv/rust --server-id main
|
|
```
|
|
|
|
```powershell
|
|
.\runicgateway-installer-windows-x86_64.exe install --game rust --rust D:\rust --server-id main
|
|
```
|
|
|
|
| Flag | |
|
|
|---|---|
|
|
| `--rust <path>` | The server root: the directory holding `RustDedicated` |
|
|
| `--server-id <id>` | **Required.** The id the website will know this server by: lowercase letters, digits and `-`, up to 64. It names this server's sidecar, service and files |
|
|
| `--web-port <port>` | The port the website reaches this sidecar on. Default: the first free from 8090 |
|
|
| `--bundle <tag>` | Install an exact published bundle instead of the current one |
|
|
| `--verify` | Report everything it would do, and write nothing |
|
|
| `--host <name>` | The hostname to print in the sidecar URL |
|
|
| `--site-url <url>` | Your site, so the printed link opens the right page |
|
|
|
|
What a run does:
|
|
|
|
1. **Resolves the bundle** for Rust and checks the plugin's own manifest against it.
|
|
2. **Detects the framework** — Oxide or Carbon — from the server itself. A tree with both is
|
|
refused (they cannot run together); one with neither is refused with what to install.
|
|
3. **Writes the plugin's config** (`oxide/config/RunicGateway.json` or
|
|
`carbon/configs/RunicGateway.json`) holding just `ServerId` and `Port`, **only if there is none**.
|
|
The plugin fills in every other key on its first load. An existing config is never rewritten,
|
|
and one that names a different server stops the run and says so — the website locks a server's
|
|
id once it has seen it.
|
|
4. **Installs the sidecar**, writes its config with this server's ports, and registers its service.
|
|
5. **Places the plugin** in `oxide/plugins/` or `carbon/plugins/`. Both frameworks load a plugin
|
|
the moment it lands, so on a running server the bridge comes up at once; on a stopped one, at
|
|
the next boot. The installer never starts or stops your server.
|
|
6. **Prints what the website needs.**
|
|
|
|
### More than one server on a host
|
|
|
|
Run it once per server, each with its own `--rust` and `--server-id`. Each gets its own service,
|
|
config, database, game port (from 7799) and web port (from 8090). They share one sidecar binary, so
|
|
`update` moves them all together.
|
|
|
|
### Where everything lands
|
|
|
|
| | Linux | Windows |
|
|
|---|---|---|
|
|
| The sidecar binary (shared) | `/usr/bin/runicgateway-rust-link` | `%ProgramFiles%\RunicGateway\rust-link-sidecar.exe` |
|
|
| One server's sidecar config — **holds its token** | `/etc/runicgateway/rust/<id>.toml` (mode 600) | `%ProgramData%\RunicGateway\rust\<id>\sidecar.toml` |
|
|
| One server's database | `/var/lib/runicgateway/rust/<id>/rust-link.db` | beside its config |
|
|
| One server's service | `runicgateway-rust@<id>.service` | `RunicGatewayRust-<id>`, as `NT SERVICE\RunicGatewayRust-<id>` |
|
|
| The record | `/etc/runicgateway/rust/install.json` | `%ProgramData%\RunicGateway\rust\install.json` |
|
|
|
|
The Rust record is separate from ServUO's, so a host running both games keeps two records that
|
|
cannot interfere. The token is in the sidecar config and nowhere else — not in the record.
|
|
|
|
---
|
|
|
|
## B. The Pterodactyl egg
|
|
|
|
Each Rust-Link release carries **`egg-rust-runicgateway.json`**: egg 18 "Rust Autowipe" with the
|
|
bridge added. A panel administrator imports it (**Admin → Nests → Import Egg**); the panel has no
|
|
API for writing an egg.
|
|
|
|
When you create a server from it:
|
|
|
|
| Variable | |
|
|
|---|---|
|
|
| **Modding Framework** (`FRAMEWORK`) | `oxide` or `carbon`. `vanilla` installs no bridge and the server boots exactly as egg 18's |
|
|
| **Runic Gateway: server id** (`RUSTLINK_SERVER_ID`) | The id on the website. Read **once**, when the plugin writes its first config; after that the file holds it and the site locks it, so changing the variable later changes nothing — the launcher hands the sidecar the id from the plugin's config and says so in the console |
|
|
| **Runic Gateway: sidecar port** (`RUSTLINK_WEB_PORT`) | **One of this server's allocations.** The panel does not tell a server which ports it holds, so a port that is not allocated binds but is never reachable |
|
|
| **Runic Gateway: sidecar token** (`RUSTLINK_WEB_TOKEN`) | Leave blank: the sidecar generates one. Anyone who can see this server's startup variables can read a token typed here |
|
|
| **Runic Gateway: bundle** (`RUNICGATEWAY_BUNDLE`) | Blank takes the current bundle. A tag pins one |
|
|
| **Runic Gateway: history days** (`RUSTLINK_RETAIN_DAYS`) | Blank keeps the default |
|
|
|
|
**The install** downloads the sidecar, its launcher and the plugin from the bundle, checks each
|
|
against the bundle's checksum, and only then places them: the sidecar in `rust-link/`, the plugin in
|
|
`oxide/plugins/` or `carbon/plugins/`. Any mismatch fails the install with the reason, before
|
|
anything is placed.
|
|
|
|
**The first boot** prints, in the console, the token the sidecar generated — **once** — and a line
|
|
with what the website needs:
|
|
|
|
```
|
|
[rust-link] A new sidecar token was generated. It is shown ONCE, here:
|
|
[rust-link] 4f9c…
|
|
[rust-link] It is kept in rust-link/sidecar.toml; read it there if you lose it.
|
|
[rust-link] Admin -> Rust -> Servers: server id 'main', sidecar URL http://<this node's address>:21009 (listening on 0.0.0.0:21009)
|
|
```
|
|
|
|
Every later boot prints only the second line. The sidecar runs inside the server's container
|
|
and stops when the server stops.
|
|
|
|
**Updating is a reinstall.** The bridge is fetched at install and reinstall only, never on a
|
|
restart, so a restart can never change the protocol under a website that has not been updated.
|
|
Reinstall with **Regen Server** off to take the current bundle (or the pinned one) and keep the map.
|
|
|
|
**A wipe keeps the bridge's history.** With Regen Server on, the install script deletes
|
|
`REMOVE_FILES` — and moves `rust-link/` out of the way while it does, so no list, wildcards
|
|
included, can reach the database or the token. After a wipe the website still has the server's
|
|
history, and the token is unchanged.
|
|
|
|
---
|
|
|
|
## C. By hand
|
|
|
|
1. **Pick the bundle** — the current one is
|
|
[`v2/rust/current.json`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/bundles/v2/rust/current.json)
|
|
on the installer's `bundles` branch. It names the sidecar binary for your platform and the
|
|
plugin tarball, each with a `sha256`.
|
|
2. **Download and verify** both against those checksums (`sha256sum -c`, or `Get-FileHash`).
|
|
3. **The plugin:** copy `runicgateway-rust-plugin/RunicGateway.cs` from the tarball into
|
|
`oxide/plugins/` or `carbon/plugins/`, and every other `.cs` the tarball's `manifest.json` lists
|
|
in `files` beside it — from protocol 13 that is `RunicGatewayZones.cs`, which lets ZoneManager count a
|
|
player already standing in a zone when it opens or comes back after a restart. It is optional: leave
|
|
it out and events still score everybody, but ZoneManager's own flags miss that player. To name the server, create its config first —
|
|
`oxide/config/RunicGateway.json` or `carbon/configs/RunicGateway.json` — holding
|
|
`{ "ServerId": "<id>", "Port": 7799 }`. Without it, the plugin calls the server `main`.
|
|
4. **The sidecar:** put the binary somewhere stable and give it a config:
|
|
|
|
```toml
|
|
[game]
|
|
bind = '127.0.0.1:7799' # the plugin's Port
|
|
server_id = '<id>'
|
|
|
|
[web]
|
|
bind = '127.0.0.1:8090'
|
|
auth_token = "" # generated and saved on first start
|
|
|
|
[store]
|
|
path = '/var/lib/rust-link/<id>/rust-link.db'
|
|
```
|
|
|
|
`rust-link-sidecar --print-config --config <file>` generates the token, saves it, and prints it.
|
|
5. **Run it** under your supervisor of choice with `--config <file>`, as an unprivileged user that
|
|
can write the database directory. On Windows the same `.exe` runs as a service
|
|
(`sc.exe create … binPath= "\"<exe>\" --config \"<file>\""`); it speaks the service handshake
|
|
itself.
|
|
|
|
Every environment variable the sidecar reads (`RUSTLINK_CONFIG`, `RUSTLINK_WEB_BIND`,
|
|
`RUSTLINK_WEB_TOKEN`, …) overrides the file; an empty one is ignored.
|
|
|
|
---
|
|
|
|
## Connect the website
|
|
|
|
On your site, open **Admin → Rust → Servers** and add the server:
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Id | the server id — the installer's `--server-id`, or the egg's variable |
|
|
| Name | anything; players see it |
|
|
| Sidecar address | the sidecar URL the installer or the console printed |
|
|
| Sidecar token | the token |
|
|
|
|
Test it from the same page: it should answer *plugin connected* with the protocol.
|
|
|
|
### If the website is on another machine
|
|
|
|
The installer's sidecar listens on `127.0.0.1`, reachable only from the game host. Put a TLS
|
|
reverse proxy (or a VPN link) in front of it and give the website the proxy's URL — the token is a
|
|
plain bearer token, and the sidecar speaks HTTP. The egg's sidecar listens on its allocation,
|
|
which is reachable from the network; firewall it to the website's address. **Leave the game bind
|
|
on loopback either way**: that socket takes commands for the game server and has no token, and
|
|
being loopback-only is what makes that safe.
|
|
|
|
---
|
|
|
|
## Day two
|
|
|
|
With the installer:
|
|
|
|
| | |
|
|
|---|---|
|
|
| `doctor --game rust [--server-id <id>]` | Per server: the framework; whether the plugin file is still the one deployed; each helper deployed beside it, as a **warning** when missing or edited (the bridge runs without one, and the row says what that costs); that the plugin's config names this server; the required uMod plugins; the service; and `/health` through to **plugin connected**. A stopped server is a warning; a running one whose plugin never connected is a failure, printed with the framework versions the plugin is known good on |
|
|
| `update --game rust` | Moves the sidecar and every server's plugin to the current bundle, and restarts the sidecars. Always all servers together — they share one binary |
|
|
| `uninstall --game rust [--server-id <id>] [--purge]` | Removes the service and the plugin file. **Keeps the plugin's config** — it is the website's, and it names the server. `--purge` also removes the sidecar config (the token) and the database. Removing the last server removes the shared binary too |
|
|
|
|
With the egg: reinstall to update (above); the console is the diagnosis.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause |
|
|
|---|---|
|
|
| The install stops: *already names this server "x"* | The plugin's config names another server. Use that id, or — if the server really is new to the site — remove the config file |
|
|
| The install stops: *has both Oxide and Carbon* | A tree mid-migration. Remove the framework you are leaving |
|
|
| The site says the sidecar is unreachable | The website cannot reach the URL: a loopback bind with the site elsewhere (see above), a firewall, or — on the egg — a sidecar port that is not one of the server's allocations |
|
|
| The site answers `409` | The site's Rust module speaks another protocol than the bridge. Update whichever is behind (and see the note in *Before you begin*) |
|
|
| `doctor`: *plugin connected — no, and the server is running* | The plugin did not compile or cannot reach its sidecar. The server console names a compile error; the plugin's `Port` must equal the sidecar's game port |
|
|
| The sidecar logs *refusing a plugin that names another server* | Two servers dial one sidecar: a plugin's `Port` is another server's game port. Correct that plugin's `Port` (in its config) and reload it. Nothing it sent was stored |
|
|
| `doctor`: *not the file that was deployed* | The plugin was edited by hand. `update --game rust` puts the released one back |
|
|
| The egg's console prints a new token after every boot | Only a sidecar older than the first Rust-Link release does this; reinstall the server |
|