docs(modules): module-rust supports Carbon too, ships a Pterodactyl egg, and the rigs move to the panel #253

Merged
whitlocktech merged 4 commits from docs/rust-carbon-and-pterodactyl into main 2026-09-16 02:54:25 +00:00
2 changed files with 150 additions and 18 deletions
Showing only changes of commit 22159ec78f - Show all commits

View File

@@ -16,10 +16,10 @@ code has to care. Everything not listed here is identical by construction.
> 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.
> **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.
---
@@ -57,6 +57,8 @@ 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>` |
@@ -65,6 +67,11 @@ compiler simply evaluates the unknown symbol as false.
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
@@ -116,7 +123,7 @@ the two roots are obtained.
---
## 4. Permissions — same API, different persistence
## 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`,
@@ -127,6 +134,9 @@ Every member R2 depends on exists with the same name and the same argument shape
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
@@ -138,14 +148,43 @@ 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.
### The store — **this section was wrong, and the truth is worse**
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.
> **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`,
@@ -171,7 +210,17 @@ Carbon's 129 published commands are `c.`-prefixed. The ones with Oxide counterpa
| `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**.
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
@@ -305,3 +354,25 @@ framework, and that is a deliberate decision rather than an oversight.
| 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.

View File

@@ -1763,15 +1763,15 @@ including one finding that changes R20's shape.
| | |
|---|---|
| Panel | `http://192.168.0.12` (no TLS — `https://` fails outright), node 1 `Main` |
| Server | `rust-oxide`, id **17**, identifier **`e6758c06`** |
| Egg | 18 `Rust Autowipe`, `ghcr.io/pterodactyl/games:rust` |
| `FRAMEWORK` | `oxide` |
| Servers | `rust-oxide` id **17** / **`e6758c06`**, and `rust-carbon` id **18** / **`87fb1f67`** (§14.5) |
| Egg | 18 `Rust Autowipe`, `ghcr.io/pterodactyl/games:rust` — both rigs, one egg |
| `FRAMEWORK` | `oxide` / `carbon` |
| Limits | 8192 MB memory, 25600 MB disk — deliberately under half the node, so the Carbon rig fits beside it |
| Allocations | 21000 game (default), 21001 query, 21002 RCON, 21003 Rust+, **21004 held for the sidecar's `[web].bind`** |
| Allocations | oxide 21000-21004, carbon 21005-21009 — game, query, RCON, Rust+, **and one held for the sidecar's `[web].bind`** |
| World | procedural, size 3000, seed 1234 |
| SFTP | `192.168.0.12:2022` |
The RCON password is a generated 24-byte token rather than the old rig's `letmein`, kept out of this
The RCON passwords are generated 24-byte tokens rather than the old rig's `letmein`, kept out of this
document and out of the repo. §6 named that shape as the thing nothing published should copy; this is
the first rig where it was not copied.
@@ -1943,6 +1943,67 @@ string is composed so that whatever the entrypoint prepends still lands on `Rust
is more work and survives upstream changing its entrypoint; the second is free and depends on a line
in somebody else's repository. Raised rather than settled.
### 14.5 The Carbon rig, and R19 proven
`rust-carbon` — id **18**, identifier **`87fb1f67`**, same egg, same world (procedural, 3000, seed
1234), same limits, allocations **21005-21009 with 21009 held for the sidecar**, `FRAMEWORK=carbon`.
**A clean install, never a converted one**, per R21: install 122s, boot 543s, running.
**R19 is proven.** The byte-identical `RunicGateway.cs` that runs on the Oxide rig — no `#if CARBON`
anywhere in it, nothing conditional at all — loaded on Carbon **2.0.259.0** and behaved the same:
```
[INFO] Carbon 2.0.259.0 [2026.09.03.0] 21063e8 on Linux
[INFO] [Runic Gateway] protocol 1, serverId 'main', sidecar 127.0.0.1:7799
[INFO] Loaded plugin Runic Gateway v0.1.0 by RunicGateway [2367ms]
[INFO] [Runic Gateway] cannot reach the sidecar: Connection refused - retrying quietly [RunicGateway Link|26]
```
That last line is the no-stall contract holding on its **third** platform now — Windows/Mono,
Linux/Oxide, Linux/Carbon — from one source file. [`CARBON.md`](CARBON.md) §10 is the full scorecard;
the parts that change decisions are below.
**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. D3 put the plugin's config inside R18's editor;
had that editor used a literal `oxide/config/`, **it would not have found its own plugin's config on
half of all installs.** No test would have caught it; only two rigs would.
**And one claim was refuted — the one with the sharpest consequence.** `CARBON.md` had said Oxide
stores permissions as JSON and Carbon as Protobuf or SQLite, offering the difference as the reason
not to read the file. Both rigs say otherwise:
| | Oxide rig | Carbon rig |
|---|---|---|
| Path | `oxide/data/oxide.users.data` | `carbon/data/`**`oxide.users.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`** |
**Same binary format, same filenames, different directory** — and Carbon writes *its* data into files
named after Oxide. **This makes R2's API-only rule more important, not less.** A file reader would
have worked on both rigs today and broken silently for the one operator who ran `c.migrate_perms_sql`
— no error, no version marker, just a site reporting drift against a store nobody is writing any
more. The rule survives; the reasoning behind it was wrong and is now right.
Two things nobody had thought to claim, found by looking:
- **Carbon auto-creates a third default group, `moderator`**, auto-granted by auth level alongside
`default` and `admin`. R2 pushes its *full* set on connect, so it has to tolerate a group the
framework will recreate the moment it is deleted — otherwise the site reports drift for ever.
- **`c.plugins` reports per-plugin `hook fires`, `hook time`, `hook memory`, `hook lag` and
`hook exceptions`** — most of §6's "log which expected hooks have fired" mechanism, free, and only
on Carbon. Useful when debugging on Carbon; **not a substitute** for the plugin's own counter, which
must work on both.
**A warning about how a wrong console command fails here.** Pterodactyl's `command` endpoint returns
`204` whether or not anything happened, and Carbon prints nothing for an unknown command. So
`oxide.plugins` on Carbon — which is simply not a command — is indistinguishable from success at the
API. Anything driving the console has to read a log to know, which is the same conclusion §14.2c
reached about `oxide.reload` and the same shape R18's rollback needs.
---
[rl]: https://gitea.whitlocktech.com/RunicGateway/Rust-Link