docs(modules): module-rust supports Carbon too, ships a Pterodactyl egg, and the rigs move to the panel #253
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.**
|
||||
@@ -1,8 +1,10 @@
|
||||
# `module-rust` — the plan
|
||||
|
||||
**Status:** approved in outline 2026-09-15, not started. **Eighteen decisions of record, no open
|
||||
questions.** Audited against the whole contract, not just the game-facing chapters (§7); the event and
|
||||
engagement catalogues are §9 and §10; §11 is a second pass over `MODULE_API.md` itself.
|
||||
**Status:** phases 0 and 1 done, 2026-09-15. **Twenty-one decisions of record; one outstanding
|
||||
request, no open questions.** Audited against the whole contract, not just the game-facing chapters
|
||||
(§7); the event and engagement catalogues are §9 and §10; §11 is a second pass over `MODULE_API.md`
|
||||
itself. **R19–R21 (2026-09-15) added a second modding framework, a Pterodactyl egg, and moved the
|
||||
rigs off the workstation** — see [`CARBON.md`](CARBON.md) for the framework reference.
|
||||
|
||||
The [dry run](../rust-dryrun.md) designed this module on paper and deliberately did not build it.
|
||||
This is the document that builds it. Where the two disagree, this one is later and wins — but the dry
|
||||
@@ -675,9 +677,149 @@ open to everybody, so granting a permission for it rewards nobody with anything.
|
||||
kit dropdown must surface which kits are permission-gated and refuse — or at minimum warn loudly —
|
||||
on one that is not. That is a real refusal with a real reason, and exactly what R3's envelope is for.
|
||||
|
||||
### R19 — the plugin is framework-agnostic: Oxide **and** Carbon, from now rather than later
|
||||
|
||||
**Decided 2026-09-15 (org lead).** Modded Rust runs on two frameworks, not one, and `module-rust`
|
||||
supports both from the phase it first reads anything — not as a port after phase 18. The bridge
|
||||
plugin stays **one `.cs` file in the `Oxide.Plugins` namespace deriving from `RustPlugin`**, which is
|
||||
also Carbon's own documented first example, with `#if CARBON` used only where the APIs genuinely
|
||||
differ.
|
||||
|
||||
**This is affordable because the divergence is concentrated, not spread.** Carbon is not a fork of
|
||||
Oxide; it is a separate loader shipping an Oxide compatibility layer, and at the level a plugin sees
|
||||
the two are the same API. [`CARBON.md`](CARBON.md) is the reference — where it came from, what was
|
||||
read, and the honest note that **none of it has yet run on a live Carbon server.**
|
||||
|
||||
Three existing decisions take an amendment, and no decision is reversed:
|
||||
|
||||
- **R18 — paths come from the framework, never from a literal.** Carbon's config directory is
|
||||
`carbon/configs` (plural) and its data directory `carbon/data`, *and every one of Carbon's
|
||||
directories is relocatable from the command line* (`-carbon.configdir`, `-carbon.datadir`,
|
||||
`-carbon.rootdir`, and nine more). So the recursive walk is rooted at
|
||||
`Interface.Oxide.ConfigDirectory` and the directory it refuses to walk is
|
||||
`Interface.Oxide.DataDirectory`. Carbon reimplements both accessors; a hardcoded `oxide/config/`
|
||||
is wrong on Carbon *and* on an Oxide server whose operator moved things. The reasoning behind R18
|
||||
is untouched — only how the two roots are obtained.
|
||||
- **R2 — the store was never readable and now it is unreadable by construction.** Oxide persists
|
||||
permissions as JSON (`oxide/data/oxide.users.data`); Carbon persists them as **Protobuf or
|
||||
SQLite**, switchable at run time. R2 always planned to read the API, so nothing changes — but the
|
||||
file-reading shortcut is now permanently closed, which is worth saying once. §12.2's
|
||||
`PermissionExists` pre-check also survives intact: Carbon's `GrantUserPermission` returns `bool`
|
||||
where Oxide's returns `void`, so **the framework that would have told us whether the write landed
|
||||
is the one we cannot portably listen to.**
|
||||
- **R4 — `doctor` asks *which* framework, not *whether Oxide*.** The payload drops into
|
||||
`PluginDirectory` either way. One thing gets weaker: **Carbon's releases are rolling tags**
|
||||
(`production_build`, `edge_build`), not an incrementing build number, so "current enough" is a
|
||||
claim `doctor` can make about Oxide and can only approximate about Carbon.
|
||||
|
||||
**What this decision explicitly refuses.** Carbon publishes 30 hooks Oxide does not, including an
|
||||
`OnCarbon*` family mirroring its admin module's every moderation action — a tempting staff-audit
|
||||
feed, and precisely the thing that would quietly make Carbon required. **No Carbon-only hook and no
|
||||
Carbon-only convar enters a catalogue** unless it has an Oxide answer first, or is advertised
|
||||
conditionally on the connected server's framework as a deliberate decision. Likewise Carbon's native
|
||||
`Carbon.Plugins` / `CarbonPlugin` shape is not used: it is the single choice that would make the
|
||||
source Carbon-only.
|
||||
|
||||
**The one thing to hold loosely.** Thirteen hook names our uMod mirror carries are absent from
|
||||
Carbon's published catalogue ([`CARBON.md`](CARBON.md) §6). At least two look like renames rather
|
||||
than holes, and none is in a phase today. The protection is the one §6 already requires for a
|
||||
different reason — *the plugin logs which of its expected hooks have fired at least once* — which
|
||||
answers this on either framework without trusting either catalogue.
|
||||
|
||||
### R20 — a Pterodactyl egg is a Rust-Link deliverable, with the sidecar inside the game container
|
||||
|
||||
**Decided 2026-09-15 (org lead).** Most Rust servers are rented, and most rented Rust servers run on
|
||||
a Pterodactyl panel. So alongside the installer (R4) and the hand install, **a published Pterodactyl
|
||||
egg is the third supported way the shard side reaches an operator** — and it has to work *in the same
|
||||
manner* as the other two, not as a degraded variant.
|
||||
|
||||
The egg is derived from the community **"Rust Autowipe"** egg, taken as the known-good base, and it
|
||||
keeps everything that egg already gets right: the steamcmd install script, the wipe-day
|
||||
`REGEN_SERVER` / `REMOVE_FILES` mechanism, the Rust+ `APP_PORT`, and — the reason it is the right
|
||||
base — a **`FRAMEWORK` variable already offering `vanilla | carbon | oxide`**. The operator picks the
|
||||
framework at deploy time, which is R19's justification restated as a deployment fact: we do not get
|
||||
to choose.
|
||||
|
||||
**The sidecar runs inside the game's own container, and that is the load-bearing part.** A
|
||||
Pterodactyl server gets its own network namespace, so `127.0.0.1` inside it is genuinely private —
|
||||
which means **D2 survives untouched**: the game link stays loopback and stays unauthenticated,
|
||||
because loopback *is* the authentication. The startup command becomes a small wrapper that launches
|
||||
`rust-link-sidecar` and then `RustDedicated`.
|
||||
|
||||
The alternative — a second Pterodactyl server running the sidecar — was rejected for exactly that
|
||||
reason. Two containers have no shared loopback, so it would force a token and a routable bind onto
|
||||
the game link. That is the case argued at D2 and overruled; it is not reopened here.
|
||||
|
||||
Four things the egg must get right, each of which is a way to get it wrong:
|
||||
|
||||
- **A second allocation for `[web].bind`.** The sidecar's HTTP/WS side is the half the website
|
||||
reaches, so it binds to the container's assigned address on an allocation the panel hands out —
|
||||
not to loopback. The token is what guards it, exactly as on a hand install.
|
||||
- **The sidecar's database must never appear in `REMOVE_FILES`.** That variable is the wipe
|
||||
mechanism, and R12 keeps **all-time rollups across wipes**. A sidecar store swept on wipe day is
|
||||
the one failure that looks like success: the server comes back, the site repopulates, and every
|
||||
player's history is silently gone.
|
||||
- **Stop means stop the game.** The egg's stop command is `quit`, addressed to RustDedicated. The
|
||||
wrapper has to let the sidecar go down with it rather than outliving it or holding the container
|
||||
open.
|
||||
- **The plugin and the sidecar come from a release, never from a copy.** The install script fetches
|
||||
the pinned pair the same way the installer resolves a bundle — which makes the egg the third
|
||||
consumer of the protocol-pairing check, not an exception to it.
|
||||
|
||||
**It lands in phase 18, beside the installer**, because phase 18 is already "how the shard side
|
||||
reaches an operator", and one story told twice is how two stories drift apart.
|
||||
|
||||
### R21 — both Rust rigs move to Pterodactyl, because one install cannot prove two frameworks
|
||||
|
||||
**Decided 2026-09-15 (org lead).** Oxide and Carbon **cannot coexist in one install** — Oxide ships
|
||||
a patched `Assembly-CSharp.dll` and Carbon requires Facepunch's vanilla one. So R19 cannot be proven
|
||||
on `D:\rust`, or on any single server, at all.
|
||||
|
||||
Both rigs move to the existing Pterodactyl panel at **192.168.0.12** (node `Main`): one server with
|
||||
`FRAMEWORK=oxide`, one with `FRAMEWORK=carbon`, on the same egg. They are started and stopped as
|
||||
needed rather than both left running.
|
||||
|
||||
This replaces `D:\rust` as the rig of record, and it buys more than parity:
|
||||
|
||||
- **The egg gets exercised by every phase**, not only by phase 18. R20's deliverable stops being a
|
||||
thing written once at the end against a panel nobody has used.
|
||||
- **It ends the wipe-day maintenance that dominated §4.** `start.bat`'s steamcmd argument ordering,
|
||||
re-extracting Oxide after every `app_update`, checking `Assembly-CSharp.dll`'s byte size to tell a
|
||||
half-done Oxide install from a working one — all of that becomes the panel's job, through
|
||||
reinstall.
|
||||
- **It is a Linux rig.** Every previous finding came from Windows and Mono; phase 1 spent real time
|
||||
on a Mono-specific NUL-padded `SocketException.Message`. Production Rust servers are Linux, so the
|
||||
rig moving there makes findings more representative, and makes any remaining Windows-only
|
||||
behaviour something we notice rather than depend on.
|
||||
|
||||
**A Carbon rig must be a clean install, not a converted one.** Carbon migrates an Oxide install on
|
||||
first boot — it copies config, data, lang and permission files across. A Carbon rig made by
|
||||
converting the Oxide rig would start out holding the Oxide rig's state, and would prove less than a
|
||||
fresh one.
|
||||
|
||||
**The access, and the one thing it cannot do.** The panel's application API token is at
|
||||
`RunicGateway/pterodactyl_claude_api_token` (`ptla_…`). It creates and configures servers,
|
||||
allocations and users, and reads eggs. **It cannot touch files, power or console** — Pterodactyl puts
|
||||
those on the *client* API, which rejects an application key outright. So the deployment loop is three
|
||||
tiers, matched to what each is for:
|
||||
|
||||
| What | How | Why that one |
|
||||
|---|---|---|
|
||||
| **A release artefact** — the pinned plugin + sidecar pair | The egg's own install script, re-run by a panel **reinstall** | It is the path we ship. Exercising it on the rig is acceptance testing for free |
|
||||
| **Working-tree iteration** — an uncommitted `.cs` under test | A **client** API key (`ptlc_…`): `files/write`, then `command` to reload | The Pterodactyl analogue of `servuo-plugins/deploy.ps1`, and it carries the same caveat: **if something only works when the push script copies it, it does not ship** |
|
||||
| **Bulk or binary** — sidecar builds, world files | SFTP on the node, port 2022 | Where the client API's per-file write is the wrong shape |
|
||||
|
||||
**The client key is the one thing outstanding**, and it is a request rather than a decision: an
|
||||
application key cannot be widened into one, so the org lead generates it from the account page. Until
|
||||
it exists, iteration is reinstall-only — correct, and slow enough that nobody would choose it twice.
|
||||
The push script itself lives in **`Rust-Plugins`**, mirroring where `deploy.ps1` lives for ServUO.
|
||||
|
||||
## 3. Open questions
|
||||
|
||||
**None.** Both questions this section carried were closed on 2026-09-15.
|
||||
**None — but one outstanding request.** R21's iteration loop needs a Pterodactyl **client** API key
|
||||
(`ptlc_…`); the application key already on disk cannot be widened into one, and only the account
|
||||
holder can mint it. Not a question and not a decision: until it exists, pushing a file to a rig means
|
||||
a panel reinstall. Everything else in this section was closed on 2026-09-15.
|
||||
|
||||
*Clans in the base set while the Team provider reads first-party* was confirmed as the intended
|
||||
reading: complementary, not in conflict — the plugin is installed for alliances and clan chat, the
|
||||
@@ -689,48 +831,62 @@ online. The persisted pending-grant queue that question was weighing is not need
|
||||
|
||||
## 4. The test rig
|
||||
|
||||
`D:\rust` on the org lead's workstation. **Brought current in phase 0 (see §12):** build
|
||||
**25230300**, Oxide **2.0.7716**, a fresh procedural world (seed 1234, size 4000) generated for this
|
||||
wipe, and all four base plugins loaded. Its Oxide permission store holds a `default` and an `admin`
|
||||
group with one admin user, so R2's mechanism was exercised on day one and works.
|
||||
**Two servers on the Pterodactyl panel at `192.168.0.12`, one per framework (R21).** They replace
|
||||
`D:\rust`, which was the rig for phases 0 and 1 and whose findings are still recorded in §12 and §13.
|
||||
|
||||
A wipe keeps `server/server1/cfg/`. That directory holds `users.cfg`, and `users.cfg` holds the
|
||||
`ownerid` line — delete the whole identity directory and you silently remove the operator's own
|
||||
ownership along with the map.
|
||||
| | Oxide rig | Carbon rig |
|
||||
|---|---|---|
|
||||
| Panel | node `Main` (192.168.0.12), nest 4 "Rust" | same |
|
||||
| Egg | ours, derived from **"Rust Autowipe"** (panel egg id 18 is the unmodified base) | same egg |
|
||||
| `FRAMEWORK` | `oxide` | `carbon` |
|
||||
| Allocations | game, query, RCON, Rust+, **plus one for the sidecar's `[web].bind`** | same |
|
||||
|
||||
Three traps recorded here because each cost time before it was understood:
|
||||
Started and stopped as needed rather than both left running; the other servers on the node are
|
||||
shut down, which is what makes two ~20 GB Rust installs fit a 128 GB disk.
|
||||
|
||||
- **`start.bat` never updated anything — the 2026-09-15 diagnosis was wrong, corrected in phase 0.**
|
||||
The script was read as *"updates `C:\rustserver`, runs `D:\rust`"*, and the fix changed the path to
|
||||
`d:\rust\`. The path was never the problem. **steamcmd requires `+force_install_dir` before
|
||||
`+login`**, and the script had it after:
|
||||
**The Carbon rig is a clean install, never a converted one.** Carbon migrates an Oxide install on
|
||||
first boot — config, data, lang and permission files all come across — so a Carbon rig made by
|
||||
switching `FRAMEWORK` on the Oxide rig would start out holding the Oxide rig's state and would prove
|
||||
strictly less.
|
||||
|
||||
```
|
||||
steamcmd.exe +login anonymous +force_install_dir d:\rust\ +app_update 258550 +quit
|
||||
→ Please use force_install_dir before logon!
|
||||
→ Error! App '258550' state is 0x486 after update job.
|
||||
```
|
||||
**What the panel changes about how work reaches a rig.** The token at
|
||||
`RunicGateway/pterodactyl_claude_api_token` is an *application* key: it manages servers, allocations
|
||||
and users, and it **cannot write a file, press a button or run a console command** — Pterodactyl puts
|
||||
those on the client API. R21's table has the three tiers; the short version is *release artefacts
|
||||
arrive by reinstall, iteration needs a client key, bulk goes over SFTP on port 2022.*
|
||||
|
||||
So the flag was discarded, the update ran against steamcmd's own directory, and the job errored out
|
||||
every single time. It updated **no** directory, ever — which is the actual reason `D:\rust` fell a
|
||||
wipe behind, and why `C:\rustserver` sits at the *same* stale buildid rather than a newer one.
|
||||
Now reads `+force_install_dir d:\rust\ +login anonymous +app_update 258550 +quit`; the pristine
|
||||
original is kept at `start.bat.bak` and the path-only fix at `start.bat.broken-order-20260915`.
|
||||
With the ordering right, the run is a delta and takes minutes.
|
||||
**Two facts about this panel that are easy to trip over:**
|
||||
|
||||
- **Re-extract Oxide after every `app_update`.** Updating the server and re-installing Oxide together
|
||||
is the standard operator routine, not a discovery — Oxide ships a *patched* `Assembly-CSharp.dll`
|
||||
and a Steam update restores Facepunch's. Recorded here only for the mechanical detail: the update
|
||||
does **not** remove `Oxide.Core.dll` and friends, so a half-done install still *looks* Oxided while
|
||||
loading no plugins and raising no hook. Check the size rather than the directory — on build
|
||||
25230300 vanilla is 9,758,544 bytes and Oxide 2.0.7716's is 9,953,280.
|
||||
- **`C:\oxide_files` is a 2025-04-23 Oxide and must not be copied anywhere.** Oxide ships a patched
|
||||
`Assembly-CSharp.dll`; that bundle's is 6,842,880 bytes against the live 9,953,280, so copying it
|
||||
over a real install is a hard downgrade. `D:\rust` is already correct and needs nothing from it.
|
||||
- `/api/client/**` returns **403 `AccessDeniedHttpException`** for the application key — a clear
|
||||
error, but only if you are expecting it. It is not a permissions grant that can be widened.
|
||||
- The application API has **no egg-write endpoint at all** (`/api/application/eggs` is a 404; eggs
|
||||
are read through `/api/application/nests/{nest}/eggs`). Importing a new egg version is an admin-UI
|
||||
or `php artisan` operation, so the egg's release artefact is a JSON file a human imports — which is
|
||||
also exactly how an operator will consume it.
|
||||
|
||||
**Rust force-wipes on the first Thursday of the month and Oxide is rebuilt to match**, so "is the
|
||||
rig current" is a recurring question, not a one-time setup step. Every phase that touches the plugin
|
||||
re-checks it.
|
||||
### What moving off the workstation retires
|
||||
|
||||
Everything below was true of `D:\rust` and is kept only because it explains findings in §12. **None
|
||||
of it is maintenance any more** — the panel's reinstall does the same work correctly.
|
||||
|
||||
- **`start.bat` never updated anything.** steamcmd requires `+force_install_dir` **before** `+login`
|
||||
and the script had it after, so the flag was discarded, the update ran against steamcmd's own
|
||||
directory, and the job errored every single time (`Error! App '258550' state is 0x486`). That — not
|
||||
the path, which was the first and wrong diagnosis — is why the rig fell a wipe behind.
|
||||
- **Re-extract Oxide after every `app_update`.** Oxide ships a *patched* `Assembly-CSharp.dll` and a
|
||||
Steam update restores Facepunch's, but the update does **not** remove `Oxide.Core.dll` and friends
|
||||
— so a half-done install still *looks* Oxided while loading no plugins and raising no hook. The
|
||||
tell was file size: on build 25230300, vanilla 9,758,544 bytes against Oxide 2.0.7716's 9,953,280.
|
||||
- **`C:\oxide_files` is a 2025-04-23 Oxide and must not be copied anywhere** — its
|
||||
`Assembly-CSharp.dll` is 6,842,880 bytes, a hard downgrade over a live install.
|
||||
- **A wipe keeps `server/server1/cfg/`**, which holds `users.cfg` and therefore the `ownerid` line.
|
||||
Delete the whole identity directory and you silently remove the operator's own ownership along with
|
||||
the map. This one still applies — it is the game's shape, not the host's, and it is why the egg's
|
||||
`REMOVE_FILES` list is worth reading carefully rather than trusting.
|
||||
|
||||
**Rust force-wipes on the first Thursday of the month, and both frameworks rebuild to match.** "Is
|
||||
the rig current" stays a recurring question rather than a setup step; what changed is that the answer
|
||||
is now a reinstall rather than a sequence of manual steps that can half-succeed.
|
||||
|
||||
## 5. The phases
|
||||
|
||||
@@ -744,6 +900,12 @@ are events, whose catalogue is **§9**. 14 is the map. 18 is how any of it reach
|
||||
not us. The Android legs (5, 8, 11, 15) each trail the
|
||||
website surface they consume by one phase, per R10.
|
||||
|
||||
**R19 and R21 do not add a phase — they change what "done" means for several.** Both rigs exist from
|
||||
phase 3 onward, so from phase 3 a criterion is met when it is met **on both frameworks**, and a
|
||||
finding that holds on only one is a finding either way. Phase 2's release artefacts and phase 18's
|
||||
egg are the two places the second framework is visible in the deliverable rather than only in the
|
||||
proving.
|
||||
|
||||
Each phase ends with its findings written down, as every workstream here does.
|
||||
|
||||
| # | Phase | Repos | Done when |
|
||||
@@ -751,12 +913,12 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
| 0 | **The rig.** ✅ **Done 2026-09-15 — as built and findings in §12.** Updated to the current wipe (the script was fixed *again*, properly), Oxide re-laid, base set installed, the grant path proven end to end and both zone transitions observed live with a player connected. **Both criteria met** | docs | A current server boots with all four loaded, `oxide.grant` demonstrably gates something, and a test zone reports who is standing in it |
|
||||
| 1 | **Protocol 1, three skeletons, and every bundle seam at once.** ✅ **Done 2026-09-15 — as built and findings in §13.** Plugin, sidecar and module all exist and all three were exercised against the live rig; three org-lead decisions (§13.0), five defects only a running server found (§13.3), and a correction to §11.3 (§13.2). **Both criteria met** | all 3 + docs | One hello line travels game -> sidecar -> module; killing the sidecar does not stall the game; all five guards green on an untouched skeleton |
|
||||
| 2 | **Packaging and release.** `release.yml`, the install manifest, the `sha256`, the host allowlist — and a real install into a running core from a manifest URL | Module-Rust + docs | An operator installs the empty module from Admin -> Modules and it reaches `started` |
|
||||
| 3 | **The read path.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots distinct at the wire; `wipe_id` **and server id** on every row (R8); all-time rollups (R12); every board re-emitted on connect | all 3 + docs | A restarted sidecar is fully populated within one connection, and a wipe does not erase a player's history |
|
||||
| 3 | **The read path, on both frameworks.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots distinct at the wire; `wipe_id` **and server id** on every row (R8); all-time rollups (R12); every board re-emitted on connect. **First phase to run against the Carbon rig (R19/R21)** — it turns [`CARBON.md`](CARBON.md) from a source-read hypothesis into tested fact, including whether the 13 unlisted hook names are renames or holes | all 3 + docs | A restarted sidecar is fully populated within one connection, a wipe does not erase a player's history, and **the same plugin file does all of that on Oxide and on Carbon** |
|
||||
| 4 | **The first pages.** Server list as the landing page, `/rust/servers/:id` beneath it, killfeed, leaderboard; nav rows; the UI kit (`PublicLayout` `shell`, `PageHeader` props); `capabilities`; the `site.footer.status` slot (R13) | Module-Rust | The site renders the last thing each server said while every server is off |
|
||||
| 5 | **Android leg A** (R10). Capability-driven shell from `GET /api/v1/public/modules`, plus the phase-4 screens | Android-app | The app renders a Rust site it has never seen, and a UO site unchanged |
|
||||
| 6 | **Identity** (R1), and the `admin.users.detail` slot (R13) | 3 + docs | A player links an account in-game; an operator sees the Steam id inside core's own user page |
|
||||
| 7 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported | all 3 + docs | A grant made on the website gates a third-party plugin in-game, and survives a wipe |
|
||||
| 7b | **Mod configuration from the site** (R18). **Recursive** walk of `oxide/config/` (never `oxide/data/`), generated form from the live values, raw-JSON advanced tier, explicit reload target, versioned read/write, auto-reload watched on `OnPluginLoaded`, **automatic rollback** over the whole file set, path-traversal guards, secret redaction, its own permission and an audit trail | all 3 + docs | An admin flips a ZoneManager setting from the website and it takes effect; a deliberately broken config rolls itself back and says why; a nested `<Mod>/x.json` is found and reloads the right plugin |
|
||||
| 7 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported. The `PermissionExists` pre-check stays the mechanism on **both** frameworks (R19); Carbon's 14 permission hooks are tested here as a possible live drift signal, and suppressed against our own pushes if they fire | all 3 + docs | A grant made on the website gates a third-party plugin in-game, survives a wipe, and behaves the same against Oxide's JSON store and Carbon's Protobuf/SQLite one |
|
||||
| 7b | **Mod configuration from the site** (R18). **Recursive** walk of `Interface.Oxide.ConfigDirectory` — never `DataDirectory`, and never either as a literal path (R19) — generated form from the live values, raw-JSON advanced tier, explicit reload target, versioned read/write, auto-reload watched on `OnPluginLoaded`, **automatic rollback** over the whole file set, path-traversal guards, secret redaction, its own permission and an audit trail | all 3 + docs | An admin flips a ZoneManager setting from the website and it takes effect; a deliberately broken config rolls itself back and says why; a nested `<Mod>/x.json` is found and reloads the right plugin |
|
||||
| 8 | **Android leg B** (R10). Identity and permission surfaces | Android-app | A player links from the app |
|
||||
| 9 | **Teams from first-party clans** (R5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
|
||||
| 10 | **Notifications and engagement** (R7). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook — **the catalogue is §10**, including the in-game-popup question | Module-Rust + docs | The offline raid alert reaches the player whose base it was, and nobody else |
|
||||
@@ -767,8 +929,8 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
| 15 | **Android leg D** (R10). Map and events | Android-app | The map renders on a phone with the same layer gates |
|
||||
| 16 | **Discord slash commands** (R11). A small read-only set, every refusal deferred ephemeral | Module-Rust + docs | A refusal does not go public in the channel |
|
||||
| 17 | **Optional mod integrations** (R15). **BetterChat** first — leaderboard titles through `API_RegisterThirdPartyTitle`, a pull with no drift — then the uMod **Clans** adapter (alliances and clan chat, beside the provider rather than under it, R5), then others as they prove useful | Rust-Plugins + Module-Rust + docs | A server missing every optional mod still runs the module, Teams included |
|
||||
| 18 | **The installer** (R4). `--game servuo|rust`, the bundle payload as a variant, an Oxide prerequisite check in `doctor`, the protocol pairing refusal carried over | installer + docs | An operator sets a Rust server up with the released binary and nothing hand-copied |
|
||||
| 19 | **Docs, kit feedback, cutover.** `docs/`; **`.profile`** (three repos were added); **`runicgateway.com`** (a second game is a headline change); and the Integration-kit question R2 raised | docs + Integration-kit + .profile + runicgateway.com | `docs/` describes what shipped, the front door names the new repos, and R2's missing chapter is answered either way |
|
||||
| 18 | **The installer** (R4) **and the Pterodactyl egg** (R20) — the two halves of "how the shard side reaches an operator", built together so one story is not told twice. `--game servuo\|rust`, the bundle payload as a variant, a **framework** prerequisite check in `doctor` (which one, not whether Oxide — R19), the protocol pairing refusal carried over; the egg derived from "Rust Autowipe" with the sidecar inside the game container, a second allocation for `[web].bind`, the sidecar store held out of `REMOVE_FILES`, and its install script fetching the same pinned pair the installer resolves | installer + Rust-Link + docs | An operator sets a Rust server up with the released binary and nothing hand-copied; **and** a second operator imports the egg, deploys, and reaches the same place — on either framework |
|
||||
| 19 | **Docs, kit feedback, cutover.** `docs/`; **`.profile`** (three repos were added); **`runicgateway.com`** (a second game is a headline change, and Pterodactyl is a hosting claim the site can now make); the Integration-kit question R2 raised; and whether the kit owes a reader anything about **supporting two mod frameworks at once** (R19) — a shape it has no chapter for either | docs + Integration-kit + .profile + runicgateway.com | `docs/` describes what shipped, the front door names the new repos, and R2's missing chapter is answered either way |
|
||||
|
||||
### Why the lease comes before the reward action
|
||||
|
||||
@@ -825,15 +987,31 @@ than being quietly lost.
|
||||
never called, silently, with no warning at load — the single most common way a Rust plugin does
|
||||
nothing. The plugin must log which of its expected hooks have fired at least once, so a hook
|
||||
Facepunch renamed on a wipe is visible rather than mysterious. See [`README.md`](README.md) §2.
|
||||
**R19 gives that mechanism a second job:** it is also the only trustworthy answer to "does this
|
||||
hook exist on Carbon", since two published catalogues disagreeing is evidence about the catalogues
|
||||
and not about the frameworks.
|
||||
- **The cheapest way to make Carbon required is to do it by accident.** Carbon's extra 30 hooks, its
|
||||
23 extra convars and its `bool`-returning permission API are each individually useful, individually
|
||||
small, and collectively a framework lock-in nobody decided on. R19's refusal is written down
|
||||
because it will be re-argued, once per convenience.
|
||||
- **Two rigs is twice the state that can be quietly wrong.** A finding proven on the Oxide rig and
|
||||
assumed on the Carbon one is exactly the failure this project keeps finding in source-read claims.
|
||||
From phase 3, "done" means done on both, and a phase that could only check one says so.
|
||||
- **A convar that applies cleanly and does nothing.** Most game config is read once at boot and
|
||||
cached; applying it later succeeds, reads back correctly, and changes nothing. Every lease key gets
|
||||
verified live — apply, observe in the running game, restore — before it is advertised. The UO
|
||||
module surveyed 156 config reads and found roughly eight that were live.
|
||||
- **The wipe cadence is the schedule.** A monthly force wipe moves the hook list, rebuilds Oxide, and
|
||||
invalidates every ledgered resource. Phases that end near one should expect to re-verify rather
|
||||
than assume.
|
||||
- **`start.bat`'s RCON password is `letmein` in plaintext with `rcon.web 1`.** Acceptable on a
|
||||
loopback dev rig, and it must never be the shape anything published copies.
|
||||
- **The wipe cadence is the schedule.** A monthly force wipe moves the hook list, rebuilds both
|
||||
frameworks, and invalidates every ledgered resource. Phases that end near one should expect to
|
||||
re-verify rather than assume. **Carbon's self-updating and rolling release tags mean the Carbon rig
|
||||
may move under us between two runs on the same day**, where an Oxide build number at least says so.
|
||||
- **The old rig's RCON password was `letmein` in plaintext with `rcon.web 1`.** Acceptable on a
|
||||
loopback dev rig behind a home firewall, and it must never be the shape anything published copies —
|
||||
which now matters more, because the panel rigs are reachable on a LAN address and **the egg is a
|
||||
published artefact that people will copy defaults out of.**
|
||||
- **The panel is the rig and the deliverable at once.** Convenient, and a way to prove the wrong
|
||||
thing: a rig hand-tuned through the panel UI stops testing the egg. Anything a rig needs belongs in
|
||||
the egg or in the push script, never only in a server's saved configuration.
|
||||
|
||||
## 7. Contract coverage audit
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Rust — the Oxide/uMod ecosystem reference
|
||||
# Rust — the modding-framework reference
|
||||
|
||||
Reference material for the **upcoming `module-rust`**: a mirror of the uMod/Oxide documentation —
|
||||
the Rust game API *and* the game-independent plugin framework around it — captured here so the
|
||||
module can be designed and built against it without a round trip to umod.org on every question.
|
||||
Reference material for **`module-rust`**: a mirror of the uMod/Oxide documentation — the Rust game
|
||||
API *and* the game-independent plugin framework around it — captured here so the module can be
|
||||
designed and built against it without a round trip to umod.org on every question.
|
||||
|
||||
Everything below was **scraped verbatim from uMod on 2026-09-15**.
|
||||
The mirrored material was **scraped verbatim from uMod on 2026-09-15**. One file,
|
||||
[`CARBON.md`](CARBON.md), covers the *other* framework modded Rust servers run: PLAN.md **R19**
|
||||
commits this module to supporting Oxide and Carbon both, and that file records only where the two
|
||||
differ.
|
||||
|
||||
## The mirror
|
||||
|
||||
@@ -15,6 +18,7 @@ Everything below was **scraped verbatim from uMod on 2026-09-15**.
|
||||
| [`DEFINITIONS.md`](DEFINITIONS.md) | **What things are called.** 678 items (short name, id, display name) and 2,590 workshop skin ids across 104 items. |
|
||||
| [`OPERATING.md`](OPERATING.md) | **How it gets run.** The 6 operator pages — installing Oxide on a server, then installing, configuring and permissioning plugins. |
|
||||
| [`agent/`](agent/README.md) | The same facts in **machine shape** — TSV and JSONL, ~46% of the tokens. Generated in the same pass, so it cannot drift. |
|
||||
| [`CARBON.md`](CARBON.md) | **The other framework.** Where Carbon diverges from Oxide and nowhere else — file layout, the permission store, the `c.` commands, 30 Carbon-only hooks and 13 uMod names its catalogue omits. Sourced from Carbon's own metadata and source, **not yet proven on a live Carbon server.** |
|
||||
|
||||
**The one file here that is ours:** [`PLAN.md`](PLAN.md) — the schedule and the decisions of record
|
||||
for actually building `module-rust`. Everything else in this directory is copied from uMod; that one
|
||||
@@ -40,8 +44,12 @@ The dry run's central structural fact is the thing this reference serves:
|
||||
|
||||
> A ServUO shard is C# **source** the operator compiles into their own server, so our bridge plugin
|
||||
> can be anything we want. **A Rust server is a binary nobody outside Facepunch patches.** The only
|
||||
> way in is a mod — specifically an **Oxide plugin**, since Oxide/uMod is what modded Rust servers
|
||||
> run — hooking the game's own events.
|
||||
> way in is a mod — hooking the game's own events through a modding framework.
|
||||
|
||||
The dry run named that framework as Oxide, and **R19 corrected it: there are two.** Carbon runs an
|
||||
Oxide compatibility layer, so one plugin serves both and the ceiling below is the same ceiling —
|
||||
but *which* framework an operator installed is their choice, not ours. [`CARBON.md`](CARBON.md) is
|
||||
the difference list.
|
||||
|
||||
Two consequences, and they are the two halves of this directory:
|
||||
|
||||
@@ -50,8 +58,8 @@ Two consequences, and they are the two halves of this directory:
|
||||
those 477 hooks (or from a game type one of them hands you), the bridge cannot report it. That
|
||||
makes it the input to the Rust sidecar's event catalogue — the analogue of
|
||||
[`docs/link/PLAN.md`](../../link/PLAN.md) §5 on the UO side.
|
||||
2. **We are a guest in someone else's plugin framework.** Our plugin is compiled, loaded, permissioned
|
||||
and configured by Oxide, on Oxide's terms. [`OXIDE_API.md`](OXIDE_API.md) is that rulebook, and
|
||||
2. **We are a guest in someone else's plugin framework** — and we do not get to pick which one. Our
|
||||
plugin is compiled, loaded, permissioned and configured by Oxide or by Carbon, on its terms. [`OXIDE_API.md`](OXIDE_API.md) is that rulebook, and
|
||||
[`OPERATING.md`](OPERATING.md) is what the server owner has to do — which is the surface our
|
||||
deployment story has to sit on, the way
|
||||
[`installer/INSTALL.md`](../../installer/INSTALL.md) sits on top of ServUO.
|
||||
|
||||
Reference in New Issue
Block a user