PLAYER_WALK's step-2 rows 3-9 walked with the org lead in the game on both rigs (2026-09-27). Records the results, corrects four rows (the helper is unloaded, not deleted; no enter message can show; sleepers do not score; ZoneManager's own entry counted the player anyway), and adds D186 (a link code's second pass skips servers known down) and D187 (no permission sync to a server the poll saw go away) with F6, F7 and F12's as-walked notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
473 lines
41 KiB
Markdown
473 lines
41 KiB
Markdown
# `module-rust` — plan fixes
|
||
|
||
**Status:** plan, approved 2026-09-26; its open questions were answered the same day (D169–D175), and step 1's
|
||
shape was settled as work began (D176–D179), and its rig walk's fixes by D180; step 2's four open choices were
|
||
answered before its code (D181–D185). **Everything in it — fixes and redesigns alike — lands before Module-Rust's cutover
|
||
(phase 19, [`PLAN.md`](PLAN.md) §34, D145; D169).** Everything here comes from the first walk of
|
||
[`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md) with a real player in the game — the Oxide pass, walked by
|
||
the org lead on the `rust-oxide` rig with every frame checked on the console, the sidecar and the site's
|
||
database. The Carbon pass has not been walked yet and may add to this document.
|
||
|
||
This is a companion to [`PLAN.md`](PLAN.md), not a replacement: its decisions continue PLAN.md's numbering
|
||
(D159 onward), and where the two disagree this one is later and wins. It has three kinds of content, and
|
||
all of it lands before the cutover (D169, §6):
|
||
|
||
- **Fixes** (§2) — the plugin, the sidecar or the site does something other than what the plan and the
|
||
protocol say. Fifteen, F1–F15.
|
||
- **Interface and walk-doc corrections** (§3) — what works but misleads, and where the walk doc itself is wrong.
|
||
- **Changes the org lead decided during the walk** (§4) — new behaviour, each with its decision of record.
|
||
|
||
---
|
||
|
||
## 0. What the walk was
|
||
|
||
| | |
|
||
|---|---|
|
||
| Date | 2026-09-26, 16:40–21:05 UTC |
|
||
| Rig | Pterodactyl server 17 `rust-oxide`, Oxide, world 3000 / seed 1234 |
|
||
| Plugin | Rust-Plugins **v0.1.1**, the released file, byte-identical |
|
||
| Sidecar | the phase-17 build, protocol 12 (pre-D155) |
|
||
| Site | the phase-18 walk core on :3270, Module-Rust at phase 17 |
|
||
| Extra plugins | Kits 4.4.9, ZoneManager 3.1.14, BetterChat 5.2.15, PopupNotifications 0.2.1, and — added during the walk — PermissionsManager 2.1.2 and ZoneDomes 2.0.2 |
|
||
|
||
**Scorecard.** Player walk: 1–7, 9, 10 pass (4 and 9 with findings), 8 needs a second player. Identity:
|
||
1–5 pass, 6 behaves as written but that behaviour is F5. Permissions: all seven pass, three with findings.
|
||
Configuration: 1, 3, 4, 6, 7 pass; 2 failed as written (F9) and passed on a retry; 5 not applicable; 8 proven
|
||
at the site's guard only. Events: 1–6 pass in the game, 6's frame is F13; 7 needs several players. Rewards:
|
||
1, 3, 4, 6 pass (6's text is F15); 2 is covered by 3; 5 deferred. Chat titles: 1 authored, not earned; 2–5
|
||
not walked. Map and phone walks: not walked (two accounts, the app).
|
||
|
||
## 1. Decisions of record
|
||
|
||
Taken by the org lead during and straight after the walk.
|
||
|
||
| # | Decision |
|
||
|---|---|
|
||
| **D159** | **"Gathered" is everything a player harvests** — swings, the bonus on the final hit, and what is picked up off the ground. Rejected: swings only (the leaderboard would disagree with the inventory). F1. |
|
||
| **D160** | **The site is the source of truth for all permissions and groups** — those that were on the server before the site, those added in the game, and those added on the site. Everything flows back to the site. **This reverses D31**, under which a permission the site had not authored was "somebody else's business" and never enumerated. |
|
||
| **D161** | **Each server has a policy for a change made in the game: revoke, adopt, or auto-adopt. The default is auto-adopt.** |
|
||
| **D162** | **The permission screen follows uMod PermissionsManager's flow** (the reference is PermissionsManager 2.1.2 on the rig, screenshots in the workspace's `perms-screenshots/`): all players ⇄ all groups → a player or group → its plugins → that plugin's permissions with Granted / Revoked, Grant all / Revoke all → a player's groups, a group's players, Remove all. |
|
||
| **D163** | **Every subject on that screen shows the linked website account, if any, and the in-game name — or the Steam id when the server has no cached name.** |
|
||
| **D164** | **A "kit open for everyone" event is a lease paired with a use credit.** `core.lease` on the kit's permission plus `rust.kit.entitle` (one use per recipient, D103, withdrawn on revert), offered as a template — and the lease form warns when the kit's own limits mean players have already used it up. Rejected: a new lease that rewrites Kits' usage data, which D103 deliberately avoided. |
|
||
| **D165** | **The live map's monument markers get a per-type toggle, and the default shows the major monuments only.** Substations (18 on the rig's map), caves and similar small places are off unless staff turn them on. |
|
||
| **D166** | **The zone step offers ZoneManager's own flags and settings**, read from the installed ZoneManager, not a hard-coded list. |
|
||
| **D167** | **ZoneDomes is an optional dependency.** The zone step gains a "show a dome" option; the bridge adds the dome and removes it with the zone. |
|
||
| **D168** | **When a plugin we depend on does not expose what we need, we may write a small helper plugin for it.** It ships from Rust-Plugins beside the bridge, is optional, and is detected at hello like the other integrations. The bridge keeps calling only public APIs (R2); the helper is where a gap in someone else's plugin gets bridged. |
|
||
| **D169** | **Every fix and every redesign in this document lands before Module-Rust's cutover (phase 19).** Rejected: cutting over after the fixes and following with the redesigns. |
|
||
| **D170** | **An expired zone is marked expired on the site.** Once F13 makes `world.expired` recognisable, ingest marks the run's resource row `expired`. This amends D96's "the website maps it to nothing". F14. |
|
||
| **D171** | **Steam sign-in belongs to the website's own plan and is part of the base website**, exposed to modules however they need it. It leaves this document (§4.8). |
|
||
| **D172** | **Chat titles use all twenty-three conditions the org lead listed, under new names** (§4.6). Rejected: a first subset. |
|
||
| **D173** | **"Quests completed" counts Rust's own missions** — the ones the game's NPCs already hand out on the map. No quest plugin. How the bridge detects a completed mission is settled by a spike on the rig (§4.6), because uMod's catalogue names no mission hook and [`CARBON.md`](CARBON.md) lists none among Carbon's own. |
|
||
| **D174** | **The title rules §4.6 recommended are adopted:** the two kill-distance titles are *best* columns (the longest single kill), not sums; a weapon-class kill counts any kill the player is credited with (players, NPCs, animals), from weapon lists kept in one place; the APC and the helicopter credit the killing blow; healing counts only other players. |
|
||
| **D175** | **The titles in §4.6 are the module's defaults, not fixed names.** An admin can write their own title for any category, and it supersedes the default everywhere that category is used; clearing it brings the default back. A rule may still carry its own text, which wins over both. |
|
||
| **D176** | **F15 is held for protocol 13** and releases with it, not ahead of it as a patch. Rejected: a Rust-Plugins v0.1.2 on protocol 12. |
|
||
| **D177** | **F9 and F10 open protocol 13.** They are built first, on `edge` in Rust-Plugins, Rust-Link and Module-Rust; the rest of §6 step 2 joins the same bump, and all of it releases together. Rejected: F9 alone as protocol 13 with the rest as 14; and a wider window now as a stop-gap. |
|
||
| **D178** | **A reload fails on evidence, not on a clock.** While a configuration reload is pending the plugin reads the framework's new log lines each second and rolls back the moment they show the target plugin failing to compile or initialise. The ceiling is 30 s; when it passes, the plugin checks whether the target is loaded (a hook it missed) before restoring anything, and reports what actually loaded (F10). A framework hook for a failed load, if the rig shows one, replaces the log read. Rejected: the clock alone with a longer ceiling, which leaves a broken plugin down for the whole ceiling. |
|
||
| **D179** | **The configuration page polls the write.** The save is recorded as `reloading`; ingest settles that row from the outcome frame, and the page polls it every couple of seconds until it does. Rejected: pushing the outcome over the admin event stream. |
|
||
| **D180** | **The step-1 rig walk's fixes stay inside the bridge, and the console is a fair way in.** The walk (2026-09-26) found Carbon's Oxide-compatible `ReloadPlugin` does not reload (W3), Carbon's log is `Carbon.Core.log` (W4), Carbon loads a plugin whose config threw and raises `Loaded` anyway (W5), Oxide's "Could not initialize plugin" line was missed (W2), and Oxide's idle compiler drops a reload with "Timed out waiting for plugin to be compiled" (W1). All are fixed in `RunicGateway.cs` (PROTOCOL.md §19.1), never by changing a third-party plugin. Running server console commands (`c.reload`) is approved, and so is a helper plugin that hooks and runs console commands where one is needed (D168). |
|
||
| **D181** | **F12 is fixed by a ZoneManager helper that patches ZoneManager with Harmony** (D168). The patch follows `Zone.InitializeZone` — the one method a zone passes through when it is created, updated or loaded from ZoneManager's data file — and enters every connected player already inside it through ZoneManager's own entry path, so its flags, messages and `OnEnterZone` apply as if they had walked in. It fixes **every** zone on the server, not only the bridge's. Rejected: calling ZoneManager's private `UpdatePlayerZones` by reflection for our zones only; and measuring distance in the bridge with no helper at all. |
|
||
| **D182** | **The helper ships in the overlay beside the bridge and is installed by default** (ZoneManager is already a required plugin). An operator may delete it: the bridge still loads, reports the helper missing at hello, and **scores the zones it made by ZoneManager's public `IsPositionInZone`** — so a missing helper costs the zone's flags for somebody already inside, never their score. The site says so. The same fallback applies when the helper loads but cannot find what it patches. |
|
||
| **D183** | **F14's expiry is core's to record: the resource ledger gains an `expired` status, and a module reports one with `ctx.events.expired({ kind, ref })`.** `expired` is terminal like `reverted` — teardown never tries to take it back — and the run log says `resource.expired`. It is a MODULE_API minor (1.10.0 → 1.11.0), and it must not disturb Module-uo, which is already built against 1.10.0: additions only, no existing status changes meaning. Rejected: asking core to reconcile, which files an expected expiry as `orphaned` (a fault, and still claimable for a revert); and core marking rows expired from `lease_until` on its own clock. |
|
||
| **D184** | **F8's signal is `plugin.loaded` / `plugin.unloaded`, carrying the permissions that plugin added or removed.** The plugin diffs the registered permission list around each load and unload, emits only once the world is ready (a boot is the restart sync's), and the site marks the permission sync dirty when the list is not empty. The same list is §4.1's first evidence of which plugin owns a permission. Rejected: every load, names only. |
|
||
| **D185** | **F2 is the site's: NPC attackers are labelled by the killfeed**, which trims a prefab's variant digits (`wolf2` → wolf) and names the NPC families that read badly otherwise (scientists, bandits, animals). No wire change. Rejected: the plugin sending a display name Rust may not have. |
|
||
| **D186** | **F6, as walked: a link code's second pass skips the servers the site already knows are down.** Asking the non-issuers in parallel was not enough: while any server was down, a code no issuer held — every made-up one — still waited out that server's whole timeout (12 s on both rigs, the step-2 walk). A server the board poll last saw without a connected game is not an issuer, so it cannot hold the code: it counts as offline without the wait. Issuers are asked whatever their state, so a good code on a down server still answers "unsure". Rejected: dropping the second pass (a code typed before its mint is ingested would be refused once); a short timeout on it (still a wait, and a guess at the number). |
|
||
| **D187** | **F7, as walked: no permission sync goes to a server the poll saw go away until a fresh hello says its world is ready.** The `worldReady` hold reads the stored hello, which is the old boot's until the poll reads the new one — and on Carbon a due audit went out in that gap, 80 s before "Server startup complete". The poll already writes `online: 0` the moment a server goes away, so the hold reads that too (titles already did). A human's "sync now" is still not held. |
|
||
|
||
## 2. Fixes
|
||
|
||
Grouped by severity. Each says what the walk saw, why, and the fix. Line numbers are against
|
||
Rust-Plugins v0.1.1 and Module-Rust at phase 17.
|
||
|
||
### 2.1 High
|
||
|
||
**F15 — every non-ASCII character the site sends to the game is corrupted.** *(Rust-Plugins)*
|
||
An event announcement's em dash arrived as `â` in chat and in the console. `RunicGateway.cs:787` reads the
|
||
socket byte by byte and appends `(char)buffer[i]` — UTF-8 decoded as Latin-1. The write side (line 756) is
|
||
correct, so only site→game text breaks: announcements, news in chat and popups, chat titles and styles,
|
||
kit and zone names — **and configuration saves**: a file saved from the website that holds any accented
|
||
letter, dash or emoji is written back to disk mangled. Configuration step 3 passed only because the file
|
||
was ASCII.
|
||
*Fix:* collect bytes per line and decode the finished line with `Encoding.UTF8`; a multi-byte sequence split
|
||
across two reads must survive. *Test:* é, — and an emoji through an announcement and a config save, on both
|
||
frameworks. Add the same line to the walk.
|
||
|
||
**F9 — a valid configuration edit is rolled back because the reload window is shorter than a cold compile.**
|
||
*(Rust-Plugins, Module-Rust)*
|
||
Kits' chat command changed from the site → "The plugin did not come back, so the old file was put back
|
||
automatically". Oxide's log: the compiler had idled out (it stops after 60 s, `oxide.config.json`), restarted,
|
||
compiled Kits in 2.85 s, and loaded it — just after `ConfigReloadWindowSeconds = 4f` (line 4087). The retry,
|
||
needing no compile, passed. The window races work whose length the plugin does not control: host speed,
|
||
plugin size, Carbon's own compiler. Operators must not have to tune Oxide to make the site work.
|
||
*Fix:* answer the save at once ("saved, reloading…") and deliver the outcome as a frame when it arrives, under a
|
||
long ceiling (30 s or more). Roll back on a real failure — Oxide logs "Failed to initialize plugin" the moment
|
||
it happens (configuration step 4 showed it) — not on a clock. Protocol-visible (§5). Settled by D177–D179: the
|
||
plugin reads the log for that failure, the ceiling is 30 s with a loaded check before any restore, and the site
|
||
records the write as `reloading` and polls it.
|
||
|
||
**F10 — the rollback can say "did not come back" about a plugin that did, and can race it.** *(Rust-Plugins)*
|
||
After F9's timeout the plugin restored the file and fired a second reload, but the log shows one compile, so
|
||
which file the running plugin read depends on timing. *Fix:* never restore while a compile of that plugin is
|
||
in flight, and report what actually loaded. F9's fix removes the cause.
|
||
|
||
**F12 — a player already inside a zone when it is created or restored is not "in" it.** *(Rust-Plugins)*
|
||
After a restart the bridge re-created the event zone correctly (same id, same place, row still `confirmed`);
|
||
the org lead woke up 18 m inside it and ZoneManager's `IsPlayerInZone` said nobody. The same happened when a
|
||
zone opened around a player. ZoneManager counts a player only on trigger *enter*, and the participation tally
|
||
asks it every five seconds (§29.7) — **so after any restart mid-event, everybody already in the arena scores
|
||
nothing until they walk out and back in.**
|
||
*Fix:* after creating or re-creating a zone, have ZoneManager re-evaluate who is inside; if its API has no way
|
||
to, that is D168's case (a helper), or the tally measures distance itself for zones the bridge made. Walk it on
|
||
Carbon too. Settled by D181 and D182: ZoneManager has no public way (its `UpdatePlayerZones` is private and
|
||
runs only for a connecting player), so a helper patches `Zone.InitializeZone` with Harmony; without the helper the
|
||
bridge scores its own zones by `IsPositionInZone`.
|
||
*As walked, 2026-09-27, both rigs:* narrower than it reads. Without the helper, ZoneManager 3.1.14 counted the
|
||
player every time we tried — a zone opened around them, a zone re-created by a ZoneManager reload, and a rejoin
|
||
after a restart (its `UpdatePlayerZones` runs on connect) — only late: with the helper in, its sweep one tick after
|
||
`InitializeZone` found the player not yet counted. So the helper makes entry immediate, and its flags apply at
|
||
once; the score never depended on it. What the first walk saw may need a busier server than one player to recur.
|
||
|
||
**F13 — the `world.expired` frame leaves with its kind overwritten.** *(Rust-Plugins, protocol)*
|
||
Three zones expired in the game exactly on time; all three frames were filed as `kind: "zone"`. Line ~7310
|
||
builds `Frame("world.expired", "event")` and then sets `frame["kind"] = e.Kind`. [`PROTOCOL.md`](../../rust-link/PROTOCOL.md)
|
||
§15's example has no field for the entity's kind, so the collision never showed in the spec. The walk's own
|
||
check (`/events?kind=world.expired`) cannot pass. Present since protocol 9.
|
||
*Fix:* send the entity kind as `what`, the name the site's resource rows already use; amend §15. Protocol bump.
|
||
|
||
### 2.2 Medium
|
||
|
||
**F1 — `gathered` counts swings only.** *(Rust-Plugins)* 825 wood in the inventory, 587 tallied; 20 cloth from
|
||
hemp, none. The tally's shape was right (60-second deltas that sum). *Fix (D159):* also count
|
||
`OnDispenserBonus` and `OnCollectiblePickup`, and `OnGrowableGathered` for farmed plants; add all three to
|
||
`ExpectedHooks` so `rg.hooks` shows them. PROTOCOL.md §8.6 names the hooks that feed the counter.
|
||
|
||
**F3 — destroying your own building counts toward the raid stat.** *(Rust-Plugins)* The org lead broke their own
|
||
wall and the next tally carried `structures: 1`. The `entity.destroyed` frame is right, and the site already
|
||
skips the raid alert for an attacker authorised on the cupboard (D59) — only the counter is wrong.
|
||
*Fix:* skip `Structures++` (~line 1700) when the attacker owns the piece or is authorised on its cupboard;
|
||
still emit the frame.
|
||
|
||
**F5 — one dead server makes every wrong link code say "your code is still good".** *(Module-Rust)* With five of
|
||
seven enabled servers offline, a spent code and a made-up `ZZZZZZ` both answered "One of the servers could not
|
||
be reached… Your code is still good", and will until an admin disables the dead server.
|
||
*Fix:* ingest already sees every `account.link.requested` with its server and TTL. Ask only the servers that
|
||
issued a code in the last five minutes, and answer "unsure" only when one of *them* is unreachable
|
||
(`links.model.js:181`).
|
||
|
||
**F6 — redeeming a code waits on every dead server in turn.** *(Module-Rust)* About 21 seconds, four per dead
|
||
server; the rig sorts last, so a successful link waited too. F5's fix usually leaves one server to ask; failing
|
||
that, ask in parallel with a short timeout.
|
||
*As walked, 2026-09-27:* the parallel fleet still took 12 s for a made-up code whenever a server was down —
|
||
one whole timeout on the second pass. Settled by D186; 338 ms after it, 360 ms with a rig stopped as well.
|
||
|
||
**F8 — loading a missing plugin does not re-sync permissions.** *(Rust-Plugins, Module-Rust)* PopupNotifications
|
||
came back at about 18:44; the grant that had been unresolved landed at 18:57, on the fifteen-minute audit. The
|
||
page promises "It will land by itself when the plugin is back" — true, thirteen minutes late.
|
||
*Fix:* emit a frame from `OnPluginLoaded` / `OnPluginUnloaded` when that plugin registers permissions; ingest
|
||
marks the server dirty the way `perm.drift` does. §4.1 needs the same signal. Shape settled by D184.
|
||
|
||
**F11 — the lease target picker was designed and never built.** *(website)* `core.lease`'s own comments say the
|
||
authoring form reads the chosen lease's target source; `EventEditor.jsx` only ever renders `param.source`
|
||
(line 299), so Module-Rust's `rust.options.grouppermissions` is never offered and the target is typed by hand.
|
||
The walk's first attempt put it in the wrong field. Built as part of §4.3.
|
||
|
||
### 2.3 Low
|
||
|
||
**F7 — the restart re-sync fires before the game has loaded.** *(Module-Rust)* With Oxide's permission files
|
||
deliberately wiped, the site restored its group, membership and grant — R2's central promise, proven. But its
|
||
restart sync ran 35 s before "Server startup complete", timed out with no warning in the log (the titles push
|
||
at the same moment did log one), and the retry 2.5 minutes later recorded "0 applied", so nothing says what the
|
||
restart restored. *Fix:* hold the restart sync until the hello says `worldReady: true`; log a failed sync.
|
||
*As walked, 2026-09-27:* the restart sync waited on both rigs, but on Carbon a due *audit* went out the moment
|
||
the sidecar reconnected — the stored hello was still the old boot's. Settled by D187.
|
||
|
||
**F2 — NPC killers are named by prefab.** *(Rust-Plugins or Module-Rust)* "killed by `wolf2`". Send a display name,
|
||
or map prefab to label on the site — whichever the killfeed page already expects. The site's, by D185.
|
||
|
||
**F4 — a death of a player made by another plugin carries `steamId: null`.** *(Rust-Plugins)* `UserIDString` is
|
||
empty for players other plugins spawn; the plugin already avoids it for `entity.destroyed`. Use
|
||
`((ulong)player.userID).ToString()` at all 13 sites. Real connected players are unaffected.
|
||
|
||
**F14 — the site never learns that a zone expired.** *(Module-Rust)* Expired zones stayed `confirmed` on the run
|
||
console until the runs were cancelled, when teardown found them "already gone" and counted that a success. This
|
||
was the design — PROTOCOL.md §15: "The website maps it to nothing. Core learns about it through `reconcile` and
|
||
`revert`" (D96) — and D170 changes it. *Fix:* after F13, ingest maps `world.expired` to the run's resource row
|
||
and marks it `expired`; teardown treats `expired` as already reverted; amend PROTOCOL.md §15. The row is core's
|
||
(`event_run_resources`), so core gains the status and the call (D183, MODULE_API 1.11.0).
|
||
|
||
## 3. Interface and walk-doc corrections
|
||
|
||
**U-1 — "unresolved" is a sentence in the server block, not on the grant.** The org lead granted a permission for
|
||
an unloaded plugin, looked, and saw nothing; the warning sits under the server's row next to "in sync". State
|
||
belongs on each toggle (§4.1).
|
||
|
||
**U-2 — a configuration conflict offers nothing.** "That file changed on the server since you opened it", with no
|
||
way to load the server's version or compare. Offer the server's version beside the operator's edit.
|
||
|
||
**U-3, U-4 — the event step editor is shaped for developers.** Type tags on every field (`LEASE * · STRING`), the
|
||
lease is a text box with a separate "Pick from…" menu, the examples are Ultima Online values shown to a Rust
|
||
admin, and a yes/no value is typed. The org lead: "it should be a dropdown of possible values created
|
||
dynamically". §4.3.
|
||
|
||
**U-5 — Start runs the published version and says nothing about an unpublished edit.** The org lead fixed a step,
|
||
saved, pressed Start, and ran the broken version 1. Ask "Publish and start, or start version 1?".
|
||
|
||
**U-6 — a lease's "10 minutes" lasted one second.** A phase with no advance rule ends as soon as its steps finish,
|
||
and teardown takes the lease back. Nothing in the editor says so. §4.3.
|
||
|
||
**Walk-doc corrections** ([`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md)):
|
||
|
||
- **D-1** — permission step 4 names `zonemanager.admin`, which ZoneManager does not register, and never says the
|
||
permission must already be granted on the site to someone (true today; D160 retires the condition).
|
||
- **D-2** — configuration step 2 names a ZoneManager setting ("Auto Show Zones") that 3.1.14 does not have. Use
|
||
Kits' chat command, which changes something every player can see.
|
||
- Events step 6's check (`kind=world.expired`) cannot pass until F13.
|
||
- Configuration step 8 through the site is refused by the site first; the plugin's own guard needs a direct
|
||
sidecar request with the rig's token.
|
||
- Record that Kits hides a kit the player lacks permission for (`Show kits … without the permission: false`),
|
||
so "locked" in the events and rewards walks means "not listed".
|
||
|
||
## 4. Changes decided during the walk
|
||
|
||
### 4.1 The permission manager, rebuilt around the game's real state (D160–D163)
|
||
|
||
What exists: a desired-state mirror. The site pushes the set it authored, the plugin reports `foreign` holders
|
||
only for names the site claims (`buildDesired` → `managed`), and `rust_perm_catalogue` holds just
|
||
`(server_id, permission, seen_at)` — no owner, no holders, no groups.
|
||
|
||
What it becomes:
|
||
|
||
- **An inventory read** — a new plugin verb returning every registered permission *with the plugin that
|
||
registered it*, every group with its title, rank, parent and members, and what each player holds directly
|
||
and through groups. The walk showed why the owner must come from the registration, not the name:
|
||
PermissionsManager files `zonemanager.ignoreflag.nokits` under Kits.
|
||
- **The first import** — on first contact with a server, everything the inventory reports becomes site-owned
|
||
(D160), so a server brought under the site keeps what it had.
|
||
- **In-game changes** — per D161: auto-adopt by default, or revoke, or adopt by hand. The existing
|
||
Revoke / Adopt answers stay for the two manual policies.
|
||
- **The screen** (D162, D163, U-1) — players ⇄ groups → subject → plugin → permissions with Granted / Revoked;
|
||
each toggle shows its state on each server (granted · waiting for a first connection · unresolved · not
|
||
landed); subjects named by website account and in-game name or Steam id.
|
||
- **The plugin-load signal** (F8), so the inventory and unresolved grants move when a plugin loads.
|
||
- **Open for the plan:** ZoneManager's `zonemanager.ignoreflag.*` permissions exempt players from zone flags
|
||
(§4.4). Under D160 the site owns those too — say how the screen presents them.
|
||
|
||
### 4.2 The kit-weekend template (D164)
|
||
|
||
`core.lease` on `<server>/default/kits.<kit>` plus `rust.kit.entitle` to everyone, authored together. The lease
|
||
form reads the kit's `MaximumUses` and `Cooldown` and warns: "N linked players have already used this kit".
|
||
|
||
### 4.3 The event step editor (U-3–U-6, F11)
|
||
|
||
Pick an action or a lease by its label, and the step becomes that thing's own form: pickers for server, group,
|
||
permission, monument, kit; toggles for yes/no; the limit shown beside minutes. No type tags, no ids, examples
|
||
from the chosen lease. A phase that holds a timed step defaults to lasting that long, or says plainly that it
|
||
won't. Start warns about unpublished changes. `rust.announce` already takes `delivery: popup` (phase 17) —
|
||
show it as a Chat / Popup choice, and consider popups for the other player-facing event messages (a zone
|
||
entered, a reward earned).
|
||
|
||
### 4.4 Zones: ZoneManager's options, and a dome (D166, D167)
|
||
|
||
- **Options.** ZoneManager 3.1.14 takes key/value arguments to `CreateOrUpdateZone`: settings (name, radius,
|
||
size, rotation, enter and leave messages, radiation, comfort, temperature, safe zone, permission, parent, eject
|
||
spawns) and 64 flags — vehicles (`NoVehicleMounting`, `NoVehicleDismounting`, `KeepVehiclesIn`,
|
||
`KeepVehiclesOut`), combat (`PvpGod`, `PveGod`, `NoPve`, `NoFallDamage`), building (`NoBuild`, `NoDecay`,
|
||
`UnDestr`, `NoUpgrade`), loot, NPCs (`NoNPCSpawns`, `NpcFreeze`), comms (`NoChat`, `NoVoice`) and more. The
|
||
plugin reports the installed ZoneManager's flag list at hello; the form groups them and offers presets
|
||
("Arena"). **There is no flying flag** — "allow flying in the zone" needs another plugin or a D168 helper.
|
||
- **Domes.** ZoneDomes exposes `AddNewDome(player, zoneId, type, stack)` and `RemoveExistingDome(player, zoneId)`.
|
||
Three things the walk established: its default sphere (type 0, one layer) is invisible at noon, and red at
|
||
three layers showed only a band near the ground — choose a default by looking at each type on the rig; its
|
||
domes are saved to its own data file and **outlive the zone** unless the bridge removes them at teardown and
|
||
at expiry; and after a restart it loads before the bridge re-creates the zone, so the bridge must re-add the
|
||
dome. Check that a null `player` is safe (it is used for messages only).
|
||
- F12 and F13 land in the same code.
|
||
|
||
### 4.5 The live map (D165)
|
||
|
||
A per-type marker setting for staff, defaulting to the major monuments. Decide in the plan whether the plugin's
|
||
monument sweep or the site does the filtering; the type (`monument_substation`, cave, …) is already in the
|
||
monument data.
|
||
|
||
### 4.6 Chat titles: twenty-three conditions (D172)
|
||
|
||
All of the conditions the org lead listed, each with a new default title. The conditions are decided; the titles
|
||
are **defaults an admin can replace** (D175). Every hook below is in uMod's catalogue ([`HOOKS.md`](HOOKS.md)); each is declared `void`, as the read
|
||
path requires (PROTOCOL.md §8.7), and each must be checked on Carbon, whose catalogue renames some.
|
||
|
||
| Condition | Title | Where the count comes from |
|
||
|---|---|---|
|
||
| Animal kills | **Beastbane** | `OnEntityDeath`, victim a `BaseAnimalNPC` — today folded into `npcKills`; split it |
|
||
| NPC kills | **Exterminator** | `OnEntityDeath`, victim a human NPC (scientists, bandits) — the other half of that split |
|
||
| Players killed | **Reaper** | the existing `kills` column (`player.death` with a player attacker) |
|
||
| Bow kills | **Fletcher** | a kill whose weapon is a bow or crossbow — the weapon shortname the killfeed already carries |
|
||
| Melee kills | **Brawler** | a kill with any `BaseMelee` weapon |
|
||
| Blade kills | **Duelist** | a kill with a bladed melee weapon (machete, swords, knives) — a named list |
|
||
| Revolver kills | **Outlaw** | a kill with the revolver or the python |
|
||
| Headshots | **Crownbreaker** | a killing blow with `HitInfo.isHeadshot` |
|
||
| PvP kill distance | **Longshot** | the *longest* single player kill — the `distance` the killfeed already carries |
|
||
| PvE kill distance | **Marksman** | the longest single NPC or animal kill |
|
||
| APC kills | **Tankbreaker** | `OnEntityDeath`, victim a `BradleyAPC`, credited to the killing player |
|
||
| Helicopter kills | **Rotorbane** | `OnEntityDeath`, victim a patrol helicopter (and the CH47), credited to the killing player |
|
||
| Wood gathered | **Timberwright** | `gathered.wood`, all three sources (D159) |
|
||
| Ore gathered | **Prospector** | `gathered` metal, sulfur and high-quality ore, all three sources |
|
||
| Plants gathered | **Greenhand** | `OnGrowableGathered` plus picked-up plants (hemp, berries, mushrooms, corn, potatoes, pumpkins) |
|
||
| Clothes crafted | **Seamster** | `OnItemCraftFinished`, item category attire |
|
||
| Weapons crafted | **Armorer** | `OnItemCraftFinished`, item category weapon |
|
||
| Structures built | **Mason** | `OnEntityBuilt` placing a building block |
|
||
| Structures repaired | **Tinkerer** | `OnStructureRepair` |
|
||
| Players healed | **Mender** | healing *another* player — `OnHealingItemUse` on someone else, and `OnPlayerRevive` |
|
||
| Rockets fired | **Artillerist** | `OnRocketLaunched` |
|
||
| Explosives thrown | **Demolitionist** | `OnExplosiveThrown` and `OnExplosiveDropped` |
|
||
| Quests completed | **Wayfarer** | Rust's own NPC missions (D173) — detection settled by the spike below |
|
||
|
||
**Defaults, and the admin's own (D175).** Phase 17 already has the machinery: a title rule, per server, ranks
|
||
one stat for the current wipe and gives its top N a title (`rust_title_rules`: `stat`, `top_n`, `text` of up to 24
|
||
characters with `[ ] < > { }` stripped, `color`, `position`). Today `stat` is one of `kills`, `npckills` and
|
||
`playtime`; the twenty-three categories become more values of it. What changes is where a rule's text comes from,
|
||
in three layers, the first that is set winning:
|
||
|
||
1. **The rule's own text**, if the admin typed one for that rule.
|
||
2. **The admin's title for the category** — set once for the site, on the titles screen, and used by every rule
|
||
of that category on every server that has not typed its own.
|
||
3. **The module's default** from the table above, shipped with the module.
|
||
|
||
So an admin who renames Beastbane to "Hunter" does it once, and every animal-kill rule shows "Hunter" from the next
|
||
push; clearing it brings Beastbane back. The same 24-character limit and markup stripping apply to an admin's
|
||
title as to a rule's. New rules open with the category's current title filled in, so what the admin sees is what
|
||
the players will see. Nothing is stored per player — titles are still computed from the standings at each push
|
||
(D135), so a rename is never stale.
|
||
|
||
The rules, adopted (D174):
|
||
|
||
- **Two kinds of column.** Every existing leaderboard column is a sum; the two distance titles are a *best* (the
|
||
largest single value). The tally carries the interval's maximum, and the site keeps the running best.
|
||
- **A weapon-class kill counts any kill the player is credited with** — players, NPCs and animals — so Fletcher
|
||
rewards a hunter as well as a raider. The weapon lists (bow, blade, revolver) are named sets in one place, so a
|
||
Rust update adds a weapon by editing a list.
|
||
- **A vehicle kill goes to the killing blow.** The APC and the helicopter are usually killed by several people;
|
||
credit the player who dealt the last hit (`HitInfo.InitiatorPlayer`) — the rule the killfeed already uses.
|
||
- **Healing counts only other players**, never yourself — otherwise Mender is earned by bandaging after every fight.
|
||
- **Counters are aggregates** (rule 2): all of these ride on `player.tally`, never one frame per shot, craft or swing.
|
||
|
||
**Wayfarer: Rust's own missions (D173).** The game already has quests — the missions its NPCs hand out at
|
||
monuments — so no quest plugin is installed and none is needed. What is not known is how to see one finish:
|
||
**uMod's hook catalogue ([`HOOKS.md`](HOOKS.md)) names no mission hook, and [`CARBON.md`](CARBON.md) lists none
|
||
among Carbon's own**, so the detailed plan
|
||
starts with a spike on the rig, trying in this order and keeping the first that works on both frameworks:
|
||
|
||
1. **A framework hook** the catalogues missed — check Oxide's and Carbon's current hook lists at build time, since
|
||
both grow with Rust updates.
|
||
2. **Read the game's own record.** The player object keeps its missions and their status; at each tally flush the
|
||
plugin counts the ones that became *completed* since the last flush. No hook, main thread, public fields only
|
||
(R2). The spike confirms the field names and statuses against the live server's assembly.
|
||
3. **A D168 helper** that patches mission completion (both frameworks load Harmony) and raises a hook the bridge
|
||
subscribes to — the last resort, because a patch breaks when Facepunch changes the method.
|
||
|
||
The spike's pass condition is a person on the rig taking a mission from an NPC, finishing it, and seeing Wayfarer's
|
||
count move by exactly one — and not moving for a mission abandoned or failed.
|
||
|
||
### 4.7 NPCs
|
||
|
||
Research before deciding: which free uMod NPC plugins are maintained and expose spawning with a loadout, a
|
||
name and a behaviour, against extending `rust.npc.place` to dress the stock scientist from a Kits kit and give it
|
||
a display name. The goal the org lead named is server customisation — NPCs with different kits and names.
|
||
|
||
### 4.8 Steam sign-in (moved to the website's plan, D171)
|
||
|
||
Steam as a sign-in provider beside Google and Discord is part of the base website and is planned there, not
|
||
here. What this module needs from it: a way to learn a signed-in account's verified Steam id, so linking by
|
||
Steam can stand beside the in-game code (R1). Steam is OpenID 2.0, not OAuth2/OIDC, so the website needs its
|
||
own adapter.
|
||
|
||
## 5. Protocol
|
||
|
||
One bump, **protocol 13**, carries every wire change here, so the plugin, sidecar, module and PROTOCOL.md move
|
||
once:
|
||
|
||
- `world.expired` carries `what` (F13).
|
||
- The configuration reload outcome as an asynchronous frame (F9).
|
||
- A plugin-loaded / unloaded frame (F8), with the permissions it added or removed (D184).
|
||
- The ZoneManager helper's state at hello (D182).
|
||
- The permission inventory verb and its reply (§4.1).
|
||
- ZoneManager's flags at hello, zone options on `world.zone`, the dome option (§4.4).
|
||
- New tally fields: D159's gathering sources, the animal / NPC split, weapon-class kills, headshots, crafting,
|
||
building, repairs, heals, rockets, explosives, and the two per-interval maxima for kill distance (§4.6).
|
||
- `world.expired` handled by the site (F14, D170) — no wire change beyond F13's.
|
||
|
||
F15 changes no message shape, but it is held for the bump and releases with it (D176). The work lands on `edge`
|
||
in Rust-Plugins, Rust-Link and Module-Rust, and the three cut over to `main` together once §6 step 2 is done
|
||
(D177).
|
||
|
||
## 6. Order, and what gates the cutover
|
||
|
||
**All of it lands before Module-Rust's cutover (D169).** The order inside that:
|
||
|
||
1. **First:** F15 (data corruption), and F9 + F10 (they throw away edits) — the opening of protocol 13, on `edge`
|
||
(D176, D177).
|
||
2. **Protocol 13 with the remaining fixes:** F13, F14, F12, F1, F3, F8, F7, F2, F4 in the bridge and the module,
|
||
and F5/F6 in the module.
|
||
*As built, 2026-09-27* (decisions D181–D185): Rust-Plugins#18, Module-Rust#22, Rust-Link#19 (the egg),
|
||
installer#35 and website#209 (core's `expired` status, MODULE_API 1.11.0). The spec is
|
||
[`PROTOCOL.md`](../../rust-link/PROTOCOL.md) §19.4–19.8. Walked on both rigs without a player: the
|
||
bridge and the ZoneManager helper compile and load, the helper patches ZoneManager 3.1.14, the hello
|
||
carries its state, and `plugin.loaded` / `plugin.unloaded` carry the right permissions. The site side
|
||
was walked the same night on a walk core running Module-Rust#22 on website#209, against `rust-oxide`: the
|
||
servers list shows the helper `patched`; a Kits unload and load was synced as `dirty` 24 s later (F8);
|
||
a 3-minute zone expired into `expired` with `resource.expired` and teardown never touched it (F13, F14)
|
||
— which found a run left `pending` for ever when its last resource expires while it runs, fixed in
|
||
website#209; a restart's sync waited for `worldReady` and went after it (F7); and `ZZZZZZ` with five
|
||
servers down answered "unknown or has expired", once, in one timeout (F5, F6). The rows of
|
||
[`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md)'s step-2 section that need somebody in the game (3–9)
|
||
were walked with the org lead in the game on 2026-09-27, on Oxide and then Carbon, and all passed but row 8's timing: the gather
|
||
tally matched the inventory to the unit, a player's own wall was not a raid, a zone scored a player who stood
|
||
still through its opening, a restart and a ZoneManager reload, with the helper and without it, and the killfeed
|
||
read Bear, Wolf, Boar and Scientist. The walk found two gaps, both fixed in Module-Rust on `edge` and walked on
|
||
the walk core: a made-up code still waited one timeout on a dead server (D186), and a due audit went to a
|
||
restarting Carbon before its hello was read (D187). F12 turned out narrower than written (see F12). Not walked:
|
||
a player killed by another player — there was no second player.
|
||
3. **The redesigns**, each planned in detail before code and walked on both frameworks: the permission manager
|
||
(§4.1), the event step editor with the kit-weekend template (§4.2, §4.3), zones and domes (§4.4), the map's
|
||
marker types (§4.5), the chat title conditions (§4.6), and the NPC research (§4.7).
|
||
4. **The cutover**, once the re-walk (§8) passes on Oxide and Carbon.
|
||
|
||
## 7. Helper plugins (D168)
|
||
|
||
Where the walk already points at one:
|
||
|
||
- **ZoneManager** — re-evaluating who is inside a zone when it is created, updated or loaded (F12). It has no API
|
||
for it, so this is the first helper, built in step 2 (D181, D182). A flying permission per zone (§4.4) may join
|
||
it.
|
||
- **ZoneDomes** — only if a null `player` turns out to be unsafe in its API.
|
||
- **Kits** — none needed: D164 uses the existing credit path.
|
||
|
||
A helper is optional, versioned and released with the bridge, detected at hello, and does one job. The bridge
|
||
never depends on one to load.
|
||
|
||
## 8. The re-walk
|
||
|
||
After step 2 of §6, on both frameworks:
|
||
|
||
- The whole player walk again, adding: a line with é, — and an emoji through announce and a config save (F15); a
|
||
configuration save that needs a cold compile (F9); wood from a felled tree and picked-up hemp (F1); your own
|
||
wall (F3); a code redeemed with a server down (F5, F6).
|
||
- A zone restarted with a player standing in it, and a zone opened around a player (F12).
|
||
- A zone left to expire, checked by `kind=world.expired` (F13).
|
||
- A plugin unloaded, a grant made, the plugin loaded again — the grant lands within a minute (F8).
|
||
- An expired zone reads `expired` on the run console (F14).
|
||
- Each redesign's own walk, and every title condition earned at least once.
|
||
- Then the steps still owed: the Carbon subset, player step 8 and the map walk with a second player, events step
|
||
7 with several players, the phone walks.
|
||
|
||
## 9. Answered, and what stays open
|
||
|
||
The four questions this plan first asked were answered on 2026-09-26: the gate (D169), F14 (D170), Steam sign-in
|
||
(D171) and the title conditions (D172) — then quests (D173), the title rules (D174) and admin-owned title names
|
||
(D175).
|
||
|
||
Left for the detailed plans of §6 step 3, none of them blocking the fixes:
|
||
|
||
1. How a completed mission is detected — the spike in §4.6 (D173).
|
||
2. Whether the plugin or the site filters monument types (§4.5).
|
||
3. The ZoneDomes default type and stack, chosen by looking at each on the rig (§4.4).
|
||
4. The NPC route — a plugin, or extending `rust.npc.place` (§4.7).
|