# 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. > > **Verified on a live Carbon server on 2026-09-15** — Carbon **2.0.259.0** `[2026.09.03.0]` on > Linux, the `rust-carbon` rig (PLAN.md §14.5). Three of the four load-bearing claims held. **One was > wrong, and it was wrong about Oxide as well as Carbon** — see §4. Corrected in place; §10 is the > scorecard. --- ## 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", "", "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 | | `RUST` | The game is Rust | | `OXIDE_PUBLICIZED` | Compiled against publicised Oxide assemblies | | `WIN`, `UNIX` | Host operating system | | `STAGING`, `AUX01`, `AUX02` | Rust branch | | `RUST_ABV_`, `RUST_BLW_`, `RUST_IS_` | Rust protocol above / below / exactly `` | | `CARBON_ABV_` | 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. **Confirmed on the live rig**: `carbon/config.json` reports `"ConditionalCompilationSymbols": ["CARBON", "RUST", "OXIDE_PUBLICIZED"]`, and the list is an operator-editable setting (`c.addconditional` adds to it), so treat the three above as the ones present by default rather than the ones guaranteed. ### 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, same format, different directory 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. **All of the member names above were confirmed present on the live Carbon rig**, which loaded and ran our plugin against them unchanged. **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 — **this section was wrong, and the truth is worse** > **Corrected 2026-09-15 against both live rigs.** This document previously said *"Oxide persists to > JSON; Carbon persists to Protobuf or SQLite"*, and offered that difference as the reason not to read > the file. **Both halves were wrong.** The real shape is more dangerous than the one that was > imagined, which is the only reason it is worth the space. Read off the two running servers, byte for byte: | | Oxide rig | Carbon rig | |---|---|---| | Path | `oxide/data/oxide.users.data`, `oxide.groups.data` | `carbon/data/oxide.users.data`, `oxide.groups.data` | | First bytes | `0a 16 0a 07 64 65 66 61 75 6c 74 …` | `0a 17 0a 07 64 65 66 61 75 6c 74 …` | | Format | **Protobuf** | **Protobuf** | | Default groups | `default`, `admin` | `default`, `admin`, **`moderator`** | **Neither framework writes JSON, and Carbon writes Carbon's data into files named after Oxide.** So the trap is not "two formats you must tell apart". It is: 1. **The filename is identical and tells you nothing**, so a reader keyed on `oxide.users.data` silently follows the wrong framework's file if it ever guesses the directory wrong. 2. **The format is an undocumented binary**, not the JSON the name and the `.data` extension suggest. 3. **Carbon can change it out from under you at run time** and Oxide cannot. `PermissionSerialization` in `carbon/config.json` defaults to `0` (the Protobuf above); `c.migrate_perms_sql` moves the whole store to SQLite at `server/identity/carbon.perms.db`, itself relocatable via `-carbon.sqlpermsdb`. `Oxide Overrides/PermissionSql.cs` and `PermissionStoreless.cs` are those backends. **R2's conclusion is unchanged and the argument for it is now much stronger.** A file reader would have *worked* on both rigs today — same format, same names — and would break for the one operator who ran a migrate command, with no error and no version marker to notice. **Drift detection reads the API, or it does not work.** **One more thing R2 has to accommodate: Carbon creates a third default group.** `carbon/config.json` names `PlayerDefaultGroup`, `AdminDefaultGroup` and `ModeratorDefaultGroup`, all auto-granted by auth level (`AutoGrantPlayerGroup` / `AutoGrantAdminGroup` / `AutoGrantModeratorGroup`, all `true`). A site that pushes its *full* group set on connect must not treat `moderator` as drift to be reported, nor delete it — the framework will simply recreate it, and the site will report drift for ever. **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**. **Confirmed on the live rig:** `c.version`, `c.plugins`, `c.grant` and `c.group` all answered; **`oxide.plugins` produced no output at all**. Note the shape of that failure — Pterodactyl's `command` endpoint returns `204` either way, and Carbon prints nothing for an unknown command, so *a wrong prefix looks exactly like a command that worked.* `c.plugins` is also worth knowing about for a reason unrelated to permissions: **it reports per-plugin `hook fires`, `hook time`, `hook memory`, `hook lag` and `hook exceptions`**, which is most of the "log which of its expected hooks have fired at least once" mechanism [`PLAN.md`](PLAN.md) §6 requires — for free, and only on Carbon. Useful when debugging *on* Carbon; **not a substitute for the plugin's own counter**, which has to work on both. Our plugin appears there as `Runic Gateway RunicGateway v0.1.0 … 2367ms [1077ms]`, under `Scripts`, with `failed plugins (0)`. **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.** --- ## 10. Scorecard — what the live rig confirmed and what it corrected Run 2026-09-15 against `rust-carbon` (Carbon **2.0.259.0** `[2026.09.03.0]` `21063e8`, Linux, `production_build`, Rust 103/2633.288.1), with the Oxide rig alongside for comparison. | Claim | Verdict | Evidence | |---|---|---| | An `Oxide.Plugins` / `RustPlugin` source file loads unchanged | **CONFIRMED** | The byte-identical `RunicGateway.cs` that runs on the Oxide rig loaded as `Runic Gateway v0.1.0` in `2367ms`, printed the same startup line, and retried the absent sidecar the same way | | The framework root is `carbon/`, config dir is `configs` (plural) | **CONFIRMED** | `/carbon/{configs,data,lang,logs,plugins,extensions,modules,managed,native,modifiers,temp,tools}`; **no `/oxide` directory at all** | | `Interface.Oxide.ConfigDirectory` resolves there | **CONFIRMED, indirectly and decisively** | The plugin's own config was written to **`/carbon/configs/RunicGateway.json`** by the same code that writes `/oxide/config/RunicGateway.json` on the Oxide rig. A literal path in R18 would not have found it | | Console prefix is `c.`, `oxide.` is not aliased | **CONFIRMED** | `c.version` / `c.plugins` / `c.grant` / `c.group` answered; `oxide.plugins` produced nothing | | `#if CARBON` is defined | **CONFIRMED** | `carbon/config.json` → `ConditionalCompilationSymbols: ["CARBON", "RUST", "OXIDE_PUBLICIZED"]` — and two symbols this document had not known about | | Carbon self-updates | **CONFIRMED** | `SelfUpdating.Enabled: true`, plus the egg refetching `production_build` every boot | | *"Oxide stores JSON, Carbon stores Protobuf or SQLite"* | **WRONG — see §4** | **Both** store Protobuf, under **identical filenames**, differing only in directory. The refutation strengthens R2 rather than weakening it | | The 13 uMod hook names missing from Carbon's catalogue | **NOT YET TESTED** | None is in a phase; the plugin's own fired-hook log is the standing answer either way | **Two things this document did not know to claim**, both found by looking rather than reading: Carbon ships a **third default group** (`moderator`) that R2's push must tolerate, and `c.plugins` exposes per-plugin hook telemetry Oxide has no equivalent for.