docs(rust): protocol 13 step 2 — expiry, plugin loads, the zone helper, the tally (§19.4-19.8, MODULE_API 1.11.0)
The spec for PLAN_FIXES §6 step 2, as built (D181-D185): - PROTOCOL.md §19.4 world.expired's `what` and the website recording an expiry (amends §15's "maps it to nothing"); §19.5 plugin.loaded / plugin.unloaded with the permission diff; §19.6 the ZoneManager helper and `zoneHelper` at hello; §19.7 what the tally counts (F1, F3, F4); §19.8 the website (F7 hold, F5/F6 link fleet, F2 names). - MODULE_API.md 1.11.0 and ctx.events.expired; EVENTS.md §L the `expired` status, terminal and green, and `resource.expired`. - rust-link/INSTALL.md: the helper in the tarball, by hand, and in doctor. - PLAYER_WALK.md: events step 6 can pass now; a step-2 section whose rows 1-2 were walked on both rigs without a player and 3-9 need one. - PLAN_FIXES §6: step 2 as built, with its PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -1511,8 +1511,11 @@ erases a zone whose `holdMs` has run out, whether or not the website is heard fr
|
||||
{"kind":"world.expired","type":"event","id":"rg-13-35875416-1","runId":"13"}
|
||||
```
|
||||
|
||||
The website maps it to nothing. Core learns about it through `reconcile` and `revert`, as with
|
||||
`lease.expired`.
|
||||
Until protocol 13 the website mapped it to nothing (D96), and core learned about it only through
|
||||
`reconcile` and `revert`, as with `lease.expired`. **Protocol 13 changes both halves** (§19.4): the
|
||||
frame carries the entity's kind as `what` — protocols 9 to 12 wrote it over the frame's own `kind`, so no
|
||||
`world.expired` ever arrived as one — and the website records the run's resource as `expired`
|
||||
(PLAN_FIXES D170, D183).
|
||||
|
||||
### 15.4 `EventsEnabled`, and the two bounds
|
||||
|
||||
@@ -1950,8 +1953,9 @@ untouched.
|
||||
|
||||
Planned in [`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md): one bump for every wire change the org
|
||||
lead's first walk with a player in the game found (§5 there). It is built on `edge` in Rust-Plugins,
|
||||
Rust-Link and Module-Rust and released together (D177). **This section grows as the pieces land**;
|
||||
the first is the configuration save.
|
||||
Rust-Link and Module-Rust and released together (D177). **This section grows as the pieces land**:
|
||||
the configuration save first (§19.1–19.3, step 1), then step 2's fixes (§19.4–19.8, decisions
|
||||
D181–D185).
|
||||
|
||||
One fix rides in the same release without a wire change: **the plugin decodes what the sidecar sends
|
||||
as UTF-8** (F15). Protocols 1 to 12 decoded each byte as a character — Latin-1 — so every non-ASCII
|
||||
@@ -2073,3 +2077,108 @@ from `config.outcome` by `(server_id, write_id)`, and only while it is still `re
|
||||
frame moves nothing and a late one still lands. A row past `settle_by` reads as **`lost`**. The page polls
|
||||
`GET /api/v1/admin/rust/config/{serverId}/writes/{writeId}` every two seconds until the write settles
|
||||
(D179).
|
||||
|
||||
### 19.4 `world.expired` is recognisable, and an expiry is recorded (F13, F14)
|
||||
|
||||
The frame gains **`what`**, the entity's kind — the name the website's resource rows already use:
|
||||
|
||||
```json
|
||||
{"kind":"world.expired","type":"event","id":"rg-13-35875416-1","what":"zone","runId":"13"}
|
||||
```
|
||||
|
||||
Protocols 9 to 12 wrote the entity's kind into `kind`, over the frame's own, so every expiry reached the
|
||||
sidecar as `kind: "zone"` and nothing listening for `world.expired` ever heard one (F13). A website that
|
||||
reads protocol 12 frames will find them filed under `zone`; nothing is re-filed.
|
||||
|
||||
**The website records it** (D170, D183). Module-Rust hands core the resource the zone step ledgered —
|
||||
kind `world`, ref `<serverId>:<id>` (§15, the ref names the server) — through
|
||||
`ctx.events.expired({ kind, ref })` (MODULE_API 1.11.0). Core marks that row **`expired`**: terminal,
|
||||
never taken back at teardown, and distinct from `orphaned`, which `reconcile` uses for a thing that
|
||||
vanished with nobody asking. An expiry for a zone no run ledgered is not an error.
|
||||
|
||||
`world.expired` and `lease.expired` are **staff** kinds (§8.5). Neither was classified before protocol
|
||||
13 — default deny kept both off public pages — and both are an event's machinery; the public hears what
|
||||
an event did from core's announcements.
|
||||
|
||||
### 19.5 `plugin.loaded` and `plugin.unloaded` (F8)
|
||||
|
||||
A grant the site made for a plugin that was not loaded stays unresolved until the plugin is back. The
|
||||
first walk watched one land thirteen minutes after the plugin loaded, on the fifteen-minute audit. So
|
||||
the bridge now announces a load or an unload of **another** plugin, with the permissions it registered
|
||||
or dropped (D184):
|
||||
|
||||
```json
|
||||
{"kind":"plugin.loaded","type":"event","name":"Kits","version":"4.4.9","permissions":["kits.admin","kits.rgreward"]}
|
||||
```
|
||||
|
||||
- **`permissions`** is a diff of the framework's registered-permission list, taken **one tick after the
|
||||
hook**, because the order of "drop an unloaded plugin's permissions" and "call `OnPluginUnloaded`"
|
||||
is the framework's own. It is sorted, and it may be empty.
|
||||
- **Only once the world is ready** (§15.5). A boot loads every plugin, and a boot is the restart sync's
|
||||
job; the bridge takes its baseline list at `OnServerInitialized`.
|
||||
- **A reload in one tick shows an empty list on both frames.** Carbon's `c.reload` unloads and loads in
|
||||
the same tick, so by the next the list is what it was. That is the right answer: what a grant
|
||||
resolves against did not change. Two plugins loaded in one tick share one diff, under the first name.
|
||||
- Both are **staff** kinds. The website marks the server's permission sync dirty when `permissions` is
|
||||
not empty, and the next tick (30 s) pushes. §4.1's permission manager will take plugin ownership from
|
||||
its own inventory read, not from this.
|
||||
|
||||
Walked on both rigs, 2026-09-27: `oxide.unload Kits` / `oxide.load Kits` and Carbon's `c.unload` and
|
||||
the queued load each carried `["kits.admin","kits.rgreward"]`; the ZoneManager helper's load carried `[]`.
|
||||
|
||||
### 19.6 The ZoneManager helper, and `zoneHelper` at hello (F12)
|
||||
|
||||
ZoneManager counts a player as inside a zone only when the zone's trigger fires on **entry**, so a zone
|
||||
created, updated or re-created around somebody standing there holds nobody — its flags, messages and
|
||||
`OnEnterZone` never happen for them, and the bridge's tally, which asks ZoneManager's membership, scored
|
||||
nobody already in an arena after a restart. ZoneManager has no public way to look again.
|
||||
|
||||
**`RunicGatewayZones.cs`** (D181, D168) ships beside the bridge and is installed by default (D182). It
|
||||
Harmony-postfixes ZoneManager's `Zone.InitializeZone` — the one method a zone passes through when it is
|
||||
created, updated (`Zone.Reset`) or loaded from ZoneManager's data file — and one tick later enters every
|
||||
connected player ZoneManager's own `IsPositionInZone` places inside, through ZoneManager's own
|
||||
`OnPlayerEnterZone`. A player ZoneManager already counts is skipped, and ZoneManager refuses a second
|
||||
entry anyway. It fixes **every** zone on the server, not only the bridge's. It sweeps all zones once when
|
||||
it patches, and re-patches when ZoneManager reloads. `rgz.status` prints its state.
|
||||
|
||||
The bridge never depends on it. `server.hello` carries, whenever ZoneManager is loaded:
|
||||
|
||||
```json
|
||||
"zoneHelper": { "state": "patched", "version": "0.1.0" }
|
||||
```
|
||||
|
||||
`state` is `patched`, `unsupported` (loaded, but this ZoneManager lacks what it patches — `reason` says
|
||||
what), `no-zonemanager`, or `missing` (the file is not there). **Anything but `patched` and the bridge
|
||||
scores the zones it made by ZoneManager's public `IsPositionInZone`**, for the participation tally and
|
||||
the kill credit alike — so a missing helper costs ZoneManager's flags for somebody already inside, never
|
||||
their score. The website shows the cost on the servers page.
|
||||
|
||||
Walked 2026-09-27: it patched ZoneManager 3.1.14 on Oxide and on Carbon, and the site read
|
||||
`{"state":"patched","version":"0.1.0"}` from both hellos. **Not yet walked with a player standing in a
|
||||
zone** — that needs somebody in the game.
|
||||
|
||||
### 19.7 What the tally counts (F1, F3, F4)
|
||||
|
||||
- **`gathered` is everything a player harvests** (D159). `OnDispenserGather` is the swings;
|
||||
`OnDispenserBonus` (the final hit's bonus), `OnCollectiblePickup` (hemp, mushrooms, stones off the
|
||||
ground) and `OnGrowableGathered` (a farmed plant) now add to the same per-item counter. All three are
|
||||
`void` (§8.7) and in `rg.hooks`. A pick-up another plugin vetoes is still counted — the hook fires
|
||||
before the items move — which is rarer than a second hook is worth.
|
||||
- **`structures` no longer counts your own base.** A building block the attacker placed, or one on a
|
||||
cupboard they are authorised on, is not counted. The `entity.destroyed` frame still goes; the site
|
||||
already skips the raid alert for an authorised attacker (D59).
|
||||
- **Every Steam id comes from `userID`.** The thirteen frames that read `UserIDString` sent `null` for a
|
||||
player another plugin spawned; they now use the same `SteamIdOf` as the map.
|
||||
|
||||
### 19.8 The website
|
||||
|
||||
- **The loading hold** (F7). Permission and title pushes wait while the last hello says
|
||||
`worldReady: false` — the first walk's restart sync went 35 s before the save loaded, timed out, and
|
||||
the retry reported "0 applied". A human's "sync now" is not held. A failed or refused sync logs a
|
||||
warning.
|
||||
- **Link codes** (F5, F6). A code is asked of the servers that minted one in the last six minutes (every
|
||||
`/link` stores an `account.link.requested`) first, then of the rest, each group **in parallel**. The
|
||||
answer is "a server could not be reached — your code is still good" only when one of the minting
|
||||
servers is unreachable; a dead server that minted nothing no longer makes a wrong code look good.
|
||||
- **NPC attackers** (F2, D185) are named by the killfeed: a family (`scientistnpc_*` → Scientist,
|
||||
`bradleyapc` → Bradley APC) or the prefab without its variant digits (`wolf2` → Wolf). No wire change.
|
||||
|
||||
Reference in New Issue
Block a user