All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 12s
The `clans` board carries every clan the game will list, with its roster and roles, plus the facts the website needs to trust it: enabled, backend, supported, truncated, and whether the optional uMod Clans plugin is loaded. It is re-sent on connect and on the 60-second cadence, and a few seconds after any membership change. The five events are clan.created, clan.disbanded, clan.member.added, clan.member.left and clan.member.kicked. What the game's assemblies dictated: - The game has no enumerate-all call. The clan leaderboard is the only listing, and the game clamps it to 100 clans. A board at that ceiling says `truncated`, as does one that would not fit in 768 KiB (the sidecar drops lines over 1 MiB). - The local backend is synchronous behind ValueTask. A task that has not completed means another backend (Nexus), which is refused with a reason rather than waited on from the main thread. - A clan id is not an identity, so every frame carries createdMs. - OnClanMemberAdded hands over a bare id before the game's cache refreshes, so the clan is read back, with a fallback to the id alone. - uMod Clans raises OnClanDisbanded(string, List<ulong>), which has the same name and arity as the game's hook, so the bridge types it exactly. A member's LastSeen is never sent: it is presence (PLAN.md §23). overlay.toml moves to protocol 6 and drops Clans from requires_plugins (D47): Teams come from the game's own clans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
105 lines
4.4 KiB
Markdown
105 lines
4.4 KiB
Markdown
# Rust-Plugins
|
|
|
|
The **in-game half** of the Runic Gateway bridge for [Rust](https://rust.facepunch.com/): one Oxide
|
|
plugin that dials out to a [rust-link](https://gitea.whitlocktech.com/RunicGateway/Rust-Link)
|
|
sidecar and speaks newline-delimited JSON over it.
|
|
|
|
It is the mirror of
|
|
[`RunicGateway/servuo-plugins`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins), which
|
|
does the same job for Ultima Online — and it inherits that plugin's threading contract wholesale,
|
|
because the reason for it is the same on both games.
|
|
|
|
## The threading contract
|
|
|
|
Everything else in this repo depends on these three:
|
|
|
|
- **`Emit` is called from the main thread. It formats nothing, blocks on nothing, and touches no
|
|
socket.** It enqueues and returns. A slow, wedged, or absent sidecar cannot stall the game.
|
|
- **One link thread owns the socket.** It connects, drains the queue, and reconnects with backoff.
|
|
A single writer keeps event ordering intact.
|
|
- **A reader thread parses inbound lines and hands each to the main thread** via
|
|
`Interface.Oxide.NextTick`. The reader touches no Unity object, no `BasePlayer` and no `ConVar` —
|
|
every one of those is main-thread-only, and reading one from the reader is the kind of bug that
|
|
presents as a crash somewhere else entirely.
|
|
|
|
The outbound queue is **bounded, drop-oldest**. On overflow the oldest record goes and is counted,
|
|
because telemetry is worth less than the server's memory.
|
|
|
|
## Loopback is the trust boundary
|
|
|
|
There is no token on the game link. The plugin and the sidecar share a host, and the sidecar binds
|
|
`127.0.0.1` — that is the authentication. Pointing `Host` at anything routable puts an
|
|
unauthenticated command channel on the network.
|
|
|
|
## Installing it
|
|
|
|
```
|
|
overlay/oxide/plugins/RunicGateway.cs → <server>/oxide/plugins/RunicGateway.cs
|
|
```
|
|
|
|
Oxide compiles and loads it on the write, and writes `oxide/config/RunicGateway.json` on first load:
|
|
|
|
```json
|
|
{
|
|
"Host": "127.0.0.1",
|
|
"Port": 7799,
|
|
"QueueCap": 5000,
|
|
"ServerId": "main"
|
|
}
|
|
```
|
|
|
|
`ServerId` is this server's stable identity across wipes and restarts, as the website knows it. It
|
|
is deliberately **not** derived from the hostname: an operator renames a server for a season, and
|
|
the site must not lose its history for it.
|
|
|
|
That is the developer's loop. An operator uses the
|
|
[installer](https://gitea.whitlocktech.com/RunicGateway/installer), which syncs the released overlay
|
|
tarball and installs the sidecar alongside it.
|
|
|
|
### Prerequisites
|
|
|
|
The bridge itself needs nothing but Oxide. The features that follow it read three third-party
|
|
plugins an operator installs from uMod — `Kits`, `PopupNotifications` and `ZoneManager`. They are
|
|
listed in `overlay.toml` so the installer's `doctor` can report a missing one by name rather than
|
|
leaving the site quietly short of a feature.
|
|
|
|
Clans on the website come from Rust's **own** clan system, not from the uMod `Clans` plugin, which
|
|
is optional. The two share no data: a server running the plugin has two unrelated clan systems, and
|
|
only the game's own clans become Teams on the site.
|
|
|
|
## Diagnosing it
|
|
|
|
```
|
|
rg.link
|
|
```
|
|
|
|
from the server console or over RCON. It reports the link's own counters:
|
|
|
|
```
|
|
protocol=1 serverId=main connected=True depth=0 sent=3 dropped=0 received=2
|
|
connects=1 writeErrors=0 bootId=boot-20260915T194502Z
|
|
```
|
|
|
|
This is the first thing to ask for when the website says a server is offline — it separates "the
|
|
plugin is not loaded", "the plugin cannot reach the sidecar" and "the website cannot reach the
|
|
sidecar", which look identical from the site.
|
|
|
|
**`bootId` identifies the server PROCESS, not the plugin load.** It is the process start time, so
|
|
`oxide.reload RunicGateway` does not change it. That matters more than it looks: the website watches
|
|
this value to tell a game restart (everything an event put in the world is gone) from a bridge
|
|
reconnect (nothing is lost), and a plugin reload is the second kind.
|
|
|
|
## The protocol is a contract
|
|
|
|
`ProtocolVersion` in the plugin and `protocol` in `overlay.toml` must agree with the sidecar's
|
|
`PROTOCOL_VERSION` and the module's own constant. The installer refuses to pair an overlay and a
|
|
sidecar that disagree, so a bump landing in one repo and not the others fails to compose rather than
|
|
half-deploying.
|
|
|
|
The canonical spec is
|
|
[`docs/rust-link/PROTOCOL.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/rust-link/PROTOCOL.md).
|
|
|
|
## Licence
|
|
|
|
GPL-3.0-or-later. See [LICENSE.md](LICENSE.md).
|