Files
docs/modules/rust/CARBON.md
wtclaude 22159ec78f docs(modules): the Carbon rig, R19 proven, and one refuted claim
rust-carbon (id 18, 87fb1f67) is built on the same egg with FRAMEWORK=carbon,
a clean install rather than a converted one. Install 122s, boot 543s.

R19 is proven. The byte-identical RunicGateway.cs that runs on the Oxide rig,
with no conditional compilation in it at all, loaded on Carbon 2.0.259.0 and
behaved identically - same startup line, same no-stall retry against an absent
sidecar. That contract now holds on three platforms from one source file:
Windows/Mono, Linux/Oxide, Linux/Carbon.

R18's amendment is confirmed the best way it could have been. The plugin's own
config, written by the same Oxide-compat API on both rigs, landed at
oxide/config/RunicGateway.json on one and carbon/configs/RunicGateway.json on
the other. Had the R18 editor used a literal path it would not have found its
own plugin's config on half of all installs, and no test would have caught it.

One claim is REFUTED, and it was wrong about Oxide as well as Carbon. CARBON.md
said Oxide stores permissions as JSON and Carbon as Protobuf or SQLite. Both
rigs say otherwise: both store Protobuf, under IDENTICAL filenames
(oxide.users.data, oxide.groups.data), differing only in directory - Carbon
writes its own data into files named after Oxide. This strengthens R2's
API-only rule rather than weakening it: a file reader would have worked on both
rigs today and broken silently for the one operator who ran c.migrate_perms_sql.

Two things nobody had thought to claim, found by looking. Carbon auto-creates a
third default group, moderator, which R2's full-set push must tolerate or
report drift for ever. And c.plugins exposes per-plugin hook telemetry that is
most of section 6's fired-hook mechanism, free, and only on Carbon.

Also: a wrong console command is indistinguishable from success at the API.
Pterodactyl's command endpoint returns 204 either way and Carbon prints nothing
for an unknown command, so oxide.plugins on Carbon looks exactly like it
worked. Anything driving the console must read a log to know.

CARBON.md gains a scorecard (section 10) and drops its unproven banner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 21:51:11 -05:00

379 lines
22 KiB
Markdown

# 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", "<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 |
| `RUST` | The game is Rust |
| `OXIDE_PUBLICIZED` | Compiled against publicised Oxide assemblies |
| `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.
**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.