The twenty-three title names in §4.6 are the module's defaults. An admin can set their own title per category, once for the site, and it supersedes the default on every server; clearing it restores the default. A rule's own text still wins over both. §4.6 describes the three layers on top of phase 17's rust_title_rules (stat grows from three values to the twenty-three categories). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
422 lines
31 KiB
Markdown
422 lines
31 KiB
Markdown
# `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–D175). **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. |
|
||
|
||
## 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`](../../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.
|
||
|
||
**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`](../../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).
|
||
- 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) — 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).
|