docs(modules): module-rust supports Carbon too, and ships a Pterodactyl egg

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
This commit is contained in:
2026-09-15 20:59:23 -05:00
parent b4b05b4108
commit 5890da633f
3 changed files with 551 additions and 58 deletions

307
modules/rust/CARBON.md Normal file
View File

@@ -0,0 +1,307 @@
# Carbon — the second modding framework, and where it differs from Oxide
**Carbon** is the other framework modded Rust servers run. It is not a fork of Oxide and it does not
load Oxide; it is a separate loader that ships an **Oxide compatibility layer** — the `Oxide.Core`,
`Oxide.Plugins` and `Oxide.Game.Rust` namespaces, reimplemented — so that a plugin written for Oxide
compiles and runs unchanged.
This document exists because [`PLAN.md`](PLAN.md) **R19** commits `module-rust` to supporting both.
It records the places the two frameworks are *not* the same, because those are the only places our
code has to care. Everything not listed here is identical by construction.
> **Provenance.** Facts below were taken on **2026-09-15** from Carbon's own published metadata —
> `api.carbonmod.gg/meta/carbon/{hooks,commands,convars,switches}.json` — and from the
> [`CarbonCommunity/Carbon`](https://github.com/CarbonCommunity/Carbon) source at `main`, with the
> narrative pages at [carbonmod.gg](https://carbonmod.gg) as the prose source. Carbon is upstream
> and wins any disagreement, exactly as uMod does for [`OXIDE_API.md`](OXIDE_API.md). Nothing here
> is a Runic Gateway contract.
>
> **Not yet proven on a live Carbon server.** Every claim here is read off metadata or source. This
> project's own record on that is poor — phases 0 and 1 each found source-read claims a running
> server contradicted — so treat the whole document as *the hypothesis phase 3 tests*, not as
> established fact.
---
## 1. The one-sentence version
**A plugin in the `Oxide.Plugins` namespace deriving from `RustPlugin` is Carbon's own documented
first example**, so the bridge plugin is one `.cs` file that serves both frameworks. What diverges is
not the plugin API but **the things around it**: where files live, how the permission store is
persisted, what the console commands are called, and which extra hooks exist.
```csharp
// Carbon's own "first plugin" page shows this, unchanged from Oxide:
namespace Oxide.Plugins;
[Info("MyPlugin", "<author>", "1.0.0")]
public class MyPlugin : RustPlugin
{
private void OnServerInitialized() => Puts("Hello world!");
}
```
Carbon also offers a native shape — `namespace Carbon.Plugins` / `CarbonPlugin` — which we do not
use and should not: it is the one choice that would make the source Carbon-only.
---
## 2. Telling the two apart
### At compile time — `#if CARBON`
Carbon feeds the Roslyn compiler a set of conditional-compilation symbols. **Oxide defines no
equivalent**, so `#if CARBON` / `#if !CARBON` is the portable framework branch, and an Oxide
compiler simply evaluates the unknown symbol as false.
| Symbol | Meaning |
|---|---|
| `CARBON` | The framework is Carbon |
| `WIN`, `UNIX` | Host operating system |
| `STAGING`, `AUX01`, `AUX02` | Rust branch |
| `RUST_ABV_<v>`, `RUST_BLW_<v>`, `RUST_IS_<v>` | Rust protocol above / below / exactly `<v>` |
| `CARBON_ABV_<YYYY_MM_DD>` | Carbon protocol above a date |
This works because the bridge plugin ships as **source** and is compiled by whichever framework
loaded it. It would not work for a precompiled DLL — a reason, among others, not to ship one.
### At run time
`#if` is decided when the file is compiled, which is what we want for API differences. Where a
*runtime* answer is needed — reporting which framework a server runs, in a `server.hello` say — ask
for the type rather than the file layout: `Carbon.Community` exists only under Carbon.
---
## 3. Where the files live — **the divergence that reaches the most decisions**
| Oxide | Carbon |
|---|---|
| `oxide/plugins/` | `carbon/plugins/` |
| `oxide/config/` | `carbon/configs/`**plural** |
| `oxide/data/` | `carbon/data/` |
| `oxide/lang/` | `carbon/lang/` |
| `oxide/logs/` | `carbon/logs/` |
| `oxide/extensions/`, plus `Oxide.Ext.*.dll` in `RustDedicated_Data/Managed` | `carbon/extensions/` only |
| — | `carbon/modules/`, `carbon/harmony/`, `carbon/developer/` |
**And none of those paths is fixed.** Carbon takes a command-line override for every single
directory — `-carbon.rootdir`, `-carbon.configdir`, `-carbon.datadir`, `-carbon.scriptdir`,
`-carbon.langdir`, `-carbon.logdir`, `-carbon.extdir`, `-carbon.moduledir`, `-carbon.modifierdir`,
`-carbon.profiledir`, `-carbon.carbonconfigdir`, `-carbon.sqlpermsdb`, `-harmonydir`. An operator
who has moved one is not doing anything unsupported.
**So the rule is: never compose a config or data path.** Carbon reimplements Oxide's own directory
accessors and populates them from its resolver:
```csharp
Interface.Oxide.ConfigDirectory // oxide/config or carbon/configs or wherever -carbon.configdir points
Interface.Oxide.DataDirectory
Interface.Oxide.PluginDirectory
Interface.Oxide.LangDirectory
Interface.Oxide.LogDirectory
Interface.Oxide.ExtensionDirectory
Interface.Oxide.RootDirectory
Interface.Oxide.InstanceDirectory
```
(`Carbon.Common/src/Oxide/OxideMod.cs` assigns each from `Defines.Get*Folder()`; `Interface.cs`
logs all eight at boot.) Asking the framework is both shorter and correct; hardcoding `oxide/config`
is wrong on Carbon and wrong on an Oxide server whose operator moved things.
**This is a direct amendment to R18.** The config editor's recursive walk is rooted at
`ConfigDirectory`, not at a literal `oxide/config/`; the directory it must refuse to walk is
`DataDirectory`, not a literal `oxide/data/`. The reasoning behind R18 is untouched — only the way
the two roots are obtained.
---
## 4. Permissions — same API, different persistence
Every member R2 depends on exists with the same name and the same argument shape
(`Carbon.Common/src/Oxide/Libraries/Permissions.cs`): `RegisterPermission`, `PermissionExists`,
`GrantUserPermission`, `RevokeUserPermission`, `GrantGroupPermission`, `RevokeGroupPermission`,
`CreateGroup`, `RemoveGroup`, `AddUserGroup`, `RemoveUserGroup`, `UserHasPermission`,
`GroupHasPermission`, `GetUserGroups`, `GetUserPermissions`, `GetGroupPermissions`,
`GetPermissionUsers`, `GetPermissionGroups`, `GetGroups`, `GetUsersInGroup`, `SetGroupParent`.
Two differences, and they pull in opposite directions.
**The return type differs, and the portable answer is the one we already chose.** Carbon's
`GrantUserPermission` returns `bool`; Oxide's returns `void` — which is
[§12.2](PLAN.md#122-four-rules-the-r2-permission-push-must-obey)'s finding, that a grant naming an
unregistered permission silently does nothing. Calling it as a statement compiles on both, so the
source stays single. But **the bool cannot be read portably**, so the `PermissionExists` pre-check
stays the mechanism on both frameworks rather than being replaced by a return value on one. Carbon
is the framework that *would* have told us, and we still cannot listen.
Carbon's signature also takes `BaseHookable` where Oxide's takes `Plugin`. Passing `this` is
correct on both; a variable typed `Plugin` is not.
**The store is not a file we can read.** Oxide persists to JSON — `oxide/data/oxide.users.data` and
`oxide.groups.data`. Carbon persists to **Protobuf or SQLite**, switchable at run time
(`c.migrate_perms_proto`, `c.migrate_perms_sql`, with the SQLite path itself relocatable via
`-carbon.sqlpermsdb`, default `server/identity/carbon.perms.db`); `Oxide Overrides/PermissionSql.cs`
and `PermissionStoreless.cs` are the pluggable backends.
R2 never planned to read the store file, so this changes nothing — but it **closes the option
permanently**, which is worth stating once. Drift detection reads the API, or it does not work.
**Carbon does give R2 something Oxide's docs do not advertise: fourteen permission hooks**, a
`Permissions` category of its own — `OnUserPermissionGranted`, `OnUserPermissionRevoked`,
`OnUserGroupAdded`, `OnUserGroupRemoved`, `OnGroupCreated`, `OnGroupDeleted`, `OnGroupParentSet`,
`OnGroupRankSet`, `OnGroupTitleSet`, `OnGroupPermissionGranted`, `OnGroupPermissionRevoked`,
`OnPermissionRegistered`, `OnPermissionsUnregistered`, `OnUserNameUpdated`. Our uMod mirror carries
most of these as universal hooks too, so drift may be **observable as it happens** on both rather
than only diffable on connect. Phase 7 should test that rather than assume it; a hook that fires on
our *own* push is a feedback loop to suppress, not a bonus.
---
## 5. Console commands — `c.` not `oxide.`
Carbon's 129 published commands are `c.`-prefixed. The ones with Oxide counterparts:
| Oxide | Carbon |
|---|---|
| `oxide.grant` / `oxide.revoke` | `c.grant` / `c.revoke` |
| `oxide.group` | `c.group` |
| `oxide.usergroup` | `c.usergroup` |
| `oxide.load` / `oxide.unload` / `oxide.reload` | `c.load` / `c.unload` / `c.reload` |
| `oxide.plugins` | `c.plugins` |
Carbon can be configured to alias the old prefix, so an operator's muscle memory survives — but an
alias is opt-in and **we must never depend on one**.
**Where this reaches us is narrow but real.** R2 and R18 both act through the plugin API, not the
console, so neither cares. The two that do care are **documentation** — every operator-facing
instruction naming `oxide.grant` needs its Carbon line — and **any place we drive a reload by
console string**, which R18's write path does. Resolve the reload through the framework rather than
by composing a command, or branch it on `#if CARBON`.
---
## 6. Hooks — Carbon is a superset, with thirteen names it does not list
Carbon publishes **894 hook entries, 774 unique names, in 42 categories**, against the **476** on
uMod's Rust hooks page that [`HOOKS.md`](HOOKS.md) mirrors. The larger number is not more game
coverage; Carbon documents patched methods our mirror's audience never sees.
Carbon flags every entry for compatibility. **30 are Carbon-only. Zero are marked Oxide-only.**
### The 30 Carbon-only hooks
| Hook | Category | What it is |
|---|---|---|
| `CanAcceptBackpackItem` | Global | Whether to accept a backpack item |
| `CanPatrolHeliSeePlayer` | Global | Patrol-helicopter line of sight to a player |
| `CanPickupAllFromRack` | Global | Taking every weapon from a rack |
| `CanPickupFromRack` | Global | Taking one weapon from a rack |
| `CanPlaceOnRack` | Global | Placing on a rack |
| `OnPickupFromRack` | Global | Controls taking items from a rack |
| `CanPlayerInheritNetworkGroup` | Global | Network-group inheritance |
| `OnChairComfort` | Global | Chair comfort |
| `OnChickenScared` | Global | A chicken is scared |
| `OnGrowableUpdate` | Global | A growable updates |
| `OnConsoleCommand` | Global | A console command is executed |
| `OnNativeCommandHasPermission` | Global | Permission check on a native console command |
| `OnEntitySpawn` | Global | An entity spawns — **not** Oxide's `OnEntitySpawned`, which exists on both |
| `OnJackieChan` | Global | Undescribed upstream |
| `OnCarbonBanPlayer`, `OnCarbonUnbanPlayer`, `OnCarbonKickPlayer`, `OnCarbonMutePlayer` | Player | Carbon admin-module moderation actions |
| `OnCarbonBlinded`, `OnCarbonUnblinded`, `OnCarbonSpectateStart`, `OnCarbonSpectateEnd` | Player | Carbon admin-module spectate and blind actions |
| `OnCarbonPrivateMessage`, `OnCarbonEmpowerPlayerStats`, `OnCarbonLockPlayerContainer` | Player | Carbon admin-module player actions |
| `OnCompilationFail`, `OnConstructorFail` | Engine | Plugin compile / constructor failure |
| `OnPluginCompileFailure`, `OnPluginOutdated` | Plugin | Plugin lifecycle |
| `OnMarketplaceTerminalPurchase` | Vending | Marketplace terminal purchase |
**None of them is load-bearing for us and none should become so.** The `OnCarbon*` family is the
Carbon admin module's own audit trail — tempting for a staff-actions feed, and exactly the kind of
convenience that quietly makes Carbon the required framework. If we ever want that feed, it has to
have an Oxide answer first.
### The 13 uMod names Carbon's catalogue does not carry
| Hook | Category | uMod's description |
|---|---|---|
| `CanNpcAttack` | Entity | An NPC attempts to attack another entity |
| `CanPushBoat` | Player | Cancelling a boat push |
| `CanUnlockTechTreeNode` | TechTree | Unlocking a blueprint in a tech tree |
| `CanUnlockTechTreeNodePath` | TechTree | …after the path check |
| `OnFrame` | Server | Each frame |
| `OnHelicopterKilled` | Entity | A CH47 is going to be killed |
| `OnNpcDestinationSet` | Entity | Cancelling an NPC destination change |
| `OnNpcPlayerResume` | Entity | Cancelling `TryForceToNavmesh` |
| `OnNpcStopMoving` | Entity | Denying an NPC move stop |
| `OnPlayerCorpse` | Player | A non-null corpse has spawned |
| `OnQuarryEnabled` | Resource | A mining quarry is turned on |
| `OnTeamInvite` | Team | Cancelling a team invitation |
| `OnTeamPromote` | Team | Cancelling a promotion |
**Absent from a catalogue is not the same as absent from the framework**, and two of these look like
renames rather than holes: Carbon lists `OnTeamMemberInvite` and `OnTeamMemberPromote` in its `Team`
category, which is `OnTeamInvite` and `OnTeamPromote` under different names. Carbon's `Team`
category also carries visible duplicates and both tenses of the same event (`OnTeamCreate` *and*
`OnTeamCreated`, `OnTeamUpdate` *and* `OnTeamUpdated`, `OnTeamMemberInvite` twice), which says the
catalogue is generated rather than curated.
So this table is **a list of things to check on a live Carbon server**, not a list of losses. The
practical protection is one we already committed to in [`PLAN.md`](PLAN.md) §6: *hooks bind by name
and arity through reflection with no compile-time check*, so the plugin logs which of its expected
hooks have fired at least once. That mechanism was written for Facepunch renaming a hook on wipe
day; it answers this question too, on either framework, without us having to trust either catalogue.
**None of the 13 is currently in a phase.** R5 settled Teams on Rust's **first-party clans**, not
first-party Teams, so `OnTeamInvite`/`OnTeamPromote` are outside the plan as written.
---
## 7. Convars — a Carbon-only set exists, and leases must not reach for it
Carbon publishes 23 convars of its own, several of them precisely the kind of live, gameplay-shaped
value [`PLAN.md`](PLAN.md) §9 wants to lease — `c.recycletickmultiplier`,
`c.safezonerecycletickmultiplier`, `c.researchdurationmultiplier` and so on, most flagged
`ForceModded`.
**A lease over one of those would work on Carbon and be undeclarable on Oxide.** Lease keys are
advertised to the event authoring form, and a key that silently does not exist on half of installs
is the failure `EVENTS.md` §H's *verify every key live* rule exists to prevent. So: **the lease
catalogue is drawn from the game's own convars, which both frameworks expose identically.** If a
Carbon-only key is ever worth the cost, it is advertised conditionally on the connected server's
framework, and that is a deliberate decision rather than an oversight.
---
## 8. Operating differences that reach deployment
- **They cannot coexist.** Oxide ships a patched `Assembly-CSharp.dll`; Carbon requires
Facepunch's vanilla one and patches in memory through Harmony. One install runs one framework, so
**one rig cannot prove both** — which is why [`PLAN.md`](PLAN.md) R21 moves the rigs to
Pterodactyl and runs two.
- **Carbon migrates an Oxide install on first boot** — it copies config, data, lang, user and group
files across and relocates `Oxide.Ext.*.dll` out of `RustDedicated_Data/Managed`. Useful for an
operator; a hazard for a test rig, because a Carbon rig built by converting an Oxide one starts
with the Oxide one's state and proves less than a clean install.
- **Carbon self-updates and its releases are rolling tags**, not versioned ones:
`production_build` (v2.0.259 at the time of writing, 2026-09-06), plus `edge_build`,
`experimental_build` and per-branch Rust builds. Oxide publishes an incrementing build number.
**So "which Carbon is this" is not answerable the way "which Oxide is this" is**, and R4's
`doctor` prerequisite check has to accept that — it can establish *that* Carbon is installed and
report the build it reports, but "current enough" is a weaker claim on Carbon than on Oxide.
- **Carbon patches hooks only when a plugin subscribes**, so an unsubscribed hook costs nothing.
That rewards the selective subscription R17 already requires for ZoneManager's chatty zone
transitions, on Carbon more than on Oxide.
---
## 9. What this costs us, in one table
| Decision | Change |
|---|---|
| **R2** permissions | None to the design. The store is API-only on Carbon *by construction* rather than by choice, and `PermissionExists` stays the check because the useful return value is Carbon-only |
| **R4** installer | `doctor` detects *which* framework, not *whether Oxide*; the payload drops into `PluginDirectory`; "current enough" is weaker on Carbon (rolling tags) |
| **R18** config editor | Roots come from `Interface.Oxide.ConfigDirectory` / `DataDirectory`, never literals; the reload is resolved through the framework, not by composing `oxide.reload` |
| **R6/R17** base mods | Unchanged — Kits, Clans, PopupNotifications and ZoneManager are Oxide plugins and Oxide plugins run on Carbon |
| **§9** event leases | Keys come from the game's convars; Carbon's own convars are out of the catalogue unless advertised conditionally |
| Everything else | Unchanged |
The honest summary: **Carbon costs three amendments and one extra rig, not a second codebase.**

View File

@@ -1,8 +1,10 @@
# `module-rust` — the plan
**Status:** approved in outline 2026-09-15, not started. **Eighteen decisions of record, no open
questions.** Audited against the whole contract, not just the game-facing chapters (§7); the event and
engagement catalogues are §9 and §10; §11 is a second pass over `MODULE_API.md` itself.
**Status:** phases 0 and 1 done, 2026-09-15. **Twenty-one decisions of record; one outstanding
request, no open questions.** Audited against the whole contract, not just the game-facing chapters
(§7); the event and engagement catalogues are §9 and §10; §11 is a second pass over `MODULE_API.md`
itself. **R19R21 (2026-09-15) added a second modding framework, a Pterodactyl egg, and moved the
rigs off the workstation** — see [`CARBON.md`](CARBON.md) for the framework reference.
The [dry run](../rust-dryrun.md) designed this module on paper and deliberately did not build it.
This is the document that builds it. Where the two disagree, this one is later and wins — but the dry
@@ -675,9 +677,149 @@ open to everybody, so granting a permission for it rewards nobody with anything.
kit dropdown must surface which kits are permission-gated and refuse — or at minimum warn loudly —
on one that is not. That is a real refusal with a real reason, and exactly what R3's envelope is for.
### R19 — the plugin is framework-agnostic: Oxide **and** Carbon, from now rather than later
**Decided 2026-09-15 (org lead).** Modded Rust runs on two frameworks, not one, and `module-rust`
supports both from the phase it first reads anything — not as a port after phase 18. The bridge
plugin stays **one `.cs` file in the `Oxide.Plugins` namespace deriving from `RustPlugin`**, which is
also Carbon's own documented first example, with `#if CARBON` used only where the APIs genuinely
differ.
**This is affordable because the divergence is concentrated, not spread.** Carbon is not a fork of
Oxide; it is a separate loader shipping an Oxide compatibility layer, and at the level a plugin sees
the two are the same API. [`CARBON.md`](CARBON.md) is the reference — where it came from, what was
read, and the honest note that **none of it has yet run on a live Carbon server.**
Three existing decisions take an amendment, and no decision is reversed:
- **R18 — paths come from the framework, never from a literal.** Carbon's config directory is
`carbon/configs` (plural) and its data directory `carbon/data`, *and every one of Carbon's
directories is relocatable from the command line* (`-carbon.configdir`, `-carbon.datadir`,
`-carbon.rootdir`, and nine more). So the recursive walk is rooted at
`Interface.Oxide.ConfigDirectory` and the directory it refuses to walk is
`Interface.Oxide.DataDirectory`. Carbon reimplements both accessors; a hardcoded `oxide/config/`
is wrong on Carbon *and* on an Oxide server whose operator moved things. The reasoning behind R18
is untouched — only how the two roots are obtained.
- **R2 — the store was never readable and now it is unreadable by construction.** Oxide persists
permissions as JSON (`oxide/data/oxide.users.data`); Carbon persists them as **Protobuf or
SQLite**, switchable at run time. R2 always planned to read the API, so nothing changes — but the
file-reading shortcut is now permanently closed, which is worth saying once. §12.2's
`PermissionExists` pre-check also survives intact: Carbon's `GrantUserPermission` returns `bool`
where Oxide's returns `void`, so **the framework that would have told us whether the write landed
is the one we cannot portably listen to.**
- **R4 — `doctor` asks *which* framework, not *whether Oxide*.** The payload drops into
`PluginDirectory` either way. One thing gets weaker: **Carbon's releases are rolling tags**
(`production_build`, `edge_build`), not an incrementing build number, so "current enough" is a
claim `doctor` can make about Oxide and can only approximate about Carbon.
**What this decision explicitly refuses.** Carbon publishes 30 hooks Oxide does not, including an
`OnCarbon*` family mirroring its admin module's every moderation action — a tempting staff-audit
feed, and precisely the thing that would quietly make Carbon required. **No Carbon-only hook and no
Carbon-only convar enters a catalogue** unless it has an Oxide answer first, or is advertised
conditionally on the connected server's framework as a deliberate decision. Likewise Carbon's native
`Carbon.Plugins` / `CarbonPlugin` shape is not used: it is the single choice that would make the
source Carbon-only.
**The one thing to hold loosely.** Thirteen hook names our uMod mirror carries are absent from
Carbon's published catalogue ([`CARBON.md`](CARBON.md) §6). At least two look like renames rather
than holes, and none is in a phase today. The protection is the one §6 already requires for a
different reason — *the plugin logs which of its expected hooks have fired at least once* — which
answers this on either framework without trusting either catalogue.
### R20 — a Pterodactyl egg is a Rust-Link deliverable, with the sidecar inside the game container
**Decided 2026-09-15 (org lead).** Most Rust servers are rented, and most rented Rust servers run on
a Pterodactyl panel. So alongside the installer (R4) and the hand install, **a published Pterodactyl
egg is the third supported way the shard side reaches an operator** — and it has to work *in the same
manner* as the other two, not as a degraded variant.
The egg is derived from the community **"Rust Autowipe"** egg, taken as the known-good base, and it
keeps everything that egg already gets right: the steamcmd install script, the wipe-day
`REGEN_SERVER` / `REMOVE_FILES` mechanism, the Rust+ `APP_PORT`, and — the reason it is the right
base — a **`FRAMEWORK` variable already offering `vanilla | carbon | oxide`**. The operator picks the
framework at deploy time, which is R19's justification restated as a deployment fact: we do not get
to choose.
**The sidecar runs inside the game's own container, and that is the load-bearing part.** A
Pterodactyl server gets its own network namespace, so `127.0.0.1` inside it is genuinely private —
which means **D2 survives untouched**: the game link stays loopback and stays unauthenticated,
because loopback *is* the authentication. The startup command becomes a small wrapper that launches
`rust-link-sidecar` and then `RustDedicated`.
The alternative — a second Pterodactyl server running the sidecar — was rejected for exactly that
reason. Two containers have no shared loopback, so it would force a token and a routable bind onto
the game link. That is the case argued at D2 and overruled; it is not reopened here.
Four things the egg must get right, each of which is a way to get it wrong:
- **A second allocation for `[web].bind`.** The sidecar's HTTP/WS side is the half the website
reaches, so it binds to the container's assigned address on an allocation the panel hands out —
not to loopback. The token is what guards it, exactly as on a hand install.
- **The sidecar's database must never appear in `REMOVE_FILES`.** That variable is the wipe
mechanism, and R12 keeps **all-time rollups across wipes**. A sidecar store swept on wipe day is
the one failure that looks like success: the server comes back, the site repopulates, and every
player's history is silently gone.
- **Stop means stop the game.** The egg's stop command is `quit`, addressed to RustDedicated. The
wrapper has to let the sidecar go down with it rather than outliving it or holding the container
open.
- **The plugin and the sidecar come from a release, never from a copy.** The install script fetches
the pinned pair the same way the installer resolves a bundle — which makes the egg the third
consumer of the protocol-pairing check, not an exception to it.
**It lands in phase 18, beside the installer**, because phase 18 is already "how the shard side
reaches an operator", and one story told twice is how two stories drift apart.
### R21 — both Rust rigs move to Pterodactyl, because one install cannot prove two frameworks
**Decided 2026-09-15 (org lead).** Oxide and Carbon **cannot coexist in one install** — Oxide ships
a patched `Assembly-CSharp.dll` and Carbon requires Facepunch's vanilla one. So R19 cannot be proven
on `D:\rust`, or on any single server, at all.
Both rigs move to the existing Pterodactyl panel at **192.168.0.12** (node `Main`): one server with
`FRAMEWORK=oxide`, one with `FRAMEWORK=carbon`, on the same egg. They are started and stopped as
needed rather than both left running.
This replaces `D:\rust` as the rig of record, and it buys more than parity:
- **The egg gets exercised by every phase**, not only by phase 18. R20's deliverable stops being a
thing written once at the end against a panel nobody has used.
- **It ends the wipe-day maintenance that dominated §4.** `start.bat`'s steamcmd argument ordering,
re-extracting Oxide after every `app_update`, checking `Assembly-CSharp.dll`'s byte size to tell a
half-done Oxide install from a working one — all of that becomes the panel's job, through
reinstall.
- **It is a Linux rig.** Every previous finding came from Windows and Mono; phase 1 spent real time
on a Mono-specific NUL-padded `SocketException.Message`. Production Rust servers are Linux, so the
rig moving there makes findings more representative, and makes any remaining Windows-only
behaviour something we notice rather than depend on.
**A Carbon rig must be a clean install, not a converted one.** Carbon migrates an Oxide install on
first boot — it copies config, data, lang and permission files across. A Carbon rig made by
converting the Oxide rig would start out holding the Oxide rig's state, and would prove less than a
fresh one.
**The access, and the one thing it cannot do.** The panel's application API token is at
`RunicGateway/pterodactyl_claude_api_token` (`ptla_…`). It creates and configures servers,
allocations and users, and reads eggs. **It cannot touch files, power or console** — Pterodactyl puts
those on the *client* API, which rejects an application key outright. So the deployment loop is three
tiers, matched to what each is for:
| What | How | Why that one |
|---|---|---|
| **A release artefact** — the pinned plugin + sidecar pair | The egg's own install script, re-run by a panel **reinstall** | It is the path we ship. Exercising it on the rig is acceptance testing for free |
| **Working-tree iteration** — an uncommitted `.cs` under test | A **client** API key (`ptlc_…`): `files/write`, then `command` to reload | The Pterodactyl analogue of `servuo-plugins/deploy.ps1`, and it carries the same caveat: **if something only works when the push script copies it, it does not ship** |
| **Bulk or binary** — sidecar builds, world files | SFTP on the node, port 2022 | Where the client API's per-file write is the wrong shape |
**The client key is the one thing outstanding**, and it is a request rather than a decision: an
application key cannot be widened into one, so the org lead generates it from the account page. Until
it exists, iteration is reinstall-only — correct, and slow enough that nobody would choose it twice.
The push script itself lives in **`Rust-Plugins`**, mirroring where `deploy.ps1` lives for ServUO.
## 3. Open questions
**None.** Both questions this section carried were closed on 2026-09-15.
**None — but one outstanding request.** R21's iteration loop needs a Pterodactyl **client** API key
(`ptlc_…`); the application key already on disk cannot be widened into one, and only the account
holder can mint it. Not a question and not a decision: until it exists, pushing a file to a rig means
a panel reinstall. Everything else in this section was closed on 2026-09-15.
*Clans in the base set while the Team provider reads first-party* was confirmed as the intended
reading: complementary, not in conflict — the plugin is installed for alliances and clan chat, the
@@ -689,48 +831,62 @@ online. The persisted pending-grant queue that question was weighing is not need
## 4. The test rig
`D:\rust` on the org lead's workstation. **Brought current in phase 0 (see §12):** build
**25230300**, Oxide **2.0.7716**, a fresh procedural world (seed 1234, size 4000) generated for this
wipe, and all four base plugins loaded. Its Oxide permission store holds a `default` and an `admin`
group with one admin user, so R2's mechanism was exercised on day one and works.
**Two servers on the Pterodactyl panel at `192.168.0.12`, one per framework (R21).** They replace
`D:\rust`, which was the rig for phases 0 and 1 and whose findings are still recorded in §12 and §13.
A wipe keeps `server/server1/cfg/`. That directory holds `users.cfg`, and `users.cfg` holds the
`ownerid` line — delete the whole identity directory and you silently remove the operator's own
ownership along with the map.
| | Oxide rig | Carbon rig |
|---|---|---|
| Panel | node `Main` (192.168.0.12), nest 4 "Rust" | same |
| Egg | ours, derived from **"Rust Autowipe"** (panel egg id 18 is the unmodified base) | same egg |
| `FRAMEWORK` | `oxide` | `carbon` |
| Allocations | game, query, RCON, Rust+, **plus one for the sidecar's `[web].bind`** | same |
Three traps recorded here because each cost time before it was understood:
Started and stopped as needed rather than both left running; the other servers on the node are
shut down, which is what makes two ~20 GB Rust installs fit a 128 GB disk.
- **`start.bat` never updated anything — the 2026-09-15 diagnosis was wrong, corrected in phase 0.**
The script was read as *"updates `C:\rustserver`, runs `D:\rust`"*, and the fix changed the path to
`d:\rust\`. The path was never the problem. **steamcmd requires `+force_install_dir` before
`+login`**, and the script had it after:
**The Carbon rig is a clean install, never a converted one.** Carbon migrates an Oxide install on
first boot — config, data, lang and permission files all come across — so a Carbon rig made by
switching `FRAMEWORK` on the Oxide rig would start out holding the Oxide rig's state and would prove
strictly less.
```
steamcmd.exe +login anonymous +force_install_dir d:\rust\ +app_update 258550 +quit
→ Please use force_install_dir before logon!
→ Error! App '258550' state is 0x486 after update job.
```
**What the panel changes about how work reaches a rig.** The token at
`RunicGateway/pterodactyl_claude_api_token` is an *application* key: it manages servers, allocations
and users, and it **cannot write a file, press a button or run a console command** — Pterodactyl puts
those on the client API. R21's table has the three tiers; the short version is *release artefacts
arrive by reinstall, iteration needs a client key, bulk goes over SFTP on port 2022.*
So the flag was discarded, the update ran against steamcmd's own directory, and the job errored out
every single time. It updated **no** directory, ever — which is the actual reason `D:\rust` fell a
wipe behind, and why `C:\rustserver` sits at the *same* stale buildid rather than a newer one.
Now reads `+force_install_dir d:\rust\ +login anonymous +app_update 258550 +quit`; the pristine
original is kept at `start.bat.bak` and the path-only fix at `start.bat.broken-order-20260915`.
With the ordering right, the run is a delta and takes minutes.
**Two facts about this panel that are easy to trip over:**
- **Re-extract Oxide after every `app_update`.** Updating the server and re-installing Oxide together
is the standard operator routine, not a discovery — Oxide ships a *patched* `Assembly-CSharp.dll`
and a Steam update restores Facepunch's. Recorded here only for the mechanical detail: the update
does **not** remove `Oxide.Core.dll` and friends, so a half-done install still *looks* Oxided while
loading no plugins and raising no hook. Check the size rather than the directory — on build
25230300 vanilla is 9,758,544 bytes and Oxide 2.0.7716's is 9,953,280.
- **`C:\oxide_files` is a 2025-04-23 Oxide and must not be copied anywhere.** Oxide ships a patched
`Assembly-CSharp.dll`; that bundle's is 6,842,880 bytes against the live 9,953,280, so copying it
over a real install is a hard downgrade. `D:\rust` is already correct and needs nothing from it.
- `/api/client/**` returns **403 `AccessDeniedHttpException`** for the application key — a clear
error, but only if you are expecting it. It is not a permissions grant that can be widened.
- The application API has **no egg-write endpoint at all** (`/api/application/eggs` is a 404; eggs
are read through `/api/application/nests/{nest}/eggs`). Importing a new egg version is an admin-UI
or `php artisan` operation, so the egg's release artefact is a JSON file a human imports — which is
also exactly how an operator will consume it.
**Rust force-wipes on the first Thursday of the month and Oxide is rebuilt to match**, so "is the
rig current" is a recurring question, not a one-time setup step. Every phase that touches the plugin
re-checks it.
### What moving off the workstation retires
Everything below was true of `D:\rust` and is kept only because it explains findings in §12. **None
of it is maintenance any more** — the panel's reinstall does the same work correctly.
- **`start.bat` never updated anything.** steamcmd requires `+force_install_dir` **before** `+login`
and the script had it after, so the flag was discarded, the update ran against steamcmd's own
directory, and the job errored every single time (`Error! App '258550' state is 0x486`). That — not
the path, which was the first and wrong diagnosis — is why the rig fell a wipe behind.
- **Re-extract Oxide after every `app_update`.** Oxide ships a *patched* `Assembly-CSharp.dll` and a
Steam update restores Facepunch's, but the update does **not** remove `Oxide.Core.dll` and friends
— so a half-done install still *looks* Oxided while loading no plugins and raising no hook. The
tell was file size: on build 25230300, vanilla 9,758,544 bytes against Oxide 2.0.7716's 9,953,280.
- **`C:\oxide_files` is a 2025-04-23 Oxide and must not be copied anywhere** — its
`Assembly-CSharp.dll` is 6,842,880 bytes, a hard downgrade over a live install.
- **A wipe keeps `server/server1/cfg/`**, which holds `users.cfg` and therefore the `ownerid` line.
Delete the whole identity directory and you silently remove the operator's own ownership along with
the map. This one still applies — it is the game's shape, not the host's, and it is why the egg's
`REMOVE_FILES` list is worth reading carefully rather than trusting.
**Rust force-wipes on the first Thursday of the month, and both frameworks rebuild to match.** "Is
the rig current" stays a recurring question rather than a setup step; what changed is that the answer
is now a reinstall rather than a sequence of manual steps that can half-succeed.
## 5. The phases
@@ -744,6 +900,12 @@ are events, whose catalogue is **§9**. 14 is the map. 18 is how any of it reach
not us. The Android legs (5, 8, 11, 15) each trail the
website surface they consume by one phase, per R10.
**R19 and R21 do not add a phase — they change what "done" means for several.** Both rigs exist from
phase 3 onward, so from phase 3 a criterion is met when it is met **on both frameworks**, and a
finding that holds on only one is a finding either way. Phase 2's release artefacts and phase 18's
egg are the two places the second framework is visible in the deliverable rather than only in the
proving.
Each phase ends with its findings written down, as every workstream here does.
| # | Phase | Repos | Done when |
@@ -751,12 +913,12 @@ Each phase ends with its findings written down, as every workstream here does.
| 0 | **The rig.****Done 2026-09-15 — as built and findings in §12.** Updated to the current wipe (the script was fixed *again*, properly), Oxide re-laid, base set installed, the grant path proven end to end and both zone transitions observed live with a player connected. **Both criteria met** | docs | A current server boots with all four loaded, `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it |
| 1 | **Protocol 1, three skeletons, and every bundle seam at once.****Done 2026-09-15 — as built and findings in §13.** Plugin, sidecar and module all exist and all three were exercised against the live rig; three org-lead decisions (§13.0), five defects only a running server found (§13.3), and a correction to §11.3 (§13.2). **Both criteria met** | all 3 + docs | One hello line travels game -> sidecar -> module; killing the sidecar does not stall the game; all five guards green on an untouched skeleton |
| 2 | **Packaging and release.** `release.yml`, the install manifest, the `sha256`, the host allowlist — and a real install into a running core from a manifest URL | Module-Rust + docs | An operator installs the empty module from Admin -> Modules and it reaches `started` |
| 3 | **The read path.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots distinct at the wire; `wipe_id` **and server id** on every row (R8); all-time rollups (R12); every board re-emitted on connect | all 3 + docs | A restarted sidecar is fully populated within one connection, and a wipe does not erase a player's history |
| 3 | **The read path, on both frameworks.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots distinct at the wire; `wipe_id` **and server id** on every row (R8); all-time rollups (R12); every board re-emitted on connect. **First phase to run against the Carbon rig (R19/R21)** — it turns [`CARBON.md`](CARBON.md) from a source-read hypothesis into tested fact, including whether the 13 unlisted hook names are renames or holes | all 3 + docs | A restarted sidecar is fully populated within one connection, a wipe does not erase a player's history, and **the same plugin file does all of that on Oxide and on Carbon** |
| 4 | **The first pages.** Server list as the landing page, `/rust/servers/:id` beneath it, killfeed, leaderboard; nav rows; the UI kit (`PublicLayout` `shell`, `PageHeader` props); `capabilities`; the `site.footer.status` slot (R13) | Module-Rust | The site renders the last thing each server said while every server is off |
| 5 | **Android leg A** (R10). Capability-driven shell from `GET /api/v1/public/modules`, plus the phase-4 screens | Android-app | The app renders a Rust site it has never seen, and a UO site unchanged |
| 6 | **Identity** (R1), and the `admin.users.detail` slot (R13) | 3 + docs | A player links an account in-game; an operator sees the Steam id inside core's own user page |
| 7 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported | all 3 + docs | A grant made on the website gates a third-party plugin in-game, and survives a wipe |
| 7b | **Mod configuration from the site** (R18). **Recursive** walk of `oxide/config/` (never `oxide/data/`), generated form from the live values, raw-JSON advanced tier, explicit reload target, versioned read/write, auto-reload watched on `OnPluginLoaded`, **automatic rollback** over the whole file set, path-traversal guards, secret redaction, its own permission and an audit trail | all 3 + docs | An admin flips a ZoneManager setting from the website and it takes effect; a deliberately broken config rolls itself back and says why; a nested `<Mod>/x.json` is found and reloads the right plugin |
| 7 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported. The `PermissionExists` pre-check stays the mechanism on **both** frameworks (R19); Carbon's 14 permission hooks are tested here as a possible live drift signal, and suppressed against our own pushes if they fire | all 3 + docs | A grant made on the website gates a third-party plugin in-game, survives a wipe, and behaves the same against Oxide's JSON store and Carbon's Protobuf/SQLite one |
| 7b | **Mod configuration from the site** (R18). **Recursive** walk of `Interface.Oxide.ConfigDirectory` — never `DataDirectory`, and never either as a literal path (R19) — generated form from the live values, raw-JSON advanced tier, explicit reload target, versioned read/write, auto-reload watched on `OnPluginLoaded`, **automatic rollback** over the whole file set, path-traversal guards, secret redaction, its own permission and an audit trail | all 3 + docs | An admin flips a ZoneManager setting from the website and it takes effect; a deliberately broken config rolls itself back and says why; a nested `<Mod>/x.json` is found and reloads the right plugin |
| 8 | **Android leg B** (R10). Identity and permission surfaces | Android-app | A player links from the app |
| 9 | **Teams from first-party clans** (R5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
| 10 | **Notifications and engagement** (R7). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook — **the catalogue is §10**, including the in-game-popup question | Module-Rust + docs | The offline raid alert reaches the player whose base it was, and nobody else |
@@ -767,8 +929,8 @@ Each phase ends with its findings written down, as every workstream here does.
| 15 | **Android leg D** (R10). Map and events | Android-app | The map renders on a phone with the same layer gates |
| 16 | **Discord slash commands** (R11). A small read-only set, every refusal deferred ephemeral | Module-Rust + docs | A refusal does not go public in the channel |
| 17 | **Optional mod integrations** (R15). **BetterChat** first — leaderboard titles through `API_RegisterThirdPartyTitle`, a pull with no drift — then the uMod **Clans** adapter (alliances and clan chat, beside the provider rather than under it, R5), then others as they prove useful | Rust-Plugins + Module-Rust + docs | A server missing every optional mod still runs the module, Teams included |
| 18 | **The installer** (R4). `--game servuo|rust`, the bundle payload as a variant, an Oxide prerequisite check in `doctor`, the protocol pairing refusal carried over | installer + docs | An operator sets a Rust server up with the released binary and nothing hand-copied |
| 19 | **Docs, kit feedback, cutover.** `docs/`; **`.profile`** (three repos were added); **`runicgateway.com`** (a second game is a headline change); and the Integration-kit question R2 raised | docs + Integration-kit + .profile + runicgateway.com | `docs/` describes what shipped, the front door names the new repos, and R2's missing chapter is answered either way |
| 18 | **The installer** (R4) **and the Pterodactyl egg** (R20) — the two halves of "how the shard side reaches an operator", built together so one story is not told twice. `--game servuo\|rust`, the bundle payload as a variant, a **framework** prerequisite check in `doctor` (which one, not whether Oxide — R19), the protocol pairing refusal carried over; the egg derived from "Rust Autowipe" with the sidecar inside the game container, a second allocation for `[web].bind`, the sidecar store held out of `REMOVE_FILES`, and its install script fetching the same pinned pair the installer resolves | installer + Rust-Link + docs | An operator sets a Rust server up with the released binary and nothing hand-copied; **and** a second operator imports the egg, deploys, and reaches the same place — on either framework |
| 19 | **Docs, kit feedback, cutover.** `docs/`; **`.profile`** (three repos were added); **`runicgateway.com`** (a second game is a headline change, and Pterodactyl is a hosting claim the site can now make); the Integration-kit question R2 raised; and whether the kit owes a reader anything about **supporting two mod frameworks at once** (R19) — a shape it has no chapter for either | docs + Integration-kit + .profile + runicgateway.com | `docs/` describes what shipped, the front door names the new repos, and R2's missing chapter is answered either way |
### Why the lease comes before the reward action
@@ -825,15 +987,31 @@ than being quietly lost.
never called, silently, with no warning at load — the single most common way a Rust plugin does
nothing. The plugin must log which of its expected hooks have fired at least once, so a hook
Facepunch renamed on a wipe is visible rather than mysterious. See [`README.md`](README.md) §2.
**R19 gives that mechanism a second job:** it is also the only trustworthy answer to "does this
hook exist on Carbon", since two published catalogues disagreeing is evidence about the catalogues
and not about the frameworks.
- **The cheapest way to make Carbon required is to do it by accident.** Carbon's extra 30 hooks, its
23 extra convars and its `bool`-returning permission API are each individually useful, individually
small, and collectively a framework lock-in nobody decided on. R19's refusal is written down
because it will be re-argued, once per convenience.
- **Two rigs is twice the state that can be quietly wrong.** A finding proven on the Oxide rig and
assumed on the Carbon one is exactly the failure this project keeps finding in source-read claims.
From phase 3, "done" means done on both, and a phase that could only check one says so.
- **A convar that applies cleanly and does nothing.** Most game config is read once at boot and
cached; applying it later succeeds, reads back correctly, and changes nothing. Every lease key gets
verified live — apply, observe in the running game, restore — before it is advertised. The UO
module surveyed 156 config reads and found roughly eight that were live.
- **The wipe cadence is the schedule.** A monthly force wipe moves the hook list, rebuilds Oxide, and
invalidates every ledgered resource. Phases that end near one should expect to re-verify rather
than assume.
- **`start.bat`'s RCON password is `letmein` in plaintext with `rcon.web 1`.** Acceptable on a
loopback dev rig, and it must never be the shape anything published copies.
- **The wipe cadence is the schedule.** A monthly force wipe moves the hook list, rebuilds both
frameworks, and invalidates every ledgered resource. Phases that end near one should expect to
re-verify rather than assume. **Carbon's self-updating and rolling release tags mean the Carbon rig
may move under us between two runs on the same day**, where an Oxide build number at least says so.
- **The old rig's RCON password was `letmein` in plaintext with `rcon.web 1`.** Acceptable on a
loopback dev rig behind a home firewall, and it must never be the shape anything published copies —
which now matters more, because the panel rigs are reachable on a LAN address and **the egg is a
published artefact that people will copy defaults out of.**
- **The panel is the rig and the deliverable at once.** Convenient, and a way to prove the wrong
thing: a rig hand-tuned through the panel UI stops testing the egg. Anything a rig needs belongs in
the egg or in the push script, never only in a server's saved configuration.
## 7. Contract coverage audit

View File

@@ -1,10 +1,13 @@
# Rust — the Oxide/uMod ecosystem reference
# Rust — the modding-framework reference
Reference material for the **upcoming `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.
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.
Everything below was **scraped verbatim from uMod on 2026-09-15**.
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
@@ -15,6 +18,7 @@ Everything below was **scraped verbatim from uMod on 2026-09-15**.
| [`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
@@ -40,8 +44,12 @@ 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 — specifically an **Oxide plugin**, since Oxide/uMod is what modded Rust servers
> run — hooking the game's own events.
> 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:
@@ -50,8 +58,8 @@ Two consequences, and they are the two halves of this directory:
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.** Our plugin is compiled, loaded, permissioned
and configured by Oxide, on Oxide's terms. [`OXIDE_API.md`](OXIDE_API.md) is that rulebook, and
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.