Files
docs/modules/rust/PLAN_FIXES.md
wtclaude 1891f4f6fd docs(rust): the step-2 player walk, and D186-D187
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
2026-09-27 03:18:40 -05:00

41 KiB
Raw Blame History

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 §34, D145; D169). Everything here comes from the first walk of 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, 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 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 §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):

  • 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); 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) names no mission hook, and 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 §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'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).