@@ -1455,7 +1455,7 @@ push, and Play (M6–M8) follow the designed app.
| Screen | Route | Reads |
| --- | --- | --- |
| **My Rust account** | `player/rust` | `GET /player/rust/links`, `POST /player/rust/link`, `DELETE …/links/{steamId}`, `GET /player/rust/permissions`, `GET /player/rust/economics` (the Coins section, 2026-10-12, [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §11.2: one card per server, "last known" when a server did not answer, the reason where it has no Economics, and left out on a 404 from an older module), `GET /player/rust/servers` + `GET /player/rust/backpacks` (the Backpack section, 2026-10-12, §11.3: one card per server, an Open button per linked account that reads once per press, the slots as pages of four-wide rows named by the game, D340, overflow marked; "not installed" on a 409, and left out with Coins on that 404) |
@@ -9,9 +9,7 @@ rig walk still to come (§5.8, D209); §3, built and walked on the rigs and the
rows still to come (§3.4); §4, built 2026-09-29 (§4.1). **§6 was spiked on 2026-09-30, and the org lead
chose neither route: Runic Gateway writes its own NPC plugin, RunicNPC, planned in
[`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md) (D214–D314), and the rest of this plan waits for it.** RunicNPC v1.0.0 shipped on 2026-10-09, and the plan
resumed with §2 the same day (D331–D334); §2 was built on 2026-10-12, and §11's Economics spike the same day
raised D335–D336 (§11.2.1). Economics was built on 2026-10-12 too (§11.2.4). The Backpacks spike followed and raised D337–D339 (§11.3.1), and Backpacks was built the same day (D340, D341, §11.3.4). The org lead then added permission rewards (§12,
D342–D346), planned the same day. **Every item left is built, tested and PR'd first; every walk —
resumed with §2 the same day (D331–D334). **Every item left is built, tested and PR'd first; every walk —
theirs and the ones still owed by §3 and §5 — happens together at the end (D331, §9).**
This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this
@@ -26,7 +24,6 @@ document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D1
| 6 | NPCs — now RunicNPC, its own plan ([`runicnpc/PLAN.md`](../../runicnpc/PLAN.md)) | §4.7 | **runicnpc-rust**, Rust-Plugins, Rust-Link, Module-Rust, installer |
| 12 | Permissions as event rewards: until the event ends, or for good | — (added 2026-10-12) | **core** (website `main`, MODULE_API 1.14.0), Module-Rust |
**Protocol.** §1, §3, §4 and §5 joined **protocol 13**, which shipped on 2026-10-09 with RunicNPC v1.0.0
(bundle 2026.10.09). The wire changes still to come, §2's and §11's, **open protocol 14** on `edge` and are
@@ -888,9 +885,6 @@ them. §6's row was never built: RunicNPC replaced route A.
| `perm.sync`: `notices` (the reward message, D333), and the report's `noticesTold` — **protocol 14** | site ⇄ game | 2.4 |
| ~~`world.place` for an NPC~~ — not built; RunicNPC (D214) | — | 6 |
| hello: `integrations.economics``{ loaded, version, startingBalance, wipesWithMap }` — **protocol 14** | game → site | 11.2.2 |
| `economics.pay`, `economics.balances`, `economics.adjust` and their replies — **protocol 14** | site → game | 11.2.2 |
| `player.tally`: `balance` (a value); `economics.wiped` — **protocol 14** | game → site | 11.2.2 |
## 8. MODULE_API 1.13.0 (core, website `main`)
@@ -994,31 +988,6 @@ The plan resumed on 2026-10-09, after RunicNPC v1.0.0, with four more:
| **D333** | **A kit reward can tell its player in game.**`rust.kit.entitle` gains an optional `message` and `delivery`; the notice rides `perm.sync` and is told once, at once or on the player's next wake; a blank message tells nobody. | The engagement notification as the only notice. | 2.4 |
| **D334** | **An optional boolean param is three buttons, [Not set] [Yes] [No]; a required one is a toggle.** | A two-state toggle everywhere, which saves No for a field nobody touched. | 2.1 |
The Economics spike raised two more, answered on 2026-10-12:
| # | Decision | Rejected | § |
|---|---|---|---|
| **D335** | **Coins paid by an event are kept.**`rust.coins.pay` is `reversible: 'none'`: a run that finishes, is cancelled or is aborted after paying leaves the coins where they are, and staff take coins back by hand from the player's admin page. | Kept on a finish but taken back on a cancel or abort (core would have to tell `revert()` how the run ended, MODULE_API 1.14.0); a per-step choice; always taken back at the end, like a kit weekend's credits. | 11.2.2 |
| **D336** | **The richest column and *Magnate* rank only balances above the server's starting balance**, read when a player connects and whenever it changes. A fresh wipe's untouched players are not ranked; a balance carried over from the last wipe is, at once. | Ranking a player only once their balance moves this wipe; ranking everyone, ties at the starting balance included. | 11.2.2 |
The Backpacks spike raised three more, answered on 2026-10-12:
| # | Decision | Rejected | § |
|---|---|---|---|
| **D337** | **Extra backpack space is a size permission leased for the run.**`rust.backpack.size` grants one of the server's `backpacks.size.N` to each recipient through `rust_perm_run_grants`, as a kit reward is granted, and the run's end revokes it. It reaches offline players, needs no experimental setting, and needs no plugin code. The highest size a player holds wins. | Backpacks' `API_AddBackpackCapacity` (+N slots), which throws unless the owner turns on *Dynamic Size (EXPERIMENTAL)*, is clamped to a size profile, and needs the player online; offering both; dropping the reward. | 11.3.2 |
| **D338** | **Taking the space back is Backpacks' own behaviour:** nothing moves at the revoke, and on the owner's next open the stacks past their size go to their inventory, or drop at their feet when it is full. Tested on both rigs at the org lead's request; the step's default message warns about it. | Holding the space until the extra slots are empty; never taking it back. | 11.3.1, 11.3.2 |
| **D339** | **The Carbon permission leak found by the spike stays out of §11.3**: filed as Rust-Plugins#32, for later. | Fixing it in §11.3; only noting it. | 11.3.1 |
The build raised two more, answered on 2026-10-12 before any code: §11.3.2's grid named an "item catalogue" the
module does not have, and R9 keeps item icons out of the asset bridge.
| # | Decision | Rejected | § |
|---|---|---|---|
| **D340** | **A backpack slot shows the item's name, not an icon.** The plugin sends each stack's English name from the game (`display`), so "Wood ×1000" and "Assault Rifle + Weapon Lasersight" need no item catalogue and nothing loads from outside the site. R9 stands. | Icons from a third-party host keyed by shortname; shortnames alone. | 11.3.4 |
| **D341** | **A blueprint names what it makes.** Backpacks stores a blueprint as `blueprintbase` with a target; the stack carries `blueprint` (the target's shortname), its `display` is the target's name, and the slot reads "Assault Rifle blueprint". | Every blueprint shown as the same anonymous "Blueprint". | 11.3.4 |
Permission rewards, added on 2026-10-12, were decided as D342–D346; they are tabled with their section, in §12.8.
## 11. First-class optional plugins (D199–D202)
The org lead named four plugins the module supports as first-class integrations. Kits already is one. The other
@@ -1042,18 +1011,15 @@ Personal GitHub mirrors are not a source: k1lly0u's mirror of Kits is two major
| Plugin | Author · version · price | API we would use | Hooks it raises | Risk |
|---|---|---|---|---|
| **Economics** | Wulf · 3.9.2 · free | `Balance(id)` → double; `Deposit`, `Withdraw`, `SetBalance(id, amount)` → bool; `Transfer` | `OnEconomicsDeposit`, `OnEconomicsWithdrawl` (sic), `OnEconomicsTransfer`, `OnEconomicsBalanceUpdated`, `OnEconomicsDataWiped` | Last released about two years ago; fine as optional, never load-bearing |
| **Backpacks** | WhiteThunder · 3.17.7 (3.17.8 at the spike) · free | `API_ReadBackpackContentsAsJson(ownerId)`; the capacity family (`API_GetBackpackCapacity`, `API_AddBackpackCapacity`, `API_SetBackpackCapacity`) | `OnBackpackOpened`, `OnBackpackClosed`, `OnBackpackDropped`, `CanOpenBackpack` | Newer builds add `EncryptedValue<ulong>` overloads; one community report of backpack data lost across a **Carbon** restart — a rig test before shipping (**not reproduced, §11.3.1**) |
| **Backpacks** | WhiteThunder · 3.17.7 · free | `API_ReadBackpackContentsAsJson(ownerId)`; the capacity family (`API_GetBackpackCapacity`, `API_AddBackpackCapacity`, `API_SetBackpackCapacity`) | `OnBackpackOpened`, `OnBackpackClosed`, `OnBackpackDropped`, `CanOpenBackpack` | Newer builds add `EncryptedValue<ulong>` overloads; one community report of backpack data lost across a **Carbon** restart — a rig test before shipping |
| **RaidableBases** | nivex · free 3.2.0 on uMod, paid on Codefling | `EventTerritory(Vector3)`, `SpawnRandomBase(type)`; listing and despawn not yet confirmed | `OnRaidableBaseStarted` (two overloads), `OnRaidableBaseEnded`, `OnRaidableBasePrivilegeDestroyed`, `OnRaidableBaseDespawn` — signatures not yet confirmed from source | Two products with different features and release lines; data path differs by framework; needs a PvE-damage plugin (TruePVE) for players to raid |
### 11.2 Economics (D200)
This is the plan as the org lead approved it on 2026-09-28. The spike (§11.2.1) changed two points of it, and
**§11.2.2 is the design to build**:
- **Coins as a reward.** A step `rust.coins.pay` gives each recipient N coins (the same recipient modes as
`rust.kit.entitle`, plus §2.5's `linked`). ~~It is ledgered per recipient like D103's kit credits, and
revert withdraws what the step paid.~~**Superseded by D335: the coins are kept.** Core reverts a ledgered
step at the end of every run, a normal finish included, so a ledgered payment would take back a prize.
`rust.kit.entitle`, plus §2.5's `linked`). It is ledgered per recipient like D103's kit credits, and
**revert withdraws what the step paid**. A withdrawal the player cannot cover, because they have spent it,
takes what is there and says so in the run log. It never goes negative.
- **Balances.** A new read verb, `economics.balances {steamIds[]}` (bounded like `kits.uses`), and the
`OnEconomicsBalanceUpdated` hook folded into the tally (rule 2: aggregates only), so the site keeps each
player's last balance per server. The player's account page and the app show it; staff see anyone's.
@@ -1061,371 +1027,19 @@ This is the plan as the org lead approved it on 2026-09-28. The spike (§11.2.1)
an activity row with the amount before and after.
- **Richest.** A leaderboard column (the last known balance) and a title condition, *Magnate* by default,
admin-renamable like every other category (D175). It is a *best* column in §5.5's sense, the current value
rather than a sum.**It ranks only balances above Economics' starting balance (D336).**
#### 11.2.1 The spike (2026-10-12)
The source is Economics 3.9.2, Wulf, pulled from `umod.org/plugins/Economics.cs` (845 lines) on 2026-10-12.
A throwaway plugin called its API and logged every hook on both rigs, on Rust 25877464 with Oxide 2.0.7826 and
Carbon 2.0.262. **Both frameworks answered identically, line for line.** The spike and Economics were then
removed from both rigs, with their data and config.
**The API** is five private methods, each with a `string` and an `object` overload (the `object` one takes a
`ulong`). Both overloads bind through `Call` on both frameworks.
| Call | Returns | Found |
|---|---|---|
| `Balance(id)` | `double` | **An account Economics has never seen reads as the starting balance** (1000 by default). Nothing is stored for it, and at load Economics deletes every account that sits exactly at the starting balance ("Remove unused accounts", on by default). So "1000" cannot tell a new player from one who earned and spent back to it |
| `Deposit(id, amount)` | `bool` | Refuses an amount of 0 or less. **Returns `true` while adding less than asked:** the balance is rounded to two places and silently clamped to the balance limit, so `Deposit(id, 0.004)` answers `true` and changes nothing. What was paid is the balance after less the balance before, never the amount asked |
| `Withdraw(id, amount)` | `bool` | **All or nothing.** It refuses an amount above the balance (unless the config allows negative balances); there is no partial withdrawal |
| `SetBalance(id, amount)` | `bool` | Refuses a negative amount unless the config allows one; rounds and clamps like `Deposit` |
| `Transfer(from, to, amount)` | `bool` | A withdrawal and a deposit. Not needed |
- **The amount must be a `double`.** `Call("Deposit", id, 10)`, with an `int`, matches no overload on either
framework: `Call` returns `null` and nothing happens. The bridge boxes every amount as `double`.
- **A string id is not checked.** `Deposit("notanid", 1.0)` stored an account named `notanid`. The bridge
sends only ids it has parsed as Steam ids.
- **Cost:** 500 `Balance` calls took 2.6 ms (Carbon) and 2.8 ms (Oxide) on the main thread.
- **Balances survive a reload** on both frameworks (`data/Economics.json`, written at unload and at every
server save).
- **What the API does not give:** the starting balance, the balance limit, and whether balances wipe with the
map. They are in `Economics.json` in the framework's config directory, which the bridge already reads for
Kits ([PROTOCOL.md](../../rust-link/PROTOCOL.md) §16.3). Economics' own chat and console commands find a player through covalence, so
they cannot reach a Steam id the server has never seen; the API can.
**The hooks**, in the order one deposit raises them:
| Hook | Arguments | When |
|---|---|---|
| `OnEconomicsBalanceUpdated` | `(string playerId, double balance)` | **Every** change, including the ones below, first. The new balance, not the change |
| `OnEconomicsDeposit` | `(string playerId, double amount)` | After a deposit; the amount asked, even when less landed |
| `OnEconomicsWithdrawl` (sic) | `(string playerId, double amount)` | After a withdrawal |
| `OnEconomicsTransfer` | `(string from, string to, double amount)` | After a transfer's withdrawal and deposit |
| `OnEconomicsDataWiped` | none, **or**`(IPlayer player)` | None at a new save when the config wipes balances; the `IPlayer` after `ecowipe`. One method taking an `IPlayer` binds both shapes on both frameworks, with `null` for the first |
**Two traps on the rigs**, neither Economics' fault: a Rust update needed **two** panel restarts on each rig,
because the first boot ran the old build with the download unfinished (`StateFlags` 1158 and 1062). And
**Carbon kept Economics loaded after its file was deleted**, until `c.unload`; Oxide unloaded it.
#### 11.2.2 The design to build (D335, D336)
**Hello.**`integrations.economics` is `{ loaded, version, startingBalance, wipesWithMap }`. The last two
come from `Economics.json`. When the file cannot be read, they are absent, the site assumes Economics'
default of 1000, and the servers page says so.
**Paying coins: `economics.pay`, site → game.** A one-shot command, because the coins are kept (D335):
Each adjustment is a core activity row: "Deposited 500 coins to *name* on *server*: 1,000 → 1,500".
- **Richest.** A leaderboard column on servers that report Economics, and a title category `richest`,
*Magnate*, whose standing is the current balance, **counted only above that server's starting balance
(D336)**. A fresh wipe's 40 untouched players at 1,000 are not on it; a player carrying 5,000 over is.
- **Every surface that needs Economics refuses with a reason where it is absent** (D199): the step's verify,
the adjust form, and the account page ("Economics is not installed on this server").
**The app (Android-app).** The Rust account screen shows the player's balances from the same route,
capability-gated like the rest of that screen (R10).
**Not touched:** the installer and the egg. `requires_plugins` stays Kits and ZoneManager (D199).
#### 11.2.3 The walk (deferred to the end, D331)
On both rigs, with Economics installed:
- pay three players from a tally, one of them offline, and see the popup on wake;
- retry the step, and see `repeat` and no second payment;
- pay at the balance limit, and see `at-limit`;
- deposit, withdraw (including more than the balance) and set from the admin page, and read the
activity rows;
- watch the richest column and *Magnate* on a fresh wipe, then with a carried-over balance;
- run `ecowipe`, and see the column clear;
- read a balance on the account page and in the app.
#### 11.2.4 Built (2026-10-12)
Built to §11.2.2 on protocol 14, and specified in [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.3. The
bridge compiled on both rigs (Rust 25877464; Oxide 2.0.7826, Carbon 2.0.262), which were then given back their
protocol 13 bridge. Six points the design left open were settled in the code, and are named here so they can
be overruled:
- **`economics.wiped` carries `newSave`.** After `ecowipe` the site clears the wipe's balances; at a new save it
clears nothing, because the new map's balances arrive as players connect, and the last wipe's stay as the
record of how it ended.
- **A balance frame with no `name` is not play.** The plugin names only a connected player, and the site writes
such a frame's balance without moving anybody's last-seen.
- **The Coins column is per wipe.** A balance is a value, so there is no all-time one: the column and
`sort=balance` exist only when one wipe of a server that reports Economics is shown.
- **A payment's `failed` reasons** are `at-limit`, `refused`, `unmeasured` (deposited, but not measured — remembered
with the key so a retry pays nothing more) and `bad-id`.
- **Payments and adjustments keep their keys apart**, so a step's key can never be answered with a staff
adjustment's reply. The admin form sends a fresh key with each submission.
- **The step spends a new `rust.coins` budget**, priced like the kit reward: the amount times the count, or
times the recipient bound for a mode that is not a count.
rather than a sum.
### 11.3 Backpacks (D201)
This is the plan as the org lead approved it on 2026-09-28. The spike (§11.3.1) changed how the extra space
is given, and **§11.3.2 is the design to build**:
- **Read, never write (R2).** A new verb, `backpacks.read {steamId}`, returns
`API_ReadBackpackContentsAsJson` reduced to shortname, amount, skin and slot. It never uses the write
counterpart, so the site cannot change a backpack.
- **Staff** open any player's backpack from the player's admin page. **A player** sees their own on the
account page and in the app, only for accounts they have linked (the player tier's usual scoping).
- **Extra space as a reward.** ~~A step `rust.backpack.capacity` adds N slots for the event's duration through
- **Extra space as a reward.** A step `rust.backpack.capacity` adds N slots for the event's duration through
`API_AddBackpackCapacity`, and takes them back at teardown, ledgered like a lease. If the player is offline
at either end, it waits for their next connection: the capacity calls take a `BasePlayer`.~~
**Superseded by D337: the step leases one of Backpacks' size permissions for the run instead.** The capacity
calls work only on a server whose owner has turned on an experimental setting (§11.3.1).
at either end, it waits for their next connection: the capacity calls take a `BasePlayer`.
- **The spike first:** the Carbon restart report. Fill a backpack on `rust-carbon`, restart, and check it.
**Done; the loss did not reproduce (§11.3.1).**
#### 11.3.1 The spike (2026-10-12)
The source is Backpacks **3.17.8** (one patch past the 3.17.7 that §11.1 names), WhiteThunder, pulled from
`umod.org/plugins/Backpacks.cs` (9,782 lines) on 2026-10-12. A throwaway plugin made stand-in players with
Steam-shaped ids (the RunicNPC 9d pattern), called the API and logged every hook, on both rigs, on Rust
25877464 with Oxide 2.0.7826 and Carbon 2.0.262. **Both frameworks answered identically everywhere the plugin
was concerned.** The spike and Backpacks were then removed from both rigs, with their data, config and lang
files, and the permissions the stand-ins held.
**The restart report did not reproduce.** A backpack changed after the last save (the save interval raised
to an hour first, so no timed save could run) was intact after a panel restart on both frameworks, for an
online owner and an offline one alike. A plugin reload kept it too. Backpacks saves at every server save and again at
unload. **The one way the spike lost a backpack** was its own: the first run killed its
stand-ins at unload, Backpacks took each kill for a death, and *Drop on death*, on by default, dropped the
contents on the ground. On Carbon, Backpacks was still loaded when the spike unloaded during shutdown; on
Oxide it was not, so only Carbon showed it. That is Backpacks working as configured: **any plugin that kills
a player entity drops that player's backpack.** The bridge kills no players.
**Reading.**
| Call | Found |
|---|---|
| `API_ReadBackpackContentsAsJson(ulong)` | A JSON string, or **`null` for a player who has never had a backpack**. It loads an offline player's backpack from disk. 0.2–7 ms once loaded; **55–117 ms the first time after a boot** (Oxide logs its own "took 116ms" warning), on the main thread |
| `API_CountBackpackItems(ulong, null)` | The number of stacks |
| `API_GetBackpackCapacityById(ulong, string)` | The capacity an id would have. **Caches it** (see the trap below) |
The JSON is a list of Backpacks' own `ItemData`, not of Rust items:
- **`ID` is the item id, not the shortname**; the bridge maps it with `ItemManager.FindItemDefinition`.
- **`Position` runs across pages**: page two of a 96-slot backpack starts at 48 ("Max size per page").
- **`Contents`** holds a weapon's attachments or a container's items, nested. `Name` is a renamed item's
name. Condition, ammo, fuel and the rest appear when they differ from the default.
**Capacity.** A player's size comes from permissions: `backpacks.use` gives the default (6), and the
highest `backpacks.size.N` they hold sets it, for each N in the config's *Permission sizes* (6, 12, 18, 24, 30,
36, 42, 48, 96 and 144 by default). Backpacks registers each one, so the bridge can list them.
- **`API_AddBackpackCapacity` and `API_SetBackpackCapacity` throw** (`InvalidOperationException`, logged with a
stack trace on both frameworks; `Call` returns `null`) unless the config's **"Dynamic Size
(EXPERIMENTAL)"** is on, and it is off by default. With it on, each player is clamped to the initial and
maximum size of a *size profile* they hold (`backpacks.size.profile.6-48` …); a player with no profile gains
nothing. They take a `BasePlayer`, so an offline player cannot be given space. This is why D337 leases a
permission instead.
- **A size permission takes effect at once, both ways, for a player whose backpack is loaded** (an online
player who has opened it, or whose backpack anything has read): granted `backpacks.size.48`, 6 became 48;
revoked, 48 became 6. An offline player's grant is simply there when they connect.
- **The trap: a capacity cached without its backpack.** Backpacks clears its capacity cache on a permission
change only for a backpack it has loaded. If anything reads an online player's capacity while their backpack
is not loaded (`API_GetBackpackCapacity*`), the value is pinned: a size granted afterwards did not show, even
after the owner opened the backpack, **until they disconnected**. Same on both frameworks. Backpacks itself
reads capacity only through a loaded backpack, so only another plugin's call can cause it. **The bridge never
reads a capacity except through a backpack it has just loaded with a read.**
**Taking space back (the org lead's test, 2026-10-12).** A stand-in held `backpacks.size.96`, six stacks on
page one and five notes on page two (slots 48–52); the permission was revoked.
| State | What happened, on both frameworks |
|---|---|
| After the revoke, before the owner opens it | Nothing moves. Capacity reads 6, and **the read still returns all eleven stacks**: the five past slot 6 wait in the backpack |
| The owner opens it, inventory has room | The five notes **moved into the owner's inventory** (main 5/24), and the backpack read six stacks. Backpacks tells the owner in chat: "Your backpack was over capacity. Overflowing items were added to your inventory or dropped." |
| The owner opens it, inventory full (30/30) | The five notes **dropped on the ground at the owner's feet** as loose items, and the backpack read six stacks |
That is Backpacks' default, the behaviour a VIP page that lapses already has, and **it stands (D338).** A
staff member looking at someone else's backpack sees the overflow too; only the owner's own open moves it.
**Hooks.**`OnBackpackOpened(BasePlayer looter, ulong ownerId, ItemContainer)` and `OnBackpackClosed` fired
for the owner's open and close; `OnBackpackDropped(ulong ownerId, List<DroppedItemContainer>)` for each
death drop. None is needed by the design.
**Two quirks the bridge never meets,** because it never writes: `API_TryDepositBackpackItem` took only the
first item into a backpack nobody had opened yet, and `API_GetBackpackContainer` returns `null` for a backpack
that does not exist yet. A page that has never been opened is sized to what it holds until its owner opens it.
**Found on the way, outside Backpacks:** on Carbon, a permission granted to a Steam id the store has no user
record for leaks onto every such id. Filed as
[Rust-Plugins#32](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins/issues/32) and kept out of
§11.3 (D339). The size lease grants through `perm.sync` like a kit reward, so it inherits whatever that issue
decides.
#### 11.3.2 The design to build (D337, D338)
**Hello.**`integrations.backpacks` is `{ loaded, version, sizes }`. `sizes` lists the N of every
- **One id per call, on the main thread.** It calls `API_ReadBackpackContentsAsJson`, and only when that
answered a backpack does it read `API_GetBackpackCapacityById`: that backpack is now loaded, so the
capacity follows later grants (§11.3.1's trap). A player who has never had one is `exists: false`, with no
capacity.
- **Each stack is reduced** to slot, shortname, amount, and skin and name when set; nested `contents` keep the
same shape, one level deep. An id no item definition knows is sent as `shortname: null` with its `itemId`.
- **`overflow: true`** marks a stack at or past the capacity: it is waiting for its owner's next open to be
moved (§11.3.1).
- **Refusals** are `backpacks.error`: `backpacks-missing`, `bad-id`.
**Extra space: a size lease (D337).** A new step, **`rust.backpack.size`**, *"Bigger backpack"*, grants one of
`integrations.backpacks.sizes` for the run, to the recipients a mode picks.
- **It is a kit reward with a different permission.** It writes one `rust_perm_run_grants` row per recipient,
with `permission` = `backpacks.size.N` and no kit. The permission mirror pushes it in the next `perm.sync`,
so a recipient who is offline has it when they connect, and a wipe cannot take it away. Its revert deletes the
step's rows, and the next sync revokes them. **So the space lasts until the run ends, a normal finish
included** (core reverts every ledgered step at the end of a run, §11.2).
- **No plugin code.** The grant, the revoke and their read-backs are `perm.sync`'s.
- **Params:** the server; the size (a select of the server's `sizes` above the smallest, refused when it is
not one of them); the recipients and count (`rust.kit.entitle`'s modes, `linked` included); and a message
and delivery, told once like a kit reward's notice (D333). The default message warns about the end:
*"Your backpack holds {size} until the event ends. Anything past your normal size then moves to your
inventory, or drops at your feet if it is full."*
- **The highest size wins.** A recipient who already holds a size at least as big gains nothing, and nothing is
taken from them at the end. The step's description says so.
- **The take-back is Backpacks' own (D338):** nothing moves at the revoke, and the overflow goes to the owner's
inventory, or the ground, the next time they open the backpack.
-`risk` and `reversible` are `rust.kit.entitle`'s (`change`, `ledger`). It spends a new `rust.backpacks`
budget, *Backpack sizes*: one per recipient, or the recipient bound for a mode that is not a count, as kit
rewards are priced.
- **It refuses where Backpacks is absent** (D199): "Backpacks is not installed on this server".
**The sidecar** adds `POST /backpacks/read`, a thin forward like `/kits/uses`. It keeps nothing.
**The website (Module-Rust).**
- **Staff** see a player's backpack on the user's admin page, per linked account and server, through
`GET /admin/users/:userId/rust/backpacks?serverId=&steamId=`. **A player** sees their own on their Rust
page through `GET /player/rust/backpacks?serverId=&steamId=`, for their own linked accounts only.
- **Live, never stored.** Each view is one `backpacks.read`; nothing is written to the database. A server that
is down or lacks Backpacks says so on the panel. Opening the panel reads once; there is no polling.
- **The grid** draws the slots as pages of `pageSize`, with each stack's icon from the item catalogue, its amount,
and an *overflow* mark.
**The app (Android-app).** The Rust account screen shows the player's own backpack from the player route,
capability-gated like the rest of that screen (R10).
**Not touched:** the installer and the egg. `requires_plugins` stays Kits and ZoneManager (D199). The bridge
uses none of Backpacks' hooks.
#### 11.3.3 The walk (deferred to the end, D331)
On both rigs, with Backpacks installed and a real player:
- open a staff view of a full backpack, a two-page one, and one that has never been used;
- read your own on the account page and in the app, and see another player's refused;
- run a *Bigger backpack* step, online and offline at the grant, and see the size on the next open;
- end the run with items on the extra page, open the backpack, and see them move to the inventory; then
again with a full inventory, and see them on the ground;
- remove Backpacks, and see the step and the panels refuse with a reason.
The walk's players must have real Steam ids: the bridge reads only a 17-digit id at or above
76561197960265728, so the spike's `7656119000000000N` stand-ins are refused `bad-id` (§11.3.4).
#### 11.3.4 Built (2026-10-12)
Built to §11.3.2 on protocol 14, and specified in [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.4. The
bridge compiled on both rigs (Rust changeset 167584; Oxide 2.0.7826, Carbon 2.0.262), which were then given back
the bridge they ran before. Before any code the grid's "item catalogue" turned out not to exist; D340 and D341
answered it. These points the design left open were settled in the code, and are named here so they can be
overruled:
- **Each stack carries `display`**, the game's English name (D340); for a blueprint, the name of what it makes
(D341). The grids on the site and in the app show it, then the shortname, then `item <id>` for an item the game
no longer knows.
- **A third refusal, `unreadable`**, for contents the bridge cannot parse. Backpacks writes the JSON itself, so it
should never happen; it is a sentence rather than an exception if it does.
- **The step's message is never silent.** Left empty, it is the default take-back warning, because a player
nobody warned is the one surprised by D338. An author can reword it, with `{size}`.
- **The size picker** offers each server's sizes above its smallest, grouped by server, from the last hello. The
step checks the pair it is given, and names the sizes the server does offer when it refuses one.
- **The panels offer every enabled server** and learn that one lacks Backpacks when the read answers `409`, so
nothing about which mods a server runs reaches the public server list.
- **The player route is rate-limited** to 20 reads a minute, because a read runs on the game's main thread.
- **A skin id is text** on the site's API: workshop ids are labels, and some pass 2^53.
- **The app gates the section on the Coins read's 404**: Backpacks and Economics ship in the same protocol, so an
older module has neither.
### 11.4 RaidableBases (D202)
@@ -1650,167 +1264,3 @@ The org lead answered §11.4.4's three questions the same day:
These are new verbs, events and hello fields. **They join protocol 14 (D332)**, which §2 opens: protocol 13
shipped on 2026-10-09, before §9 reached item 7.
---
## 12. Permissions as event rewards (D342–D346)
The org lead asked for this on 2026-10-12, during the Backpacks build: an event reward that grants **any
permission the server's plugins registered**, so a perk nobody planned for (night vision, a chat colour, a
`/remove` command) is a reward without code written for it. The picker sorts by mod, then by permission
inside the mod. It is its own item, after Backpacks, and planned first. Five decisions, D342–D346, were answered
the same day (§12.8).
Two steps come out of it. **Permissions until the event ends** lends them for the run, as a kit reward does.
**Permissions for good** gives them for keeps, as a staff member's grant in the permission manager does. Both
take several permissions in one step, which needs one small addition to core's step editor (§12.3).
### 12.1 What it builds on
Nothing below is new machinery; both steps are rows in tables the permission manager already pushes.
- **The lease is a kit reward's** (PLAN.md §29, D84). `rust.kit.entitle` and `rust.backpack.size` (D337) write
one `rust_perm_run_grants` row per recipient, the permission mirror pushes it in the next `perm.sync`, and the
step's revert deletes the rows so the next sync revokes them. The push reads the **union** of these rows and
the standing grants, so a player who already held the permission keeps it after the revert.
- **A standing grant is the permission manager's own** (§1): a `rust_perm_grants` row for a website user,
scoped to a server, reaching every account the user has linked (D28). It is `UNIQUE` on (user, permission,
scope), so granting one twice leaves the first.
- **The catalogue** is `rust_perm_catalogue`: per server, every registered permission and the plugin that
registered it (`owner`, the plugin's `Name`). A permission no plugin owns, as Carbon's built-in modules
register theirs, has no owner, and the permission manager groups it by its prefix (§1).
- **The message is D333's notice**, which rides `perm.sync` and is told once, at once or on the player's
next wake.
- **So there is no plugin code, no sidecar code and no protocol change.** Protocol 14 is untouched.
### 12.2 The picker: `rust.options.permissions` (D342)
- **Every permission in the catalogue of every enabled server, once each.** Its `group` is the plugin that
registered it. A permission with no owner is grouped by its prefix, labelled *"<prefix> (not
registered by a plugin)"*, the same rule the permission manager draws.
- **Sorted by group, then by permission inside the group**, both case-insensitively. The editor keeps a
source's order.
- **A row says where it is missing.** A permission that is not in every enabled server's catalogue carries
a row `note` (MODULE_API 1.13.0): *"Only on Main, PvP"*.
- **Not `searchable`.** A server registers a few hundred permissions; the multi-select's own filter box
(§12.3) narrows them in the browser. If a deployment ever passes the 2,000-row bound, the source says so, as
every bounded source does.
- **No blocklist and no allowlist** (D343). The bridge's own `runicgateway.*` permissions are in the list like
any other.
- **The step checks the server it is given.** Every picked permission must be in that server's catalogue; one
that is not is refused by name: *"PvP has no permission nightvision.allowed — its plugins did not register
it"*. The check runs in the dry run too.
### 12.3 The core addition: a param that holds several values (D344, MODULE_API 1.14.0)
The step editor has six param types, all single values. A step that takes several permissions needs a
list, so core gains one declaration flag. **It is an addition only**: a module that does not use it is
unchanged, and Module-uo's `coreApi: "^1.10.0"` still resolves.
- **`multiple: true` on an action param** of type `string` or `int`, with an optional `max` (1 to 100, 20 if
left out). It is refused on any other type, on a `duration` param, and on a template's inputs, which this
version does not extend.
- **The value is a JSON array**: each element checked as the type's literal, duplicates dropped, order kept, at
most `max`. `required` means at least one. A bare value is read as a list of one, so a spec written by hand
or over the API does not trip on it. The module's `perform()` and `cost()` receive the array.
- **The catalog serves `multiple` and `max`** beside the rest of the declaration (`GET
/admin/events/catalog`).
- **The editor draws a multi-select.**
- With a `source`: the chosen values as chips above a grouped checklist, with a filter box over it. Ticking
past `max` is refused with the number.
- With a `searchable` source: the typeahead adds a chip per pick.
- With no source: a tag box, a value per Enter.
- **The definition view and the run console** show a list as its values joined by commas.
- **Docs:** `website/MODULE_API.md` gains 1.14.0's entry and the flag in the declaration reference;
`website/EVENTS.md` §F names the control.
### 12.4 Permissions until the event ends: `rust.perm.lend` (D345, D346)
*"Lends the permissions you pick to the people a mode picks from this run's tally — or every linked player of
the server — until the run ends. Waits for them if they are offline. Someone who already held one keeps it
when the event ends."*
- **Params:**
- the server;
- the permissions (`multiple`, `max` 10, from §12.2);
- the recipients and count (`rust.kit.entitle`'s modes, `linked` included);
- a message and its delivery;
- **`confirm`**, a required boolean: *"I am sure I want to give these permissions as a reward"* (D343).
- **One row per recipient per permission** in `rust_perm_run_grants`, with no kit and no credit; the notice
goes on one row per recipient, so each is told once.
- **Unticked, it refuses** in the dry run and at run time, naming what it would have given: *"Tick "I am sure"
to make nightvision.allowed, betterchat.color a reward"*. Being a required toggle (D334), it cannot be left
unset.
- **`risk: 'change'`, `reversible: 'ledger'`**, as a kit reward is. Its revert and reconcile are
`rust.kit.entitle`'s, and core reverts it at the end of every run, a normal finish included.
- **The budget is new: `rust.permissions`, *Permissions lent*.** One per recipient per permission; a mode
that is not a count is priced at the recipient bound times the permissions, as kit rewards are.
### 12.5 Permissions for good: `rust.perm.give` (D345, D346)
*"Gives the permissions you pick to the people a mode picks, for good. Ending or cancelling the event does not
take them back; staff remove them in the permission manager."*
- **The same params as §12.4**, the confirm included.
- **A standing grant per website user per permission**, scoped to the step's server, with source `event` and
the note *"event run <run>, step <step>"*. A grant already there is left alone and counted as
*already held*.
- **`risk: 'irreversible'`, `reversible: 'none'`**, as `rust.coins.pay` is (D335). Like every step that changes
the world it arrives off until an admin turns it on under Actions; unlike §12.4's, its failure aborts the run
by default instead of pausing it, and Actions lists it as irreversible, so an admin enables the two
separately.
- **The budget is new too: `rust.permissions.kept`, *Permissions given for good***, priced as §12.4's.
- **Taking one back is the permission manager's.** The grant shows there with its source and note, and staff
delete it like any other.
- **The message rides a notice-only row** in `rust_perm_run_grants`: no permission, no kit, no credit. The
push grants nothing for it and tells the notice once. Nothing reverts the step, so the row stays; it is a
sentence that has been said.
### 12.6 Settled in this plan, and overrulable
- **A blank message tells nobody**, as a kit reward's does (D333), rather than a default sentence. A
permission's name (`nightvision.allowed`) means nothing to a player, so only the author can say what it does:
*"You can use /nv until the event ends"*. `{permissions}` puts the names in.
- **At most 10 permissions in one step**: enough for a perk bundle, and the price stays readable.
- **A permission the server's catalogue lacks is refused, not skipped.** The plugin may be unloaded for the
wipe, and a reward that silently gives less than it says is the worse failure.
- **The for-good grant is scoped to the step's server**, not every server. An author who wants it everywhere
adds a step per server.
### 12.7 Where the work lands, and in what order
1. **Website (core), on `main`:** §12.3, MODULE_API **1.13.0 → 1.14.0**, with its tests (`registries`,
`spec`, the editor's multi-select) and Module-uo's frozen-manifest proof against the new core, as 1.11.0's and
1.13.0's were run.
2. **Module-Rust, on `edge`**, after the website PR merges: the option source, the two actions, the two
budgets; `coreApi` `^1.14.0`; `ci/core-ref.json` re-pinned to the merge; `routes.manifest` regenerated.
3. **docs:** this section's *Built* notes; `website/MODULE_API.md`; `website/EVENTS.md`.
**Not touched:** Rust-Plugins, Rust-Link, the protocol, the installer, Android-app (the app does not author
events) and Module-uo. The Integration Kit's pin is at 1.12.0 and moves only when someone re-reads its
chapters for a newer core; it skipped 1.13.0 the same way.
### 12.8 Decisions of record
Answered by the org lead on 2026-10-12.
| # | Decision | Rejected | § |
|---|---|---|---|
| **D342** | **An event reward may be any permission the server's plugins registered**, picked from the catalogue grouped by the plugin that registered it and sorted by plugin, then permission. `rust.backpack.size` stays beside it, with its sizes and its take-back warning. | A fixed list of perks; folding the backpack step into the generic one. | 12.2 |
| **D343** | **No blocklist and no allowlist. The safety is a required "I am sure" toggle in the step**; unticked, the dry run and the run refuse, naming the permissions. | A pop-up when the step is saved (a core change); a blocklist of dangerous permissions. | 12.2, 12.4 |
| **D344** | **One step gives several permissions, through a multi-select picker**: a `multiple` flag on an action param, added to core as MODULE_API 1.14.0. | One permission per step; five fixed picker slots; a comma-separated text box. | 12.3 |
| **D345** | **A permission reward lasts until the run ends, or for good**, the author's choice. | Until the run ends only. | 12.4, 12.5 |
| **D346** | **The two durations are two steps**, `rust.perm.lend` (`change`) and `rust.perm.give` (`irreversible`), each with its own budget, because core gives an action one risk class. | One step with a "keep it" switch, rated `irreversible` throughout. | 12.4, 12.5 |
### 12.9 The walk (deferred to the end, D331)
On both rigs, with a plugin that registers a perk (NightVision or similar):
- open the picker and see it grouped by plugin, sorted inside each, a permission missing from a server noted,
and the unowned ones grouped by prefix;
- run a *Permissions until the event ends* step with two permissions, unticked (refused) and ticked; see the
perks work online and offline at the grant, and gone after the run ends, except for a player who already held
one;
- run a *Permissions for good* step; see it stay after the run ends, show in the permission manager with its
note, and go when staff delete it;
- unload the plugin and see the step refuse the permission by name.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.