Files
docs/modules/rust/README.md
wtclaude 30e72adfcf docs(rust-link): protocol 2 — the read path, and phase 3 as built
The specification the other three repositories are held against, plus the phase
record.

`PROTOCOL.md` §8 is the new contract. Its centre is one field: every frame now
carries `type` — `event`, `snapshot`, `reply`, `control` — and the sidecar files
on that and nothing else. That is the dumb-forwarder property made structural
rather than intended: ten new event kinds are zero change in Rust-Link, and only
a version adding an indexed column touches it at all.

Also in §8: the fifteen-kind catalogue and what each frame carries; `wipeId`
derived by the plugin, which REVERSES §3.2's "deriving one is the website's job"
and says why; boards re-sent on connect and on a cadence; the aggregate rule (a
hook that can fire more than once a second per player is a counter, not an
event); the void rule that stops a read-path hook vetoing a death or a login; and
`GET /feed`, a cursor route separate from `/events` because one route with two
orderings serves the wrong one to every caller that forgets the parameter.

§8.5 is the part to read twice. The classification of a kind as public or staff
is NOT on the wire, deliberately: a boundary declared by the sender is one a
compromised or out-of-date game host can widen, so the module holds a
default-deny allowlist and this table is what its test holds it against.

§8.8 corrects a catalogue rather than a defect: PLAN.md §10 sources
`rust.login.denied` from `CanUserLogin`, and that hook fires on every attempt —
the only way to learn of a denial from it is to be the denier. A denial is the
absence of an approval, and protocol 2 emits both facts so phase 10 can pair them.

`PLAYER_WALK.md` is new, and it exists because half this catalogue cannot fire
without somebody holding a mouse. Ten steps, what each one should produce, and
what counts as a pass — written so the walk can be run without watching the
output live, and so the answer afterwards is readable as a transcript.

PLAN.md §16 is phase 3 as built: the four decisions, the two defects only a
server that BOOTED with the plugin could find (a wipe id that was null for every
real session, and a two-second main-thread stall on unload), what was proven and
how, and — stated plainly rather than implied — the three measurements still
queued on the org lead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-16 08:39:27 -05:00

14 KiB
Raw Blame History

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, 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 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 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 What things are called. 678 items (short name, id, display name) and 2,590 workshop skin ids across 104 items.
OPERATING.md How it gets run. The 6 operator pages — installing Oxide on a server, then installing, configuring and permissioning plugins.
agent/ The same facts in machine shape — TSV and JSONL, ~46% of the tokens. Generated in the same pass, so it cannot drift.
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, and proven on a live Carbon 2.0.259.0 server — R19 at phase 0, and the whole read path at phase 3.

The one file here that is ours: 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 and docs/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). module-uo was the first. A Rust module is the second, and it was already designed once on paper — ../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 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 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 §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 is that rulebook, and 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 sits on top of ServUO.

2. How an Oxide hook actually binds

The one thing to understand before reading 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 reproduces uMod's return line for every hook and it is the load-bearing part:

uMod's wording Code in 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 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.

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/ 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 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, docs/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) 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), 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 for the design, MODULE_API.md for the contract it must satisfy, OXIDE_API.md for the framework we are a guest in, then 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):

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.