Files
docs/modules/rust/CARBON.md
wtclaude 5890da633f 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
2026-09-15 20:59:23 -05:00

17 KiB

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 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 source at main, with the narrative pages at carbonmod.gg as the prose source. Carbon is upstream and wins any disagreement, exactly as uMod does for 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.

// 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:

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'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 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 §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 §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 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.