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:
307
modules/rust/CARBON.md
Normal file
307
modules/rust/CARBON.md
Normal 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.**
|
||||
@@ -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. **R19–R21 (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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user