Files
docs/modules/rust/PLAN_FIXES.md
wtclaude 7834ef1a88 docs(rust): plan fixes — D169-D172, the org lead's answers
D169 every fix and redesign lands before Module-Rust's cutover. D170 an
expired zone is marked expired on the site (F14 moves from "by design"
to a fix, amending D96). D171 Steam sign-in moves to the website's own
plan as part of the base website. D172 chat titles use all twenty-three
listed conditions under new names: §4.6 is now a table of condition,
title and the hook that feeds it, plus what the detailed plan must
settle (best-value columns, weapon classes, vehicle-kill credit, heals
of others, quests). PLAN.md §35 and the rust README follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 16:27:52 -05:00

28 KiB
Raw Blame History

module-rust — plan fixes

Status: plan, awaiting the org lead's approval, 2026-09-26; its four open questions were answered the same day (D169–D172). 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.

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).

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.

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.

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.

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.

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.

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.

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 under a new title. The titles are proposals; the conditions are decided. 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 a quest plugin — none is installed; choose one, or a D168 helper

What the detailed plan has to settle before building:

  • Two kinds of column. Every existing leaderboard column is a sum; the two distance titles need a best (the largest single value). The tally carries the interval's maximum, and the site keeps the running best.
  • What a weapon-class kill counts. Recommended: 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.
  • Credit for a vehicle kill. The APC and the helicopter are usually killed by several people. Credit the player who dealt the killing blow (HitInfo.InitiatorPlayer) — simple, and the rule the killfeed already uses.
  • Healing someone else, 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.
  • Quests wait on a quest plugin; the other twenty-two do not.

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).
  • 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 and ships ahead of the bump.

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). Small; F15 changes no message shape.
  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.
  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 the bridge just created or restored (F12), if it has no API for it; and a flying permission per zone (§4.4).
  • 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).

Left for the detailed plans of §6 step 3, none of them blocking the fixes:

  1. The quest plugin behind Wayfarer — or a D168 helper.
  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).