Three new decisions of record, and a new reference for the second modding framework. R19 - the bridge plugin is framework-agnostic from now, not ported later. Carbon is not a fork of Oxide but a separate loader shipping an Oxide compatibility layer, so one .cs in the Oxide.Plugins namespace serves both, with #if CARBON only where the APIs genuinely differ. Three existing decisions take an amendment and none is reversed: R18's config walk roots at Interface.Oxide.ConfigDirectory rather than a literal oxide/config (Carbon uses carbon/configs AND lets an operator relocate every directory from the command line); R2's permission store is Protobuf or SQLite on Carbon, which permanently closes the file-reading shortcut it never planned to take, while the PermissionExists pre-check survives because Carbon's bool return is the one thing we cannot read portably; R4's doctor asks which framework rather than whether Oxide, and gets a weaker "current enough" claim because Carbon ships rolling release tags. R20 - a Pterodactyl egg is a third supported deployment path beside the installer and the hand install, derived from the community "Rust Autowipe" egg, which already carries a FRAMEWORK variable offering vanilla/carbon/oxide. The sidecar runs inside the game's container, which is what lets D2 stand unchanged: a container's 127.0.0.1 is genuinely private, so the game link stays loopback and stays unauthenticated. Lands in phase 18 beside the installer. R21 - both rigs move to the Pterodactyl panel, because Oxide and Carbon cannot coexist in one install and so a single server cannot prove R19. Also retires the wipe-day maintenance that dominated section 4, and makes the rig Linux where every prior finding came from Windows and Mono. New: modules/rust/CARBON.md, the difference list - file layout, the permission store, the c. commands, 30 Carbon-only hooks, and 13 uMod names Carbon's catalogue omits (at least two of which look like renames). Sourced from Carbon's own published metadata and source at main, and labelled throughout as not yet proven on a live Carbon server. One outstanding request, recorded in section 3: the panel token on disk is an application key and Pterodactyl puts files, power and console on the client API, so iteration needs a ptlc_ key only the account holder can mint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
252 lines
14 KiB
Markdown
252 lines
14 KiB
Markdown
# Rust — the modding-framework reference
|
||
|
||
Reference material for **`module-rust`**: a mirror of the uMod/Oxide documentation — the Rust game
|
||
API *and* the game-independent plugin framework around it — captured here so the module can be
|
||
designed and built against it without a round trip to umod.org on every question.
|
||
|
||
The mirrored material was **scraped verbatim from uMod on 2026-09-15**. One file,
|
||
[`CARBON.md`](CARBON.md), covers the *other* framework modded Rust servers run: PLAN.md **R19**
|
||
commits this module to supporting Oxide and Carbon both, and that file records only where the two
|
||
differ.
|
||
|
||
## The mirror
|
||
|
||
| Doc | What it holds |
|
||
|---|---|
|
||
| [`HOOKS.md`](HOOKS.md) | **What Rust will tell you.** All **477 hooks** in 20 categories — description, return contract, tags, every C# overload. The 34 *universal* hooks are marked. |
|
||
| [`OXIDE_API.md`](OXIDE_API.md) | **How a plugin is built.** The 19 developer pages — plugin structure, hooks, commands, `IPlayer`, permissions, config, data files, database, localization, timers, web requests, dependencies, CI, review. |
|
||
| [`DEFINITIONS.md`](DEFINITIONS.md) | **What things are called.** 678 items (short name, id, display name) and 2,590 workshop skin ids across 104 items. |
|
||
| [`OPERATING.md`](OPERATING.md) | **How it gets run.** The 6 operator pages — installing Oxide on a server, then installing, configuring and permissioning plugins. |
|
||
| [`agent/`](agent/README.md) | The same facts in **machine shape** — TSV and JSONL, ~46% of the tokens. Generated in the same pass, so it cannot drift. |
|
||
| [`CARBON.md`](CARBON.md) | **The other framework.** Where Carbon diverges from Oxide and nowhere else — file layout, the permission store, the `c.` commands, 30 Carbon-only hooks and 13 uMod names its catalogue omits. Sourced from Carbon's own metadata and source, **not yet proven on a live Carbon server.** |
|
||
|
||
**The one file here that is ours:** [`PLAN.md`](PLAN.md) — the schedule and the decisions of record
|
||
for actually building `module-rust`. Everything else in this directory is copied from uMod; that one
|
||
is written by this project and is where the phases, the settled decisions and the local test rig are
|
||
recorded.
|
||
|
||
> **This is a mirror, not a specification we own.** uMod is upstream and wins any disagreement; the
|
||
> point of copying it is availability and grep-ability, not authority. Nothing here may be cited as a
|
||
> Runic Gateway contract — our contracts are [`MODULE_API.md`](../../website/MODULE_API.md) and
|
||
> [`docs/link/`](../../link/).
|
||
|
||
---
|
||
|
||
## 1. Why this exists
|
||
|
||
The website core is game-agnostic; a **module** is what makes it a site for one particular game
|
||
([`MODULE_SYSTEM.md`](../../website/MODULE_SYSTEM.md)). `module-uo` was the first. A Rust module is
|
||
the second, and it was already designed once on paper —
|
||
[`../rust-dryrun.md`](../rust-dryrun.md) is that dry run, written deliberately *without*
|
||
implementing it, to find out whether the module contract generalises past Ultima Online.
|
||
|
||
The dry run's central structural fact is the thing this reference serves:
|
||
|
||
> A ServUO shard is C# **source** the operator compiles into their own server, so our bridge plugin
|
||
> can be anything we want. **A Rust server is a binary nobody outside Facepunch patches.** The only
|
||
> way in is a mod — hooking the game's own events through a modding framework.
|
||
|
||
The dry run named that framework as Oxide, and **R19 corrected it: there are two.** Carbon runs an
|
||
Oxide compatibility layer, so one plugin serves both and the ceiling below is the same ceiling —
|
||
but *which* framework an operator installed is their choice, not ours. [`CARBON.md`](CARBON.md) is
|
||
the difference list.
|
||
|
||
Two consequences, and they are the two halves of this directory:
|
||
|
||
1. **We can only emit what Oxide already hands us.** [`HOOKS.md`](HOOKS.md) is the hard ceiling on
|
||
what a Rust module can ever know about a live server. If a fact is not reachable from one of
|
||
those 477 hooks (or from a game type one of them hands you), the bridge cannot report it. That
|
||
makes it the input to the Rust sidecar's event catalogue — the analogue of
|
||
[`docs/link/PLAN.md`](../../link/PLAN.md) §5 on the UO side.
|
||
2. **We are a guest in someone else's plugin framework** — and we do not get to pick which one. Our
|
||
plugin is compiled, loaded, permissioned and configured by Oxide or by Carbon, on its terms. [`OXIDE_API.md`](OXIDE_API.md) is that rulebook, and
|
||
[`OPERATING.md`](OPERATING.md) is what the server owner has to do — which is the surface our
|
||
deployment story has to sit on, the way
|
||
[`installer/INSTALL.md`](../../installer/INSTALL.md) sits on top of ServUO.
|
||
|
||
---
|
||
|
||
## 2. How an Oxide hook actually binds
|
||
|
||
The one thing to understand before reading [`HOOKS.md`](HOOKS.md), because it is not in the table
|
||
itself:
|
||
|
||
**Oxide binds hooks by name and arity, by reflection, at runtime.** There is no interface to
|
||
implement and no compile-time check. A method whose name is misspelled, or whose parameter types do
|
||
not match, is simply **never called** — silently, with no warning at load. This is the single most
|
||
common way a Rust plugin "does nothing".
|
||
|
||
The consequence for us: **every hook name a module relies on is an untyped string constant against a
|
||
moving upstream.** Facepunch renames and removes hooks on wipes. A Rust sidecar should log which of
|
||
its expected hooks have actually fired at least once, so a hook that quietly stopped existing is
|
||
visible rather than mysterious.
|
||
|
||
### The return contract
|
||
|
||
[`HOOKS.md`](HOOKS.md) reproduces uMod's return line for every hook and it is the load-bearing part:
|
||
|
||
| uMod's wording | Code in [`agent/hooks.tsv`](agent/hooks.tsv) | What it means |
|
||
|---|---|---|
|
||
| *No return behavior* | `none` | A **notification**. Declare `void`. Nothing you return is read. |
|
||
| *Returning a non-null value overrides default behavior* | `nonnull` | A **veto with a payload**. Declare `object`; return `null` to let the game proceed, anything else to cancel it. |
|
||
| *Returning true or false overrides default behavior* | `bool` | A **veto**. Declare `object`; `null` abstains — returning `false` is *not* the same as abstaining. |
|
||
| *Returning a string will kick…* | `data` | The value is **consumed as data**, not merely as a veto. |
|
||
|
||
Two cautions that apply across the whole table:
|
||
|
||
- Never return non-`null` from a hook documented as *no return behavior*; on some hooks Oxide will
|
||
still read it as a veto.
|
||
- When several plugins hook the same veto, the **first non-`null` return wins** and the rest are not
|
||
consulted. Abstain with `null` unless you mean to decide. A bridge plugin should be a **reader**,
|
||
and must return `null` from every veto hook it listens on — see §5.
|
||
|
||
Three hooks (`OnEntityTakeDamage`, `OnExcavatorSuppliesRequested`, `OnPhoneCallStarted`) state no
|
||
return behaviour upstream at all; both the markdown and the TSV mark those *(not stated upstream)* /
|
||
`?` rather than guessing.
|
||
|
||
---
|
||
|
||
## 3. What is in the 477
|
||
|
||
| Category | Hooks | What it covers |
|
||
|---|---:|---|
|
||
| Server | 17 | Process lifecycle, tick/frame, save and restart, RCON and console commands |
|
||
| Player | 138 | Connect, chat, command, craft, loot, build, die, respawn |
|
||
| Entity | 140 | The generic `BaseEntity` graph — spawn, kill, damage, mount, loot, flags |
|
||
| Item | 43 | Item stacks and containers |
|
||
| Vehicle | 22 | Boats, cars, horses, helicopters, trains, submarines |
|
||
| Vending | 16 | Vending machines, the shop/trade flow, marketplace and drone deliveries |
|
||
| Weapon | 16 | Firing, reloading, throwing, melee, projectiles and traps |
|
||
| Structure | 15 | Placement, upgrade, demolish, stability, doors and locks |
|
||
| Resource | 12 | Gathering and node dispensers |
|
||
| Team | 12 | Rust's built-in team/party system |
|
||
| Phone | 12 | In-game telephones |
|
||
| Permission | 8 | uMod's own permission/group system (Covalence) |
|
||
| Clan | 7 | The first-party clan system, distinct from Team |
|
||
| Fishing · Plugin · TechTree | 4 each | |
|
||
| Sign | 3 | |
|
||
| Electronic | 2 | |
|
||
| Terrain · World | 1 each | |
|
||
|
||
**Player and Entity are 58% of the surface between them.** [`HOOKS.md`](HOOKS.md) opens with a full
|
||
alphabetical index (name → category → universal → return contract), which is the fastest way in when
|
||
you already know the hook's name.
|
||
|
||
### Universal vs. Rust-specific
|
||
|
||
**34 of the 477 are not Rust's.** They are uMod's own *universal* (Covalence) hooks — `Init`,
|
||
`Loaded`, `Unload`, `OnUserConnected`, the whole `OnGroup*`/`OnUserPermission*` family — which uMod
|
||
raises identically on every game it supports. They appear on the Rust page because they fire on Rust
|
||
too. Verified against <https://umod.org/documentation/games/universal> in the same capture: all 34
|
||
are in the Rust set, and the Rust page adds none of its own, so the overlap is exact.
|
||
|
||
The distinction is architectural, not trivia: **a universal hook is the portable part of the
|
||
surface.** Code written against one would carry to any other uMod-supported game; code written
|
||
against a Rust-specific hook would not. They are marked in `HOOKS.md` and flagged `1` in
|
||
[`agent/hooks.tsv`](agent/hooks.tsv).
|
||
|
||
Two notes on the data, both faithful to upstream:
|
||
|
||
- `OnShopCompleteTrade` appears **twice** — two separate records. Both are kept; the second carries
|
||
the anchor `#onshopcompletetrade-1`.
|
||
- Twelve hooks document **more than one overload** (e.g. `CanUpdateSign` takes either a `Signage` or
|
||
a `PhotoFrame`). Every code block is reproduced.
|
||
|
||
---
|
||
|
||
## 4. Reading the machine copy
|
||
|
||
[`agent/`](agent/README.md) holds the same facts as TSV and JSONL, at ~46% of the tokens — a real
|
||
3.2× saving on the hooks, 2.1× on the API prose, and only ~1.25× on the item and skin tables, which
|
||
were already dense. Reach for it when you want to *find* something; read the markdown when you want
|
||
the reasoning, which is deliberately not in there. [`agent/README.md`](agent/README.md) has the
|
||
column definitions and an honest account of where the saving is and is not.
|
||
|
||
---
|
||
|
||
## 5. Where this meets our architecture
|
||
|
||
The bridge invariants the platform already holds ([`ARCHITECTURE.md`](../../website/ARCHITECTURE.md),
|
||
[`docs/link/PLAN.md`](../../link/PLAN.md)) are game-independent, and a Rust module inherits all of
|
||
them. Restated against Oxide:
|
||
|
||
- **The game server is never network-reachable.** The Rust sidecar listens; the Oxide plugin dials
|
||
out to it, exactly as `BridgeLink.cs` does on the UO side.
|
||
- **A wedged or absent sidecar must never stall the server.** Oxide hooks run on Rust's main thread.
|
||
An emit must be an enqueue onto a bounded drop-oldest queue that returns immediately — never a
|
||
socket write, never a blocking call, and never anything expensive inside `OnTick`/`OnFrame`.
|
||
Oxide's own `timer` and `NextFrame` helpers ([`OXIDE_API.md` § Timers](OXIDE_API.md#timers)) are
|
||
the tools for deferring work off a hot hook.
|
||
- **The bridge plugin decides nothing.** It returns `null` from every veto hook it subscribes to.
|
||
Access control and audience scoping live on the website
|
||
([`SHARD_VISIBILITY.md`](../../website/SHARD_VISIBILITY.md)), not in the game plugin — the same
|
||
rule that makes the uo-link sidecar a dumb forwarder.
|
||
- **Sensitive events never reach the public stream.** Rust raises plenty that must not: `CanUserLogin`
|
||
and `OnUserApproved` carry IP addresses, `OnPlayerReported` carries player reports. These belong on
|
||
the admin channel only.
|
||
|
||
Reading order for someone picking the module up: [`../rust-dryrun.md`](../rust-dryrun.md) for the
|
||
design, [`MODULE_API.md`](../../website/MODULE_API.md) for the contract it must satisfy,
|
||
[`OXIDE_API.md`](OXIDE_API.md) for the framework we are a guest in, then [`HOOKS.md`](HOOKS.md) for
|
||
what the game can actually tell us.
|
||
|
||
---
|
||
|
||
## 6. Provenance and refreshing
|
||
|
||
| | |
|
||
|---|---|
|
||
| Captured | **2026-09-15** |
|
||
| Hooks | <https://umod.org/documentation/games/rust> · <https://umod.org/documentation/games/universal> |
|
||
| Definitions | <https://umod.org/documentation/games/rust/definitions> |
|
||
| Developer API | <https://umod.org/documentation/api/> — 19 pages |
|
||
| Operator docs | <https://umod.org/documentation/> — 6 pages |
|
||
| Content | 477 hooks (34 universal) · 678 items · 2,590 skins · 25 prose pages · 150 code examples |
|
||
|
||
**Rust wipes monthly and the hook list moves with it.** Re-capture before any significant work on
|
||
the module, and update the date in every file — they all carry it.
|
||
|
||
### How to capture it
|
||
|
||
Three different routes, because uMod serves these three ways. Cloudflare gates the HTML pages but
|
||
**not** the JSON endpoints:
|
||
|
||
**1. Hooks — a plain `curl`, no browser needed.** The page renders client-side from an endpoint that
|
||
is not gated. One request returns everything (`per_page` is 9999, so there is no paging):
|
||
|
||
```bash
|
||
curl -A "Mozilla/5.0" -H "Accept: application/json" \
|
||
"https://umod.org/documentation/hooks/rust.json" -o all_rust_hooks.json
|
||
curl -A "Mozilla/5.0" -H "Accept: application/json" \
|
||
"https://umod.org/documentation/hooks/universal.json" -o all_universal_hooks.json
|
||
```
|
||
|
||
Each record carries `name`, `subcategory`, `tags`, `description` (HTML `<ul>`) and `example` (a
|
||
markdown-fenced C# block). A `?subcategory=Server` query narrows it.
|
||
|
||
**2. Definitions — a real browser.** No JSON endpoint, and the HTML *is* gated. Load
|
||
`/documentation/games/rust/definitions`, then read the `Rust Items` table and the per-item
|
||
`Rust Skins` tables out of the rendered DOM.
|
||
|
||
**3. The prose pages — a real browser, throttled.** Also gated. Fetch each page same-origin from an
|
||
already-loaded uMod tab and take `document.querySelector('.documentation')`, dropping the
|
||
`.table-of-contents` child. **Pause ~1s between pages** — uMod returns `429 Too Many Requests` on a
|
||
tight loop, and a 429 is easy to mistake for a real page because it still returns 200-shaped HTML
|
||
with a title.
|
||
|
||
The pages mirrored here, in the order they appear:
|
||
|
||
- `api/` — overview, getting-started, hooks, commands, player, permissions, configuration,
|
||
data-files, database, localization, timers, web-requests, dependencies, integration,
|
||
preprocessor-directives, security, style-guide, continuous-integration, approval-guide
|
||
- operator — getting-started, plugins/getting-started, plugins/installation, plugins/configuration,
|
||
plugins/data-files, plugins/permissions
|
||
|
||
### Deliberately not mirrored
|
||
|
||
- **The other games** uMod supports (Hurtworld, 7 Days to Die, Reign of Kings, The Forest). Not our
|
||
ecosystem. `api/preprocessor-directives` lists their compile symbols if that ever changes.
|
||
- **Community pages** — contributing, community guidelines, reporting issues, getting help. Process,
|
||
not API.
|
||
- **The `/documentation/umod/*` paths**, which are aliases of the same pages.
|