Lock-to-first-attacker per event via a BypassUseOwners patch; a step places its base where RaidableBases picks or at an admin-given spot with a height adjustment or exact height; a blocked spot is refused with the reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
919 lines
71 KiB
Markdown
919 lines
71 KiB
Markdown
# `module-rust` — the redesigns, planned
|
||
|
||
**Status:** plan, written 2026-09-27. **Its eleven questions were answered the same day: D188–D198 (§10).**
|
||
On 2026-09-28 the org lead added the first-class optional plugins (§11, D199–D202), and after the RaidableBases spikes, D203–D208 (§11.4).
|
||
It is step 3 of [`PLAN_FIXES.md`](PLAN_FIXES.md) §6: the six changes the org lead decided during the first
|
||
player walk, each "planned in detail before code". The org lead asked for all six in one plan (2026-09-27), so
|
||
they can be read together. No code has been written for any of them yet.
|
||
|
||
This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this
|
||
document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D188**.
|
||
|
||
| § | Redesign | PLAN_FIXES | Where the work lands |
|
||
|---|---|---|---|
|
||
| 1 | The permission manager | §4.1, D160–D163 | Rust-Plugins, Module-Rust |
|
||
| 2 | The event step editor and the kit weekend | §4.2, §4.3, D164, U-3–U-6, F11 | **core** (website `main`), Module-Rust, Rust-Plugins |
|
||
| 3 | Zones: ZoneManager's options, and a dome | §4.4, D166, D167 | Rust-Plugins (bridge and helper), Module-Rust |
|
||
| 4 | The live map's marker types | §4.5, D165 | Module-Rust |
|
||
| 5 | Chat titles: twenty-three conditions | §4.6, D172–D175 | Rust-Plugins, Module-Rust |
|
||
| 6 | NPCs | §4.7 | Rust-Plugins, Module-Rust |
|
||
| 11 | First-class optional plugins: Economics, Backpacks, RaidableBases (Kits stays required) | — (added 2026-09-28) | Rust-Plugins, Module-Rust, installer, Android-app |
|
||
|
||
**Protocol.** Every wire change here joins **protocol 13**, which is still unreleased on `edge` in all three
|
||
repos. [`PLAN_FIXES.md`](PLAN_FIXES.md) §5 already lists the inventory verb, ZoneManager's flags, zone
|
||
options and the new tally fields under it, so this adds no bump (§7).
|
||
|
||
**Core.** §2 is mostly core's editor, which is game-agnostic and serves Module-uo too. Its contract
|
||
additions are **MODULE_API 1.11.0 → 1.12.0**, additions only (§8). The same proof website#209 ran for 1.11.0
|
||
applies: Module-uo's frozen-manifest job and its suites, run against the new core.
|
||
|
||
---
|
||
|
||
## 0. What was checked before writing
|
||
|
||
Each of these was a question PLAN_FIXES left open or a claim this plan depends on. All were checked on
|
||
2026-09-27 against the two rigs (`rust-oxide`, `rust-carbon`) or the code. The rig checks used a read-only
|
||
probe plugin that wrote one data file. It was unloaded and deleted afterwards on both rigs.
|
||
|
||
| # | Question | Answer | Where it lands |
|
||
|---|---|---|---|
|
||
| 0.1 | Can the bridge learn which plugin registered a permission through public API? | **Yes, on both frameworks.** `permission.PermissionExists(name, owner)` takes the owning plugin (Oxide: `Plugin`; Carbon: `BaseHookable`). Asking it of every loaded plugin named an owner for all 85 permissions on Oxide (`RustCore` owns `oxide.*`) and all but 30 on Carbon, in 3.3 ms and 1.8 ms. **ZoneManager owns all 70 `zonemanager.*` names, `zonemanager.ignoreflag.nokits` included**, which uMod PermissionsManager files under Kits. The 30 Carbon names without a plugin owner are `adminmodule.*`, which belong to Carbon's built-in AdminModule (a Carbon *module*, not a plugin). | §1.2 |
|
||
| 0.2 | Can the bridge list every user in the store? | **No.** Neither framework has a public "all users" call (`GetUsers` and `GetAllUsers` do not exist on either). The complete holder list comes from `GetPermissionUsers` for each permission plus `GetUsersInGroup` for each group. That took 1.7 ms and 2.2 ms on the rigs' tiny stores, but each call walks every user, so on a real store the cost is permissions × users. | §1.2 (sliced) |
|
||
| 0.3 | Do groups carry anything the site does not model? | **Parents.** `GetGroupParent` answers on both. The site's `rust_perm_groups` has no parent column. | §1.4 |
|
||
| 0.4 | Is there a framework hook for a finished Rust mission (D173)? | **Yes: option 1 of D173's spike.** Oxide's patched `Assembly-CSharp.dll` raises `OnMissionSucceeded`, `OnMissionFailed`, `OnMissionStarted` and `OnMissionAssigned`, and `Carbon.Hooks.Oxide.dll` patches the same four names onto `BaseMission`. Rust's own methods are `BaseMission.MissionSuccess(MissionInstance, BasePlayer assignee)` and `MissionFailed(MissionInstance, BasePlayer, MissionFailReason, bool)`. The catalogues [`HOOKS.md`](HOOKS.md) and [`CARBON.md`](CARBON.md) predate them. The exact hook arguments are confirmed when a person finishes a mission (§5.7). | §5.7 |
|
||
| 0.5 | Is ZoneDomes' API safe with a null player (PLAN_FIXES §4.4, §7)? | **No.** `AddNewDome` and `RemoveExistingDome` end with `player.ChatMessage(...)` and no null check. With a null player the dome is made and saved, and then the call throws. On boot, ZoneDomes also drops every dome whose zone does not exist yet, and the bridge's temporary zones are re-created after that. | §3.2 |
|
||
| 0.6 | Can ZoneManager's flag list be read at run time? | **Only from inside.** The list is `ZoneManager.ZoneFlags.NameToIndex`, a public static on a nested type. No hook or `Call` method returns it. The zone helper (`RunicGatewayZones.cs`) already references ZoneManager's types, and the bridge may not (R2, D168). | §3.1 |
|
||
| 0.7 | What does the map actually show? | **85 markers on the rigs' map (world 3000, seed 1234): 31 substations in four prefab kinds, 7 caves in six, 7 train-tunnel entrances in four, and 5 water wells in three.** The label (`Substation`) groups the variants. Rust also classifies monuments itself (`MonumentInfo`'s `MonumentType`: Cave, WaterWell, Lake, Mountain, Radtown, Building, Town, Airport, Lighthouse). | §4 |
|
||
| 0.8 | Can the site see how much of a kit players have used? | **One player at a time.** `kits.list` already returns each kit's `max` (MaximumUses) and `cooldown`, and the module ignores `cooldown`. Per-player use is read only during a credit settle (`GetPlayerKitUses`). | §2.6 |
|
||
| 0.9 | Where do the Ultima Online examples a Rust admin sees come from (U-3)? | **Core's own actions.** `core.lease` gives `uo.rate.skillgain` as its example, and the announcement actions talk about Britain and Cove. | §2.1 |
|
||
| 0.10 | What free NPC plugins exist (§4.7)? | **HumanNPC** (Remod/RFC1920) spawns NPCs with a name and a Kits kit (`SpawnHumanNPC(position, rotation, name, clone)`, `SetHumanNPCInfo`), but it **requires two more plugins, Pathfinding and Waypoints**, and its NPCs run its own AI, not Rust's. **NPC Loadouts** (VisEntities) dresses Rust's own NPCs from configuration, for every NPC of a type rather than one spawn. The bridge's `rust.npc.place` already spawns Rust's own scientists, and a scientist is a `BasePlayer`, so it has an inventory and a `displayName`. | §6 |
|
||
|
||
---
|
||
|
||
## 1. The permission manager (§4.1; D160–D163)
|
||
|
||
### 1.1 In one paragraph
|
||
|
||
Today the site pushes the set it authored, and the plugin reports "foreign" holders only for the names the
|
||
site claims (D31). After this change, **the plugin reads the whole store and the site keeps all of it**. The
|
||
first read of a server imports everything (D160). After that, every difference between the game and the site
|
||
is either something the site changed, which it pushes as it does now, or something changed in the game. An
|
||
in-game change is settled by the server's policy: auto-adopt, adopt, or revoke (D161). The screen is rebuilt on
|
||
PermissionsManager's flow (D162), and every subject is shown with its linked account and in-game name
|
||
(D163).
|
||
|
||
### 1.2 The inventory (plugin, protocol 13)
|
||
|
||
A new request/reply verb, **`perm.inventory`**, returns the whole store in the same shape on both frameworks:
|
||
|
||
- **`permissions`**: every registered name with its **`owner`**, the plugin that registered it, found with
|
||
`PermissionExists(name, plugin)` over `plugins.GetAll()` (§0.1). A name no plugin owns is sent with no
|
||
owner. On Carbon those are the built-in modules' names, and the site groups them by prefix under
|
||
"Carbon modules".
|
||
- **`groups`**: name, title, rank, **parent**, and the permissions the group carries directly
|
||
(`GetGroupPermissions(name, false)`; inherited permissions are the parent's, not the group's).
|
||
- **`users`**: for each Steam id holding anything, the permissions held directly, the groups (**except
|
||
`default`**, which every connected player is in by framework rule and which the site treats as "everyone"),
|
||
and the last name the store recorded. The name comes from the store's own `id(name)` entry.
|
||
- **`leased`**: the group permissions an event lease is holding. The site excludes them from the
|
||
in-game-change classification the way the foreign scan excludes them today (PLAN.md §27.3).
|
||
|
||
**Cost.** §0.2 means the read is permissions × users. It is **built in slices on the main thread**, one group
|
||
of permissions per tick, the same drain the sync already uses (`PermOpsPerStep`). It is then **sent in pages**
|
||
below 768 KiB, the way `map.fetch` stays under the sidecar's 1 MiB line cap. The site asks for page 0 and
|
||
follows `more`. A snapshot is taken once and paged from memory, so a change between pages cannot tear it. The
|
||
build walk measures a large store: the plan's ceiling is 20,000 users × 500 permissions under 2 s of drain,
|
||
with no single tick over 20 ms.
|
||
|
||
**What leaves the wire.** The sync's `managed` list and the plugin's foreign scan (`ScanPermission`,
|
||
`ScanGroup`) are replaced by the inventory. Protocol 13 is unreleased, so they are removed rather than kept
|
||
beside it. `perm.sync` groups gain **`parent`**, applied with `SetGroupParent` on both frameworks.
|
||
|
||
### 1.3 Who can hold a permission on the site
|
||
|
||
Grants and memberships are keyed by **website user** today (D28), and adopt refuses a Steam id with no account
|
||
("there is nobody to author this against"). D160 makes the site the owner of **every** holder, including the
|
||
many who never link. So the site gains **Steam-keyed holders**:
|
||
|
||
- `rust_perm_steam_grants (steam_id, permission, scope, source, …)` and
|
||
`rust_perm_steam_members (group_id, steam_id, …)`, beside the existing user-keyed tables.
|
||
`buildDesired` unions both, and they can overlap; the existing `seenGrant` de-duplication already handles
|
||
that.
|
||
- **The import writes Steam-keyed rows, even for a linked player (D188).** They are an exact copy of what the
|
||
game holds. A user-keyed row would reach every Steam account the person has linked (D28), which would widen
|
||
what the game held.
|
||
- **A toggle on the screen for a linked player writes a user-keyed row** (it reaches all their accounts, as
|
||
today). A toggle for an unlinked player writes a Steam-keyed row.
|
||
- Linking or unlinking an account moves nothing. A Steam-keyed row stays about that game account, and the
|
||
screen shows it under the person's name once they link.
|
||
|
||
### 1.4 Groups across servers
|
||
|
||
A group is one name fleet-wide today (`rust_perm_groups.name` is the primary key), with one scope, and
|
||
**everything it carries or contains is the same on every server in that scope**. Real servers disagree: each
|
||
has its own `default` and `admin`, and a `vip` on one server can carry different permissions from a `vip` on
|
||
another.
|
||
|
||
**D189: a group belongs to one server, unless an admin shares it.** The same group on several servers, or on
|
||
the whole fleet, is allowed, but it is never assumed:
|
||
|
||
- **A group is a row with its own id**, `rust_perm_groups (id, name, title, rank, parent, shared)`. What it
|
||
carries (`rust_perm_group_permissions`) and who is in it (the member tables) belong to that row, so they
|
||
are the same on every server the group is on.
|
||
- **Which servers it is on** is `rust_perm_group_servers (group_id, server_id)`, or `shared = '*'` for every
|
||
server, including servers added later. A server never has two groups of one name; the model refuses it.
|
||
- **The import makes one group per server**, even when two servers have a group of the same name with the
|
||
same contents. Nothing is merged by guessing.
|
||
- **Sharing is an admin's act.** "Use this group on other servers" picks servers, or all of them. When a chosen
|
||
server already has its own group of that name, the screen shows the difference and the admin picks which
|
||
version wins; the other is replaced. "Stop sharing" gives each server its own copy.
|
||
- `parent` names a group on the same server. A shared group's parent must be on every server the group is on.
|
||
- **Built-in groups** (`default` and `admin` on both frameworks, plus `moderator` on Carbon) are imported and
|
||
editable, and **never retired**: the site does not send `RetireGroup` for them. `default`'s membership is
|
||
never modelled (§1.2).
|
||
- **Existing installs:** a group scoped `*` today becomes a group shared with every server, and one scoped to a
|
||
server becomes that server's group. What each server receives does not change.
|
||
|
||
### 1.5 The sync, rebuilt on three sets
|
||
|
||
`rust_perm_pushed`'s own comment names the three sets. Until now the plugin could only compute the third one
|
||
for the names the site claimed. The inventory gives the site all three:
|
||
|
||
| In the game | Pushed (the site put it there) | Desired (the site authors it) | Means |
|
||
|---|---|---|---|
|
||
| yes | no | no | **added in the game** |
|
||
| no | yes | yes | **removed in the game** |
|
||
| no | no | yes | to apply (today's path) |
|
||
| — | yes | no | to retire (today's path) |
|
||
| yes | no | yes | landed by someone else; recorded as pushed |
|
||
|
||
One sync becomes: **inventory, then classify, then apply the policy (§1.6), then push** the desired set and the
|
||
retirements with the existing `perm.sync`. D32's shape is unchanged: whole set, one verb, the plugin
|
||
reconciles. Triggers are unchanged too. The drift hooks still mark the server dirty, so an in-game change is
|
||
settled within one tick (30 s) plus the read.
|
||
|
||
**The first import (D160).** A server with no completed inventory yet (new column
|
||
`rust_perm_sync.imported_at`) takes everything present as site-owned, whatever its policy, and records it
|
||
as pushed. Policies apply only to changes after that. **This includes existing installs.** The first inventory
|
||
after the upgrade imports every current hand edit, including the drift rows now waiting for an answer, and
|
||
clears those rows (D198, confirming D160 as written).
|
||
|
||
### 1.6 The policy (D161)
|
||
|
||
`rust_servers.perm_policy`, one of `auto-adopt` (the default), `adopt`, or `revoke`, set on the permission
|
||
screen's server header.
|
||
|
||
| Policy | Added in the game | Removed in the game |
|
||
|---|---|---|
|
||
| **auto-adopt** | written as the site's own, for that server only: a Steam-keyed grant scoped to it, or a change to that server's group, with `source: 'adopted'`, and recorded as pushed | the site stops giving it **on that server only** (below) |
|
||
| **adopt** | a row on the "needs a person" list, with Adopt and Revoke (today's drift answers) | a row on the list, with "Accept the removal" and "Put it back" |
|
||
| **revoke** | queued as a revocation and removed at this sync | pushed back at this sync (nothing new: the desired set re-applies it) |
|
||
|
||
**D190: an in-game change touches that server and nothing else, even when the site's row reaches more
|
||
servers.** Auto-adopt acts on it without asking:
|
||
|
||
- **A grant that reaches several servers** (scoped `*`, or a user grant) and is removed on one gains an
|
||
**exception** for that server: `rust_perm_exceptions (kind, subject, object, server_id)`. It keeps
|
||
reaching every other server, including servers added later. `buildDesired` skips an excepted row on that
|
||
server. The screen shows it as "all servers except `rust-carbon`", and removing the exception gives it back.
|
||
- **A group shared across servers** that changes on one server (a permission or a member added or removed in
|
||
the game) is **split**: that server gets its own copy of the group with the change applied, and the shared
|
||
group stops covering that server. The other servers see no change. The "needs a person" list records the
|
||
split, so an admin who wanted the change everywhere can apply it to the shared group and share it back.
|
||
- A change to a row that is **that server's alone** is adopted directly: a removal deletes the row, and an
|
||
addition writes one.
|
||
|
||
### 1.7 The screen (D162, D163, U-1)
|
||
|
||
Following PermissionsManager 2.1.2, the reference screenshots in the workspace's `perms-screenshots/`:
|
||
|
||
- **A server picker first.** PermissionsManager works on one server, and the site holds several. Everything
|
||
below is for the chosen server. A grant that reaches more servers shows where it reaches ("all servers",
|
||
"all except `rust-carbon`"), and a shared group says which servers share it. Changing either asks whether to
|
||
change it everywhere or on this server alone. "This server alone" makes an exception or splits the group, as
|
||
D190 does for an in-game change.
|
||
- **Players ⇄ Groups**, as two tabs. The players list shows the server's holders from the inventory first,
|
||
then a search over every player seen on that server and every linked account. Each row
|
||
shows the **in-game name**, the **linked website account** if there is one, and the Steam id when the
|
||
server has no cached name (D163).
|
||
- **A subject, then its plugins.** The plugin buttons come from the inventory's `owner` (§0.1), never from
|
||
the name's prefix. A button shows a count of what the subject holds from that plugin.
|
||
- **A plugin's permissions, with Granted / Revoked**, plus Grant all and Revoke all. **Each toggle carries its
|
||
own state on this server** (U-1): *granted*; *through group `vip`* (read-only, with a link to the group);
|
||
*waiting for their first connection* (a member the store cannot place yet); *not registered here* (the
|
||
plugin is not loaded, formerly "unresolved"); or *did not land* (D85).
|
||
- **A player's groups** (add, remove, Remove all). **A group's players** (Remove all), its title, rank and
|
||
parent, and the servers it is shared with (§1.4).
|
||
- **The server header:** sync state, the policy, and the "needs a person" list (§1.6).
|
||
- **`zonemanager.ignoreflag.*`** (the question PLAN_FIXES §4.1 left open): **D191, listed like any other
|
||
permission.** They are ZoneManager's 69 exemptions, under ZoneManager's button with their own names, the
|
||
same as its seventieth, `zonemanager.zone`.
|
||
|
||
The existing account-first view (what one website account holds across the fleet) stays, on the account's own
|
||
page, because that is where the events' grants (D84) and the chat styles (phase 17) are read.
|
||
|
||
### 1.8 The walk
|
||
|
||
On both rigs: install on a server with hand-made state (a `vip` group with a parent, two direct grants, one to
|
||
an unlinked Steam id), and check that the import shows it exactly, as two separate `vip` groups when both rigs
|
||
have one. Then an `oxide.grant` / `c.grant` under each of the three policies. Then, under auto-adopt, an in-game
|
||
revoke of a server-only grant (deleted), of a grant reaching all servers (an exception, still on the other
|
||
rig), and a change to a shared group (split off, the other rig unchanged). Then share a group, stop sharing it,
|
||
and Grant all / Revoke all. The plugin buttons must be by owner: `nokits` is under ZoneManager. The
|
||
large-store measurement is in §1.2.
|
||
|
||
### 1.9 As built, and as walked (2026-09-28)
|
||
|
||
Rust-Plugins `feat/perm-inventory`, Rust-Link `feat/perm-inventory`, Module-Rust `feat/perm-manager`. The
|
||
wire is [`PROTOCOL.md`](../../rust-link/PROTOCOL.md) §19.9, and the admin API is the module's swagger
|
||
fragment (`/admin/rust/permissions…`).
|
||
|
||
**What the build settled that the plan left open:**
|
||
|
||
- **The data model.** Groups are rows with ids: `rust_permgroups`, plus `_servers` (with `included = 0`
|
||
to take one server out of an all-servers group), `_permissions`, `_members`, `_steam_members` and
|
||
`_chat`. The chat style belongs to a group row now, because one server's `vip` may be styled
|
||
differently. Steam-account grants are `rust_perm_steam_grants`, and D190's exceptions are
|
||
`rust_perm_exceptions`. `rust_servers.perm_policy`, `rust_perm_sync.imported_at`,
|
||
`rust_perm_catalogue.owner` and `rust_perm_drift.direction` are new columns. The old group tables
|
||
are copied once at boot (a `rust_settings` marker makes it once) and are then left unread.
|
||
- **Every sync is read → reconcile → push.** The reconcile step runs under one fleet-wide lock, so two
|
||
servers never split one shared group at once. Each desired row remembers the authored rows that
|
||
produced it (its *sources*). That is how a removal knows whether to delete a row, add an exception, or
|
||
split a group.
|
||
- **Two things are never judged.** A permission the server does not register right now (an unloaded
|
||
plugin is not a revocation), and a pair an event lease holds.
|
||
- **A removal at the first import is pushed back**, not deleted. D198 imports what is present; a
|
||
snapshot taken the moment the site first looked is not taken as a revocation.
|
||
- **An event's grant removed in the game** is pushed back under auto-adopt, with a notice. The event
|
||
owns it, and its revert withdraws it.
|
||
- **Under `adopt`**, a removal or a changed group is *held*: it is not pushed back, retired or recorded
|
||
until a person answers. "Put back" works by forgetting the ledger row, and the next sync pushes it.
|
||
- **A group the site already authored keeps the site's title** at import. `walkvoice`, made by the site
|
||
before the rebuild, overwrote the game's empty title with "Walk voice" on Oxide.
|
||
- **The announcement voice names a group by id.** A setting saved as a name before the rebuild still
|
||
resolves, to the first styled group of that name.
|
||
|
||
**Walked** on `rust-oxide` and `rust-carbon`, against the walk core on `rustp16` (backed up first):
|
||
|
||
| Case | Result |
|
||
|---|---|
|
||
| Migration of the old groups | 2 copied (`site` → Oxide only; `walkvoice` → every server) |
|
||
| First import (D198) on an existing install | Oxide: `default`, `admin` made its own, 2 hand grants → Steam grants, the site's own grants recorded as landed; Carbon: `default`, `admin`, `moderator` with titles kept byte for byte |
|
||
| Auto-adopt: `oxide.grant` + `oxide.revoke` | adopted within one 30 s tick; the revoked Steam grant deleted |
|
||
| Fleet grant (`*`) revoked in the game on Oxide (D190) | an exception for `rust-oxide`; still pushed and landed on Carbon |
|
||
| `oxide.unload Kits` | nothing deleted; the report lists both `kits.*` as unresolved |
|
||
| `c.grant group walkvoice kits.admin` on Carbon (D190) | split: Carbon got its own `walkvoice` with the change and the style; the shared one excludes Carbon; a notice waits |
|
||
| Policy `adopt` | one addition and one removal waited; the removal was held, not pushed back; adopt and accept answered them |
|
||
| Policy `revoke` | an in-game grant undone at the next sync (`revokes: 1`) |
|
||
| Server view | plugins by owner (`nokits` under ZoneManager), groups with where they are, players named, the exception listed |
|
||
|
||
**Not walked yet:** a group with a parent; sharing and unsharing from the screen, and its conflict answer;
|
||
Grant all / Revoke all; the screen itself in a browser (every endpoint behind it was called); and the
|
||
large-store measurement.
|
||
|
||
---
|
||
|
||
## 2. The event step editor and the kit weekend (§4.2, §4.3; U-3–U-6, F11; D164)
|
||
|
||
Most of this is **core's editor** (`EventEditor.jsx`), so it lands on website `main`, stays game-agnostic,
|
||
and must leave Module-uo exactly as it is.
|
||
|
||
### 2.1 A step becomes the thing it does (U-3, U-4, F11)
|
||
|
||
- **No type tags.** `ParamField` stops printing ` · {type}` and the bare `*`. A required field says so in
|
||
words only when it is empty at save time.
|
||
- **Each type gets its control:** a toggle for `boolean`, a number field with the declared range beside it for
|
||
`int` and `float`, a date picker for `datetime`, and a text box only for `string` without a source.
|
||
- **`core.lease` renders the chosen lease's own form.** The lease is picked by its **label**. The step then
|
||
shows the value as the lease's type (a toggle for a boolean lease, a number with the lease's `min`–`max`
|
||
beside it, or a dropdown when the lease declares `values`), **the target as a picker over the lease's own
|
||
`target.source`** (F11, never built: today the editor reads only `param.source`), and the minutes with the
|
||
lease's `maxDurationMs` shown beside them.
|
||
- **Examples.** Core's own examples become game-neutral (§0.9). A lease may declare an optional `example` for
|
||
its value (**MODULE_API 1.12.0**, §8), and `target.example` already exists, so the examples on a lease step
|
||
are that lease's.
|
||
|
||
### 2.2 A phase that holds a timed step (U-6)
|
||
|
||
A phase with no advance rule ends when its steps finish, and teardown gives a lease back at once. So the
|
||
"10 minutes" of a lease lasted one second. The fix is to make the duration visible:
|
||
|
||
- A param may declare **`duration: 'minutes'`** (**MODULE_API 1.12.0**). `core.lease`'s `minutes` is one,
|
||
and so are `rust.zone.open`'s `minutes` and the other modules' equivalents.
|
||
- When a timed step is added to a phase that has **no advance rule**, the editor gives the phase
|
||
**`advance: { after: '<the longest duration in the phase>m' }`**, visibly, and the author may change it.
|
||
- A phase whose advance rule ends before its longest timed step says so under the phase: "This phase ends
|
||
after 5 minutes; the lease is given back then, not after 10."
|
||
|
||
### 2.3 Start with an unpublished edit (U-5)
|
||
|
||
When the definition has a saved draft newer than its published version, **Start** asks: "Publish and start",
|
||
or "Start version *N* (the published one)". Core client only. `runs.create` already pins the published
|
||
version.
|
||
|
||
### 2.4 Popups (§4.3)
|
||
|
||
`rust.announce`'s `delivery` already has two values from an option source. With two or three choices the
|
||
editor draws them as a segmented control rather than a dropdown.
|
||
|
||
**D193: a zone's enter and leave messages and a player's "reward earned" message get the same Chat / Popup
|
||
choice.**
|
||
|
||
- **Zones.** `rust.zone.open` gains `enterMessage`, `leaveMessage` and `delivery`. The bridge no longer passes
|
||
the messages to ZoneManager for its own zones. It subscribes to ZoneManager's `OnEnterZone` and `OnExitZone`
|
||
and sends them itself, as chat or as a popup, only for the zones in its registry. This also gives F12's
|
||
"row 5 cannot show an enter message" (PLAN_FIXES, the step-2 walk) a message to show.
|
||
- **Rewards.** The reward actions' message to the player (`rust.kit.entitle` and the other `eventRewards.js`
|
||
steps that tell a player what they won) gains the same `delivery`.
|
||
- **Without PopupNotifications** a popup falls back to chat. The step editor says so beside the choice when the
|
||
server's hello reports PopupNotifications absent, as `rust.options.delivery` already does.
|
||
|
||
### 2.5 The kit weekend as a template (D164)
|
||
|
||
There is no template concept anywhere today. **D192: core gains `api.registerEventTemplates`**
|
||
(**MODULE_API 1.12.0**), which any module can use: a module declares a named skeleton (phases, steps, params filled in, and a short list
|
||
of the inputs the author supplies). The editor's "New event" screen offers the skeletons, asks for their
|
||
inputs, and opens an ordinary draft. Nothing about a template survives into the definition, so it is purely an
|
||
authoring shortcut and the runner never sees it.
|
||
|
||
Module-Rust registers **"Kit weekend"**. Its inputs are a server, a kit and a duration, and it makes one phase
|
||
with `advance: { after }` equal to the duration and two steps:
|
||
|
||
1. `core.lease` → `rust.group.permission`, target `<server>/default/<the kit's permission>`, value `true`,
|
||
for the duration. The kit becomes visible and redeemable for everyone.
|
||
2. `rust.kit.entitle` → that kit, **every linked player on the server**, one use each (D103: withdrawn on
|
||
revert). This recipient mode does not exist yet. Today's modes are all about a run's scoring ("everyone who
|
||
scored", top N, and so on), so Module-Rust adds **`linked`**.
|
||
|
||
### 2.6 The kit warning (D164)
|
||
|
||
"N linked players have already used this kit" needs a bulk read the plugin does not have (§0.8). So there is a
|
||
new verb, **`kits.uses`**: `{kit, steamIds[]}` in, and `{max, cooldown, exhausted, onCooldown}` out (counts,
|
||
not a list). It is main thread, capped at the site's linked-player count for that server (the same bound as
|
||
the sync's).
|
||
|
||
The warning reaches the core editor through an **optional `note` on an option row** (**MODULE_API 1.12.0**).
|
||
When the chosen option carries one, the editor shows it under the field. `rust.options.grouppermissions`
|
||
attaches the note to a `kits.*` target: "Kit `vip`: 2 uses, 24 h cooldown — 9 of 41 linked players have used
|
||
it up. The weekend's extra use (step 2) is what lets them take it again."
|
||
|
||
### 2.7 The walk
|
||
|
||
Author a kit weekend from the template on each rig; a lease step with the phase's advance removed (the warning
|
||
shows); a draft edited after publishing (Start asks); a boolean lease (a toggle); and a Module-uo event opened
|
||
in the same editor, which must be unchanged apart from the controls.
|
||
|
||
---
|
||
|
||
## 3. Zones: ZoneManager's options, and a dome (§4.4; D166, D167)
|
||
|
||
### 3.1 The options (D166)
|
||
|
||
- **Read from the installed ZoneManager, by the helper.** §0.6 means only code that references ZoneManager's
|
||
types can read the flag list, and D168 puts that in the helper, not the bridge. `RunicGatewayZones.cs` gains
|
||
a hook, `RgZoneOptions()`, returning ZoneManager's version and `ZoneFlags.NameToIndex`'s names. The bridge
|
||
calls it and adds **`zoneManager: { version, flags[] }`** to hello. **Without the helper** the site offers
|
||
only a name, radius and duration, and says why, the same way D182 degrades.
|
||
- **Settings offered:** radiation, comfort, temperature, safe zone and permission. The enter and leave
|
||
messages are the bridge's own (D193, §2.4). The bridge keeps control of the id, location, radius and size,
|
||
and the rest (rotation, parent, eject spawns) stays out, because each one changes what the bridge believes
|
||
the zone is.
|
||
- **Wire:** `world.zone` gains `flags` (names) and `settings` (key → value). The plugin checks every flag
|
||
against the live list and every setting against the allowlist, and refuses an unknown one with a new reason,
|
||
`bad-option`. It passes them to `CreateOrUpdateTemporaryZone` as ZoneManager's own key/value arguments,
|
||
stores them on the zone's registry entry, and re-applies them in `RecreateZones` after a restart or a
|
||
ZoneManager reload.
|
||
- **The form groups the flags** from one module file (Vehicles, Combat, Building, Loot, NPCs, Communication,
|
||
Survival). A flag the file does not know (a newer ZoneManager) goes under **Other**, so it is never hidden.
|
||
- **Presets are the admins' own (D195).** None ship with the module, because what an "arena" is differs by
|
||
server. An admin saves a set of flags and settings under a name, for one server, several servers, or the
|
||
whole fleet: `rust_zone_presets (id, name, flags, settings, shared)` plus `rust_zone_preset_servers`, the same
|
||
shape as a shared group (§1.4). The zone step offers the presets that cover its server, and **Save as preset**
|
||
turns the flags on a step into one. A preset fills the form once, when it is picked; the step keeps its own
|
||
copy, so editing a preset later does not change a published event.
|
||
- **Exemptions** are `zonemanager.ignoreflag.<flag>` permissions, managed on the permission screen (§1.7). The
|
||
zone form links there.
|
||
- **Flying** has no ZoneManager flag (PLAN_FIXES §4.4) and is not in this step.
|
||
|
||
### 3.2 The dome (D167, D194)
|
||
|
||
**D194: ZoneDomes stays the optional dependency D167 chose, because it already has the logic. What it lacks
|
||
is filled in by a helper (D168).** §0.5 found two gaps:
|
||
|
||
- **Its API throws without a player.** A second helper, **`RunicGatewayDomes.cs`**, fixes that for ZoneDomes
|
||
and nothing else (a helper does one job). It patches ZoneDomes' message method with Harmony so a call with
|
||
no player sends no message, and so the call returns normally instead of throwing after the dome is made.
|
||
It ships in the overlay beside the zone helper and is released, installed and checked the way D182's
|
||
helper is. Hello reports it, like the zone helper's `patched` / `missing` / `unsupported`.
|
||
- **Its boot drops the domes of temporary zones.** The bridge owns the fix, because it owns the order: after
|
||
`RecreateZones` puts a zone back (at boot or after a ZoneManager reload), it calls `AddNewDome` again for
|
||
each zone that had one, and it does the same when ZoneDomes itself reloads. At expiry and at teardown it
|
||
calls `RemoveExistingDome`, so a dome never outlives its zone in ZoneDomes' data file.
|
||
|
||
The zone step gains **"Show a dome"**, with ZoneDomes' own type and stack. It is offered only when the server
|
||
reports ZoneDomes loaded **and** the domes helper patched. Otherwise the step says which one is missing.
|
||
|
||
**The default type and stack** (PLAN_FIXES §9 question 3) are chosen by looking at each on the rig, in daylight
|
||
and at night, during the build walk.
|
||
|
||
### 3.3 The walk
|
||
|
||
On each rig, a zone made from an admin's preset: the flags hold for a player without the exemption and not
|
||
for one with it; the enter and leave messages arrive as chat and then as popups; a restart, a ZoneManager
|
||
reload and a ZoneDomes reload all keep the flags and the dome; expiry and teardown remove the dome from the
|
||
world and from ZoneDomes' data file; and with the domes helper removed, the step stops offering a dome.
|
||
|
||
---
|
||
|
||
## 4. The live map's marker types (§4.5; D165)
|
||
|
||
- **The site filters (D196).** The map meta already carries each monument's `kind` and `label`. The toggle is by
|
||
**label**, because that is what staff read and it groups the variants (31 substations, four prefabs, one
|
||
"Substation"). No wire change is needed.
|
||
- **The default** is a module list of the minor labels, **off** unless turned on: Substation, Underground
|
||
Cave, Train Tunnel, Water Well, Wild Swamp, Jungle Swamp, Ice Lake, Jungle Ruin, Fishing Village (not the
|
||
large one), Large Barn and Abandoned Supermarket, as the rig's map labels them, plus the gas station, mining outpost
|
||
and ranch under whatever labels the game gives them (read at build time; they are not on the rig's map). **Every label not on
|
||
the list is on**, so a monument Facepunch adds next month appears rather than disappears.
|
||
- **Where:** `Visibility.jsx`'s map section, under the *Monuments & world events* layer: a list of the labels
|
||
on that server's current map with a switch each, a fleet default, and a per-server override. That is the
|
||
same machinery the four layers already use (D114). The filter is applied where the map meta is served, so a
|
||
hidden marker is not sent at all.
|
||
- **Not chosen:** the game's own `MonumentType` (§0.7), sent by the plugin. It is authoritative, but it is a
|
||
wire change, and nothing yet shows that substations are a type of their own.
|
||
|
||
---
|
||
|
||
## 5. Chat titles: twenty-three conditions (§4.6; D172–D175)
|
||
|
||
### 5.1 What is counted, and where
|
||
|
||
Everything rides on `player.tally` (rule 2: aggregates, never a frame per event). The **gathering** conditions
|
||
need **no plugin change**: `rust_gather_totals` already keeps every resource by shortname from all three
|
||
sources (D159), so the site sums named lists.
|
||
|
||
| Condition (title) | Counted from | Tally field (new unless noted) | Site |
|
||
|---|---|---|---|
|
||
| Animal kills (Beastbane) | `OnEntityDeath`, victim `BaseNpc` | `animalKills` | column |
|
||
| NPC kills (Exterminator) | `OnEntityDeath`, victim a human NPC | `npcKills` (exists, and keeps meaning *all* NPC kills) | `npc_kills − animal_kills` |
|
||
| Players killed (Reaper) | `player.death` (exists) | — | `kills` |
|
||
| Bow / Melee / Blade / Revolver kills (Fletcher, Brawler, Duelist, Outlaw) | every credited kill's weapon shortname, same source as the killfeed | `weaponKills: { shortname: n }` | new table, classified by lists (§5.3) |
|
||
| Headshots (Crownbreaker) | the killing blow's `HitInfo.isHeadshot`, any victim | `headshots` | column |
|
||
| PvP / PvE kill distance (Longshot, Marksman) | the killfeed's distance | `bestPvpM`, `bestPveM` (this interval's maximum) | *best* columns, `GREATEST` at ingest |
|
||
| APC / helicopter kills (Tankbreaker, Rotorbane) | `OnEntityDeath`, victim `BradleyAPC`, `PatrolHelicopter`, `CH47Helicopter` | `apcKills`, `heliKills` | columns (§5.4) |
|
||
| Wood / ore / plants (Timberwright, Prospector, Greenhand) | exists (D159) | — | sums of named resource lists |
|
||
| Clothes / weapons crafted (Seamster, Armorer) | `OnItemCraftFinished`, `item.info.category` | `crafted: { category: n }` | columns for attire and weapon |
|
||
| Structures built (Mason) | `OnEntityBuilt`, a `BuildingBlock` | `built` | column (not `structures`, which is raids) |
|
||
| Structures repaired (Tinkerer) | `OnStructureRepair` | `repaired` | column |
|
||
| Players healed (Mender) | `OnHealingItemUse` on another player, `OnPlayerRevive` | `healed` | column |
|
||
| Rockets fired (Artillerist) | `OnRocketLaunched` | `rockets` | column |
|
||
| Explosives thrown (Demolitionist) | `OnExplosiveThrown`, `OnExplosiveDropped` | `explosives` | column |
|
||
| Quests completed (Wayfarer) | `OnMissionSucceeded` (§0.4) | `missions` | column |
|
||
|
||
**Three of these hooks can veto** (`OnStructureRepair`, `OnHealingItemUse`, `OnPlayerRevive`), so they are
|
||
declared `void` (PROTOCOL.md §8.7). All nine new hooks join `ExpectedHooks`, so a framework that stops raising
|
||
one is visible on the servers page.
|
||
|
||
### 5.2 NPC kills, without changing a public number
|
||
|
||
Splitting `npcKills` in two would change what the public leaderboard's *NPC kills* column means, and it would
|
||
change it mid-wipe. So `npcKills` keeps counting every NPC and animal. `animalKills` counts the animals again
|
||
separately, and the human-NPC count is the difference. Nothing already on a page moves.
|
||
|
||
### 5.3 The weapon lists live on the site
|
||
|
||
D174 wants the weapon lists "in one place, so a Rust update adds a weapon by editing a list". The plugin sends
|
||
every credited kill by **weapon shortname**, and the site keeps `rust_weapon_kills (server, wipe, steam,
|
||
weapon, n)`, shaped like `rust_gather_totals`. The bow, melee, blade and revolver lists are one module file,
|
||
**applied when the standings are read**. Editing a list corrects the whole wipe, and adding a Rust weapon needs
|
||
no plugin release. Melee is a list too, because the site cannot ask `BaseMelee`.
|
||
|
||
### 5.4 Who killed the helicopter
|
||
|
||
D174: the killing blow. A patrol helicopter usually dies by crashing, and its death `HitInfo` may carry no
|
||
player. The build walk shoots one down on the rig. If the killing blow has no player, the credit goes to **the
|
||
last player who damaged it within 60 s**, tracked only for those three vehicle types. That is a stated default,
|
||
and the walk decides whether it is needed.
|
||
|
||
### 5.5 Titles an admin can rename (D175)
|
||
|
||
- `rust_title_categories (stat PK, text, updated_by, updated_at)` holds the admin's title for a category.
|
||
The shipped defaults (§4.6's table) stay in the module.
|
||
- `rust_title_rules.text` may be **empty, meaning "use the category's title"**. That is what makes a rename
|
||
reach existing rules. D175 also says "new rules open with the category's current title filled in". If it
|
||
were filled in as the rule's own text, the rename would never reach that rule. So the form shows the current
|
||
category title **as a placeholder** and saves nothing unless the admin types.
|
||
- `stat` takes the 23 category ids (they fit `VARCHAR(16)`). The standings query for a *best* stat is a `MAX`.
|
||
- The titles screen gains a "Category titles" section: 23 rows, each with the default, the admin's text, and
|
||
Reset.
|
||
- **The new stats feed titles only.** The public leaderboard keeps its columns in this step. Adding any of them
|
||
there is a later, separate choice.
|
||
|
||
### 5.6 Missions (D173)
|
||
|
||
`OnMissionSucceeded` increments `missions` for the assignee. A failed or abandoned mission is never counted.
|
||
The hook's exact arguments are confirmed the first time a person finishes a mission on the rig, which is also
|
||
D173's pass condition: a mission taken from an NPC and finished moves Wayfarer by exactly one, and an abandoned
|
||
one does not move it. Should the hook disappear on a framework, D173's option 2 is known to work: the player's
|
||
`acceptedMissions` carries each instance's `status` (`Completed`, `Failed`, …, §0.4).
|
||
|
||
### 5.7 The walk
|
||
|
||
Every condition earned once on each rig (PLAN_FIXES §8): kill an animal and a scientist with a bow, a machete
|
||
and a revolver, one of them by headshot; shoot a player from range (this needs the second player); craft a
|
||
shirt and a weapon; build and repair a wall; bandage and revive another player; fire a rocket; throw a grenade;
|
||
destroy an APC; shoot down a helicopter; finish a mission and abandon one. Each count is checked in the tally
|
||
frame and on the titles screen.
|
||
|
||
---
|
||
|
||
## 6. NPCs (§4.7)
|
||
|
||
**D197: the spike tries both routes on both rigs, then the org lead picks one.** The goal is the one the org
|
||
lead named: NPCs with different kits and names. NPC Loadouts is out, because it cannot dress one spawn
|
||
differently from the next (§0.10). The spike reports on each route before any site work.
|
||
|
||
**Route A: extend `rust.npc.place`.** The bridge already spawns Rust's own scientists, and they are players
|
||
underneath:
|
||
|
||
- **`kit`**: a Kits kit. After spawning, the bridge empties the NPC's inventory and gives it the kit through
|
||
Kits' API (`GiveKit`). Kits is already a required plugin (PLAN.md R6).
|
||
- **`name`**: the NPC's `displayName`, which already flows into the killfeed (`attackerName`) and on to F2's
|
||
labels.
|
||
- The spike answers: does a scientist given a kit fight with the kit's weapon, or fall back to its prefab
|
||
loadout? Does the name reach the killfeed? Does Rust re-dress it on a later tick?
|
||
|
||
**Route B: HumanNPC.** Its API (`SpawnHumanNPC`, `SetHumanNPCInfo`, `RemoveHumanNPC`) gives a name and a kit
|
||
directly. The gotchas the spike checks, each of which could rule it out:
|
||
|
||
- **Two more dependencies.** It requires Pathfinding and Waypoints. All three become optional dependencies
|
||
that operators have to install, and the egg and the installer do not ship them.
|
||
- **Carbon.** Nothing says it runs on Carbon. The spike loads all three on `rust-carbon`.
|
||
- **Its own persistence.** HumanNPC saves its NPCs to its data file and respawns them on boot, which is the
|
||
ZoneDomes problem again (§0.5). An event's NPC must be removed at teardown and must not come back after a
|
||
restart that happened mid-event. The bridge would track them in its registry by HumanNPC's id, not by an
|
||
entity net id as `world.place` does today.
|
||
- **Its own AI.** Its NPCs do not use Rust's scientist brain. Hostility, the weapon they use and whether they
|
||
chase are its settings. The spike checks that a hostile one actually fights.
|
||
- **Being counted as a player.** A HumanNPC is a `BasePlayer` with a generated `userID`. The bridge's death
|
||
and kill paths (`OnPlayerDeath`, the tally, the killfeed, a player-kill title) must count it as an NPC, not
|
||
as a player. The spike kills one and reads the frames.
|
||
- **Upkeep.** Its last release date and open issues are recorded, because an abandoned dependency is a cost.
|
||
|
||
If route A works on both frameworks it needs nothing new installed, and that is weighed against what B offers
|
||
beyond it.
|
||
|
||
---
|
||
|
||
## 7. Protocol 13, what this adds
|
||
|
||
All of these are additions to the unreleased protocol 13. PROTOCOL.md §19 gains a section for each.
|
||
|
||
| Change | Direction | § |
|
||
|---|---|---|
|
||
| `perm.inventory` / its paged reply (owners, groups with parents, users, leased) | site → game | 1.2 |
|
||
| `perm.sync`: groups carry `parent`; `managed` and the foreign report removed | site → game | 1.2 |
|
||
| `kits.uses` / its reply | site → game | 2.6 |
|
||
| hello: `zoneManager: { version, flags[] }` (from the zone helper); `zoneDomes: { loaded, version }` and the domes helper's state | game → site | 3.1, 3.2 |
|
||
| `world.zone`: `flags`, `settings`, `enterMessage`, `leaveMessage`, `delivery`, `dome: { type, stack }`; reason `bad-option` | site → game | 2.4, 3.1, 3.2 |
|
||
| the reward verbs' player message: `delivery` | site → game | 2.4 |
|
||
| `world.place` for an NPC (the verb behind `rust.npc.place`): `kit`, `name`, if route A is chosen | site → game | 6 |
|
||
| `player.tally`: `animalKills`, `weaponKills`, `headshots`, `bestPvpM`, `bestPveM`, `apcKills`, `heliKills`, `crafted`, `built`, `repaired`, `healed`, `rockets`, `explosives`, `missions` | game → site | 5.1 |
|
||
|
||
## 8. MODULE_API 1.12.0 (core, website `main`)
|
||
|
||
Additions only, so Module-uo, declared `^1.10.0`, is unaffected, and that is proven the way 1.11.0 was.
|
||
|
||
- `registerEventTemplates` (§2.5).
|
||
- A param's optional `duration: 'minutes'` (§2.2).
|
||
- A lease's optional value `example` (§2.1).
|
||
- An option row's optional `note` (§2.6).
|
||
|
||
## 9. Order of building
|
||
|
||
Each item is built on `edge`, walked on both rigs, and PR'd with its spec, like steps 1 and 2:
|
||
|
||
1. **The permission manager** (§1). It is the largest, and §3's exemptions point at it.
|
||
2. **Chat titles** (§5), including the mission hook.
|
||
3. **Zones and the dome** (§3), with the zone half of D193's popups and the domes helper.
|
||
4. **The map's marker types** (§4).
|
||
5. **NPCs** (§6): the two-route spike, then the org lead's pick, then the build.
|
||
6. **The step editor and the kit weekend** (§2). It is core's, so it can be built alongside any of the above.
|
||
It goes out as its own website PR with the Module-uo proof.
|
||
7. **The first-class optional plugins** (§11): Economics, then Backpacks, then RaidableBases, each starting
|
||
with its source pull and rig spike.
|
||
|
||
Then PLAN_FIXES §8's re-walk on Oxide and Carbon, and the cutover.
|
||
|
||
## 10. Decisions of record
|
||
|
||
The plan asked eleven questions. The org lead answered them on 2026-09-27, the day it was written.
|
||
|
||
| # | Decision | Rejected | § |
|
||
|---|---|---|---|
|
||
| **D188** | **The import records a holding by Steam account, exactly as the game has it, even when that account is linked.** A toggle made on the site for a linked player is a grant to their website account and reaches every Steam account they link (D28). | Recording imported holdings against the website account, which would widen them to the person's other Steam accounts. | 1.3 |
|
||
| **D189** | **A group belongs to one server unless an admin shares it** with chosen servers or the whole fleet. The import makes one group per server, even for identical names. The same group across servers is allowed but never assumed. | One group name fleet-wide with per-server contents (the plan's first proposal). | 1.4 |
|
||
| **D190** | **An in-game change affects that server only, and auto-adopt applies it without asking.** A grant that reaches more servers gains an exception for that server; a shared group is split, and that server gets its own copy. | Holding a change to a multi-server row for a person under every policy. | 1.6 |
|
||
| **D191** | **ZoneManager's `ignoreflag` permissions are listed like any other permission.** | One collapsed "exempt from zone flags" section. | 1.7 |
|
||
| **D192** | **Core gains event templates** (`registerEventTemplates`, MODULE_API 1.12.0), and the kit weekend is the first. | A Rust-only kit-weekend button. | 2.5 |
|
||
| **D193** | **A zone's enter and leave messages and a player's reward message get the Chat / Popup choice**, as announcements have. The bridge sends zone messages itself for its own zones. | Popups for announcements only in this step. | 2.4 |
|
||
| **D194** | **ZoneDomes stays the optional dependency (D167 stands).** A domes helper makes its API safe without a player; the bridge re-adds domes after re-creating zones and removes them at expiry and teardown. | The bridge drawing the dome itself (the plan's first proposal). | 3.2 |
|
||
| **D195** | **Zone presets are the admins' own**, saved for one server, several, or the fleet. None ship. | Shipped presets (Arena, Safe zone, No building). | 3.1 |
|
||
| **D196** | **The site filters map markers by label**, with a default-off list of minor labels; every other label shows. | The plugin sending Rust's `MonumentType`. | 4 |
|
||
| **D197** | **The NPC spike tries both routes**, extending `rust.npc.place` and HumanNPC, and reports HumanNPC's gotchas before the org lead picks. | Deciding the route before the spike. | 6 |
|
||
| **D198** | **The first inventory on an existing install adopts everything present**, including hand edits waiting for an answer today (D160 as written). | Leaving existing drift rows on the "needs a person" list. | 1.5 |
|
||
|
||
The org lead added §11 on 2026-09-28 and answered its questions the same day:
|
||
|
||
| # | Decision | Rejected | § |
|
||
|---|---|---|---|
|
||
| **D199** | **RaidableBases, Economics and Backpacks become first-class optional plugins, and Kits stays required.** Optional means what it means for PopupNotifications and Clans: the bridge detects each at hello, the steps that need one refuse with a reason when it is absent, and the installer does not require it. | Making Kits optional as well. | 11 |
|
||
| **D200** | **Economics: all four uses.** Coins as an event reward (withdrawn on revert); balances shown to the player (site and app) and to staff; staff deposit, withdraw and set a balance, with an activity row; and a *richest* leaderboard column and chat-title condition. | — | 11.2 |
|
||
| **D201** | **Backpacks: staff view any player's backpack, a player views their own (site and app), and an event can grant extra capacity for a time**, taken back at the end. | Showing backpack sizes as named tiers on the permission screen. | 11.3 |
|
||
| **D202** | **RaidableBases: all four uses, and the paid edition too.** Active bases on the live map and in the feed; an event step that spawns a base at a difficulty and despawns it at the end; bases raided per player on the leaderboard and as a title condition; and the paid edition's extras (five difficulties, lockouts) where it is installed, falling back to what both editions share. | Supporting only what the free edition has. | 11.4 |
|
||
| **D203** | **RaidableBases picks where an event's base goes**; the site learns the spot from `OnRaidableBaseStarted` and shows it. The paid edition's placement, if it has one, is offered where installed. | A helper that places a base at a chosen monument. | 11.4.3 |
|
||
| **D204** | **At an event's end, a helper (`RunicGatewayRaids.cs`, D168) despawns only that event's base.** Without the helper the base despawns on its own timer, and the run records it as expiring on its own. | Leaving every base to its timer; RaidableBases' "despawn all". | 11.4.3 |
|
||
| **D205** | **An event picks a RaidableBases profile and a difficulty, may override a bounded set of settings for that spawn (through the helper), and the site edits RaidableBases' profiles** — a named exception to the editor never walking a data directory. | Profile and difficulty only; overrides without profile editing. | 11.4.3 |
|
||
| **D206** | **Lock-to-first-attacker is set per event.** RaidableBases keeps it only in its main config, so the raids helper patches the base's `BypassUseOwners()` to answer from the run's own setting for the event's base, and leaves every other base to the config. | One server-wide switch the config page edits; no lock control at all. | 11.4.5 |
|
||
| **D207** | **A step places its base either where RaidableBases picks (the default, D203) or at an admin-given spot:** x and z, plus either a height adjustment or an exact height. The spot needs the raids helper; without it the step offers only the plugin's pick. | Only an admin-given spot; keeping D203 alone. | 11.4.5 |
|
||
| **D208** | **A blocked spot is refused, with the reason.** The step does not spawn when the plugin's area check fails or the helper's water and terrain test fails, and the run log says why ("player building", "raid base", "in water"). The editor runs the same check when the step is saved. | Falling back to the plugin's pick; spawning anyway. | 11.4.5 |
|
||
|
||
## 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
|
||
three are new, and none of them is mentioned anywhere in PLAN.md or PLAN_FIXES.md. They are **optional** (D199):
|
||
the pattern PopupNotifications set (D141) and Clans set (D47) before them.
|
||
|
||
- **Detected at hello.** `integrations` gains `economics`, `backpacks` and `raidableBases`, each as
|
||
`{ loaded, version }`, and RaidableBases also reports which **edition** it is.
|
||
- **A step or screen that needs one refuses with a reason** when it is absent ("Economics is not installed on
|
||
this server"). It never disappears and never fails silently, as R3 settled for Kits.
|
||
- **Not required by the installer or the egg.** `overlay.toml`'s `requires_plugins` keeps Kits and ZoneManager
|
||
only.
|
||
- **Public API only (R2).** Where a plugin lacks what we need, a D168 helper does the rest.
|
||
|
||
### 11.1 What was found, and what must be pulled before any build
|
||
|
||
From the research on 2026-09-28. Every exact signature below is confirmed again from the plugin's current
|
||
`umod.org/plugins/<Name>.cs` at the start of its build, the way Kits' was (PLAN.md, "pulled 2026-09-24").
|
||
Personal GitHub mirrors are not a source: k1lly0u's mirror of Kits is two major versions behind.
|
||
|
||
| 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 · 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)
|
||
|
||
- **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**. 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.
|
||
- **Staff adjust.** Deposit, withdraw and set from the player's admin page, through the plugin's API. Each is
|
||
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.
|
||
|
||
### 11.3 Backpacks (D201)
|
||
|
||
- **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
|
||
`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`.
|
||
- **The spike first:** the Carbon restart report. Fill a backpack on `rust-carbon`, restart, and check it.
|
||
|
||
### 11.4 RaidableBases (D202)
|
||
|
||
- **Live map and feed.** `OnRaidableBaseStarted` / `Ended` / `Despawn` / `PrivilegeDestroyed` become events
|
||
(`raid.started`, `raid.ended`, …) carrying the base's position, difficulty and owner. The map draws active
|
||
bases as a layer with its own audience switch (D114), and the feed says who raided what.
|
||
- **An event step, `rust.raidbase.spawn`**, spawns a base at a difficulty near a monument or position, owned
|
||
by the run. The run's teardown despawns it, and D170's expiry pattern marks it `expired` if the plugin ends
|
||
it first.
|
||
- **Stats and titles.** Bases completed per player per wipe (the player credited with the tool cupboard, as
|
||
the plugin credits it), a leaderboard column, and a title condition, *Warlord* by default.
|
||
- **The paid edition (D202).** Hello reports the edition. Where it is the paid one, the spawn step offers its
|
||
five difficulties and the feed carries lockouts. On the free edition the step offers the one difficulty it
|
||
has. **Testing the paid edition needs a licence for the rigs**, which is the org lead's to buy or supply.
|
||
- **The spike first:** pull the free source, confirm every hook and API signature above, and find the listing
|
||
and despawn calls. Then load it on both rigs, with TruePVE, and raid one base.
|
||
|
||
#### 11.4.1 The free edition, as the spike found it (2026-09-28)
|
||
|
||
The source is RaidableBases 3.2.0 (free, `umod.org/plugins/RaidableBases.cs`), and it was loaded on both rigs
|
||
with CopyPaste 4.3.0. A throwaway spike plugin made one base file from the walk hut and logged every
|
||
lifecycle hook. It was deleted afterwards, and so were the three plugins on both rigs.
|
||
|
||
**What it needs.** **CopyPaste is a hard dependency.** A base is a CopyPaste building file in
|
||
`data/copypaste/`, and **none ship with the free edition**: the default profile `RaidBases` names
|
||
`RaidBase1`–`RaidBase5`, and none of them exist. A base with no storage box is thrown away as soon as it is
|
||
pasted ("No usable boxes found"). A PvE plugin (TruePVE, SimplePVE, NextGenPVE, Imperium or AegisPVE) is
|
||
optional.
|
||
|
||
**What it costs at boot.** `OnServerInitialized` took 6.7 s (Oxide) and 7.9 s (Carbon) on the main thread.
|
||
The spawn grid then builds for about a minute: 52 s for 1,172 points on the rigs' 3000 map. **During that
|
||
minute a spawn is refused** ("grid is loading").
|
||
|
||
**Its public API** (`[HookMethod]`):
|
||
|
||
| Call | Returns | Use to us |
|
||
|---|---|---|
|
||
| `IsPremium()` | `false` on the free edition | hello's edition field |
|
||
| `GetAllDifficulties()` | `[("Normal", 512)]` on the free edition | the step's difficulty list, read from the installed plugin rather than hard-coded |
|
||
| `GetAllEventsCount()`, `GetActiveEventCount()` | ints | the servers page |
|
||
| `GetAllEvents()` / `GetAllEventsNonAlloc(list)` | an 18-field tuple per base: position, mode, level, PvP, owner, raiders, intruders, entities, base name, spawn and despawn time, radius, loot left | the live map layer. **The plugin warns that `GetAllEvents` changes shape in the next update.** |
|
||
| `EventTerritory(pos)`, `…Any`, `…All`, `GetPlayersFrom`, `GetOwnerFrom`, `HasPVPDelay` | | not needed |
|
||
|
||
**Nothing in the API spawns, despawns, or lists profiles.**
|
||
|
||
**The hooks it raises.** `OnRaidableBaseStarted`, `Ended`, `Completed`, `Despawn`, `Despawned` and
|
||
`PrivilegeDestroyed` all carry the same 17 arguments:
|
||
|
||
```
|
||
(Vector3 location, int level, bool allowPVP, string id, float, float, float loadTime, ulong ownerId,
|
||
BasePlayer owner, List<BasePlayer> raiders, List<BasePlayer> intruders, List<BaseEntity> entities,
|
||
string baseName, DateTime spawn, DateTime despawn, float protectionRadius, int lootRemaining)
|
||
```
|
||
|
||
A plugin method with exactly that signature binds on both frameworks. `Started` also has a one-argument
|
||
overload carrying the plugin's own class, which the bridge cannot name and does not need. Other hooks:
|
||
`OnPlayerEnteredRaidableBase` / `Exited` (14 arguments, the player first), `OnRaidableBaseUnlocked`,
|
||
`OnRaidableAwardGiven` / `Owner`, the PvP-delay hooks and loot hooks.
|
||
|
||
**How another plugin spawns a base: only through the server console.**
|
||
|
||
- `rbevent [baseName]` queues a base, and **RaidableBases chooses where**. On the rigs, three spawns landed at
|
||
Q6, C13 and C4, and none of them was named by the command. There is no position, no monument and no
|
||
difficulty argument, and **nothing comes back** to the caller.
|
||
- `rbe` pastes where a player is looking, so it needs a player in the game.
|
||
- **Every base's `id` is `"0"`** on the free edition. The only way to know which base the event made is the
|
||
next `Started` for that base name after the command. That is safe while events make one base at a time.
|
||
|
||
**How another plugin despawns a base: all or nothing.** `rbevent despawnall` removes every base on the
|
||
server, including ones the event did not make, and `rbevent despawn_inactive` removes every inactive one.
|
||
Despawning **one** base needs a player standing at it. Left alone, a base despawns on its profile's own
|
||
timer: 45 minutes in the default profile, as `despawn` in `Started` said.
|
||
|
||
**Its options.** Each profile has well over a hundred settings. Among them: NPCs, turrets, loot rules,
|
||
despawn timers, the plugins to block, where bases may spawn (beaches, roads, monuments), what players may
|
||
do, and backpacks. The global config has its own schedulers (Maintained, Scheduled and Manual events), map
|
||
markers, ZoneManager zones, a ranked ladder and messages. **Profiles live in the plugin's data directory**,
|
||
which the site's configuration editor deliberately never walks. The global config is in the config
|
||
directory, where the editor already reaches it.
|
||
|
||
**What that means for D202's event step.** "Spawn a base at a difficulty near a monument, and despawn it at
|
||
the end" is not possible on the free edition through anything RaidableBases offers another plugin. What
|
||
it does offer is: spawn a named base, somewhere, now; learn where and when it will despawn from the hook;
|
||
and remove every base at once. §11.4.3 asks which way to go.
|
||
|
||
#### 11.4.2 The paid edition — to be filled when the licensed copy arrives
|
||
|
||
The org lead supplies a licensed copy later in the week of 2026-09-28. The same spike runs against it on
|
||
both rigs, and this section records:
|
||
|
||
- `IsPremium()` and `GetAllDifficulties()` (expected: five tiers);
|
||
- whether it adds a spawn at a position, a monument or a difficulty, a despawn of one base, or real base
|
||
ids — by API or console command;
|
||
- whether the 17-argument hooks and `GetAllEvents` have the same shape (the free edition warns that the
|
||
latter is changing), and what `level` carries per difficulty;
|
||
- lockouts, buyable events and the ranked ladder: the hooks they raise and what the feed could say;
|
||
- anything in it that needs a D168 helper, and anything the free edition's answer in §11.4.3 would get
|
||
wrong for it.
|
||
|
||
The design in §11.4.3 is written so the paid edition extends it rather than replaces it. The step reads
|
||
what the installed edition offers — difficulties, and any placement or despawn it supports — and offers
|
||
exactly that.
|
||
|
||
#### 11.4.3 Decided (2026-09-28)
|
||
|
||
The org lead answered the spike's three questions the same day:
|
||
|
||
- **D203: where a base goes — RaidableBases picks.** The step spawns the chosen base with `rbevent`, and
|
||
RaidableBases places it wherever its grid allows. The bridge learns the spot from the next
|
||
`OnRaidableBaseStarted` for that base name. The site then shows it on the live map and in the run log, and
|
||
records it on the run's ledger. If the paid edition offers placement, the step offers it there too
|
||
(§11.4.2).
|
||
- **D204: at the end — a helper removes only the event's base.** Teardown, a revert, and an operator's stop
|
||
each despawn exactly the base the run made, never another. The free edition cannot do that from outside
|
||
(§11.4.1), so a D168 helper, **`RunicGatewayRaids.cs`**, finds the run's base by the position and spawn time
|
||
`Started` reported, and calls RaidableBases' own despawn on that one base. Like the zone helper
|
||
(D181, D182), it is optional and default-installed, and it reports at hello. **Without it, the step still
|
||
spawns, and the base despawns on its own timer.** The site says so on the step, and the run records the
|
||
base as expiring on its own, like D170's zones. A base the plugin despawns first is marked `expired`.
|
||
- **D205: what an event can choose — a profile, a difficulty, and per-event overrides; and the site edits
|
||
profiles.**
|
||
- **Profile and difficulty.** The step picks a RaidableBases profile, and a difficulty from the installed
|
||
plugin's `GetAllDifficulties()`. The free edition offers "Normal"; the paid edition offers its five.
|
||
- **Per-event overrides.** The step may override a bounded set of a profile's settings for this spawn
|
||
only. The first set is PvP on/off, despawn minutes, NPCs on/off and count, and the lock-to-first-attacker
|
||
rule; the build's spike settles which of them the plugin lets a helper change after `Started`. The free
|
||
API has no way to do this, so the helper applies them. **Without the helper there are no overrides**, and
|
||
the step says so.
|
||
- **The site edits profiles.** The configuration page gains RaidableBases' profile files,
|
||
`data/RaidableBases/Profiles/*.json`. This is a **named exception** to the rule that the editor never
|
||
walks a plugin's data directory, which exists because that directory holds live state and the
|
||
permission stores. The exception is that one folder, for that one plugin. It gets the editor's usual
|
||
guarantees: the write is recorded, a reload is watched, and a failed load rolls back (D178, D179). The
|
||
reload is `rb.reloadprofiles`, which the spike ran on both rigs, not a plugin reload.
|
||
|
||
**Still to settle in the build's own spike:** the same helper questions on Carbon and on the paid edition.
|
||
Also the one-at-a-time rule: while a first base of the same name is still waiting to start on a server, the
|
||
step refuses to spawn a second one, because the free edition's bases have no ids to tell them apart.
|
||
§11.4.4 may retire that rule.
|
||
|
||
#### 11.4.4 The helper spike (2026-09-28)
|
||
|
||
The org lead asked for two tests of a D168 helper before the build: can it despawn one base, and which
|
||
settings can it override? A third question came with them: can an admin give the spot, meaning x, z and a
|
||
height adjustment? The spike ran on the rust-oxide rig against the free edition, 3.2.0. Carbon and the paid
|
||
edition are still to run (§11.4.2). Every finding below reaches RaidableBases' internals by reflection and a
|
||
Harmony patch. None of it is public API, so the helper has to check the plugin's version at hello, as the
|
||
zone helper does.
|
||
|
||
- **Despawning one base works.** The plugin keeps its live bases in a list on the plugin, and each base has
|
||
its own `Despawn()`. Calling it on one base removed that base and no other. It fired the usual
|
||
`OnRaidableBaseDespawn` and `OnRaidableBaseDespawned` hooks, so the bridge's feed sees nothing new.
|
||
- **The helper can know exactly which base is the event's.** A Harmony postfix on the plugin's `OpenEvent`
|
||
receives the new base object itself. The helper can tag it for the run at birth, instead of matching the
|
||
position and spawn time `Started` reports afterwards (D204). The one-at-a-time rule above may then go,
|
||
if the build confirms it on Carbon and the paid edition.
|
||
- **Overrides go in at `OpenEvent`, not after `Started`.** At `OpenEvent` the base holds a reference to its
|
||
profile's settings object, which every base of that profile shares. The helper swaps in a deep copy (a
|
||
JSON round trip), changed for this spawn. The shared profile was checked afterwards and was untouched.
|
||
Proven on the rig:
|
||
- PvP on/off;
|
||
- despawn minutes (inactive and total, via the profile's own "override config" switch);
|
||
- NPCs on/off, and the scientist and murderer counts.
|
||
- **Lock-to-first-attacker is not a profile setting.** It lives in RaidableBases' main config, in two
|
||
pairs of switches (PvP and PvE): one pair for "Manual" bases, which is what `rbevent` and the helper
|
||
spawn, and one pair for scheduled bases. A per-event lock would need a second Harmony patch, on the
|
||
base's `BypassUseOwners()`. Not built; a question for the org lead.
|
||
- **An admin-given spot works.** The helper builds the same spawn request the plugin's own "spawn where I
|
||
look" command builds, from numbers instead of a player's view. It then calls the plugin's paste. Bases
|
||
landed at the given x and z every time.
|
||
- **Height has to go through a copy of the profile.** The plugin recomputes y at paste time from the
|
||
ground (or water), plus the building's own height, plus the profile's paste height adjustment. It
|
||
ignores any y it is handed, unless the profile forces a height. So the helper copies the profile:
|
||
- an adjustment is added to the copy's paste height adjustment;
|
||
- an absolute y turns on the copy's forced height.
|
||
|
||
Results, measured as the height of the base's building blocks:
|
||
|
||
| Request | Blocks at | Ground there |
|
||
| --- | --- | --- |
|
||
| no adjustment | 1–4 | −0.2 |
|
||
| +5 | 6–9 | −1.1 |
|
||
| absolute y = 30 | 31–34 | 0.5 |
|
||
| a plain `rbevent` spawned right after | 1–4 | −0.2 |
|
||
|
||
**Trap:** the plugin's own `BuildingOptions.Clone()` is a shallow `MemberwiseClone`. The first run changed
|
||
the height through that clone, and the change leaked into the shared profile: the next plain `rbevent`
|
||
base spawned 35 m up. The height settings sit in a nested object, so the helper must copy that too, or
|
||
use the same deep copy as the overrides. Nothing reached the file on disk, and `rb.reloadprofiles` reset
|
||
the leak.
|
||
- **The plugin's area check catches buildings, but not water.** Its safety check, run at an admin's spot,
|
||
refused a player's building and refused an existing raid base. At 25 m away from the building it passed. A
|
||
spot on the seabed 50 m under water also passed: the plugin tests water in its own location picker, not
|
||
in this check. So an admin-given spot needs the helper's own water and terrain test as well as the
|
||
plugin's check. The spike reported the check but did not enforce it; whether to enforce is a question
|
||
for the org lead.
|
||
- **A RaidableBases reload drops the helper's Harmony patches**, because the patched methods belong to the
|
||
old assembly. The helper must re-patch in `OnPluginLoaded`, as the zone helper does for ZoneManager.
|
||
- **After a server boot the plugin refuses every spawn for about 50 s**, until its log says "Grid
|
||
initialization completed". This includes the helper's spawns. The step has to wait for that, not fail
|
||
on it.
|
||
|
||
#### 11.4.5 Decided after the helper spike (2026-09-28)
|
||
|
||
The org lead answered §11.4.4's three questions the same day:
|
||
|
||
- **D206: lock-to-first-attacker is per event.** The helper adds a second Harmony patch, on each base's
|
||
`BypassUseOwners()`. For a base the helper tagged for a run, it answers from that run's setting. Every
|
||
other base, including one an admin spawns by hand, still follows RaidableBases' config. Without the
|
||
helper the lock follows the config, and the step says so.
|
||
- **D207: a step places its base one of two ways.** The default is D203, where RaidableBases picks. The
|
||
other is an admin-given spot: x and z, plus either a height adjustment (added to the profile's own) or
|
||
an exact height (the profile's forced height). Both go through the helper's deep copy of the profile
|
||
(§11.4.4). The spot needs the helper; without it the step offers only the plugin's pick.
|
||
- **D208: a blocked spot is refused, and the run log says why.** At spawn time the helper runs
|
||
RaidableBases' own area check and its own water and terrain test. If either fails, the step does not
|
||
spawn, and the run records the reason ("player building", "raid base", "in water"). The editor
|
||
runs the same check through the bridge when the step is saved. That is advice only, since the world
|
||
can change before the event runs.
|
||
|
||
### 11.5 Protocol
|
||
|
||
These are new verbs, events and hello fields. If they are built before the cutover they join the unreleased
|
||
protocol 13 like everything else here; if the cutover comes first, they open protocol 14. Which one is decided
|
||
when §9 reaches item 7.
|