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.**
|
||||
Reference in New Issue
Block a user