docs(rust): the chat-title conditions as built — PROTOCOL 19.10, PLAN_REDESIGNS 5.8 and D209 #295

Merged
whitlocktech merged 3 commits from docs/rust-title-conditions into main 2026-09-29 17:00:43 +00:00
2 changed files with 86 additions and 2 deletions
Showing only changes of commit e3c5d7128c - Show all commits

View File

@@ -4,7 +4,8 @@
On 2026-09-28 the org lead added the first-class optional plugins (§11, D199–D202), and after the RaidableBases spikes, D203–D208 (§11.4).
It is step 3 of [`PLAN_FIXES.md`](PLAN_FIXES.md) §6: the six changes the org lead decided during the first
player walk, each "planned in detail before code". The org lead asked for all six in one plan (2026-09-27), so
they can be read together. No code has been written for any of them yet.
they can be read together. **Built so far:** §1, walked 2026-09-28 (§1.9); §5, built 2026-09-28 with its
rig walk still to come (§5.8, D209).
This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this
document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D188**.
@@ -526,6 +527,47 @@ shirt and a weapon; build and repair a wall; bandage and revive another player;
destroy an APC; shoot down a helicopter; finish a mission and abandon one. Each count is checked in the tally
frame and on the titles screen.
### 5.8 As built (2026-09-28; not yet walked)
Rust-Plugins `feat/title-conditions`, Module-Rust `feat/title-conditions`. The wire is
[`PROTOCOL.md`](../../rust-link/PROTOCOL.md) §19.10, and the admin API is the module's swagger fragment
(`/admin/rust/title-categories…`).
**One question the plan left open, answered (D209):** a rule's `npckills` now ranks **human NPCs only**,
every NPC kill less the animals. Titles had never shipped, so no live rule changes meaning; the rigs'
walk database held one such rule.
**What the build settled:**
- **The categories are ids a rule stores**, in `model/titles/titles.js`: `kills`, `npckills`,
`animalkills`, `bowkills`, `meleekills`, `bladekills`, `revolverkills`, `headshots`, `pvpdistance`,
`pvedistance`, `apckills`, `helikills`, `wood`, `ore`, `plants`, `attire`, `weapons`, `built`, `repaired`,
`healed`, `rockets`, `explosives`, `missions`, plus `playtime`, which predates them. **Playtime has no
default title**, so it cannot be renamed and a playtime rule always carries its own text.
- **Each category names where it is read from:** a summed column, a column's `MAX` (the two distances), a
difference (`npc_kills − animal_kills`, in signed arithmetic, since a row counted before `animal_kills`
existed would otherwise wrap), or a named list over `rust_weapon_kills` or `rust_gather_totals`. Every
standing is ordered by value, then by who was seen last, the leaderboard's own tie-break.
- **The lists are one file**, `model/titles/lists.js`: bow, melee, blade, revolver, wood, ore and plants.
Every blade is also melee. Plants include cloth, which is what hemp picks up as, and leave out seeds.
- **A rule's text may be empty**, meaning its category's title. **A text typed as only markup is still
refused** (§33.4 reading 3): brackets are a mistake, not a request for the default. The form shows the
category's current title as a placeholder and never saves it into the rule.
- **The category titles** are `GET /admin/rust/title-categories` and
`PUT /admin/rust/title-categories/:stat` (an empty `text` is the Reset). A change forgets every server's
cached answer, so the next push carries the new name. The servers list also returns them, so each
server's summary line names what its rules show.
- **The plugin's choices, each to be checked by the walk:** a repair counts only when the entity's health
rose by the next tick (the hook fires before the game decides there is anything to repair); a syringe
revive raises both the revive and the heal hook and counts once; an explosive is a `TimedExplosive` that
deals damage, which leaves out supply signals and smoke; the helicopter's last-hitter credit (§5.4) is
built rather than waiting for the walk, because proving it unnecessary needs a helicopter shot down
first.
**Still to do before the walk:** the weapon lists are from knowledge of the game, not yet read off the
rig's item list. A read-only probe that lists every held item's prefab and class is written, and it runs
once the rigs' storage is back (it was unmounted on 2026-09-28, §5.7's walk waits on it too).
---
## 6. NPCs (§4.7)
@@ -639,6 +681,12 @@ The org lead added §11 on 2026-09-28 and answered its questions the same day:
| **D207** | **A step places its base either where RaidableBases picks (the default, D203) or at an admin-given spot:** x and z, plus either a height adjustment or an exact height. The spot needs the raids helper; without it the step offers only the plugin's pick. | Only an admin-given spot; keeping D203 alone. | 11.4.5 |
| **D208** | **A blocked spot is refused, with the reason.** The step does not spawn when the plugin's area check fails or the helper's water and terrain test fails, and the run log says why ("player building", "raid base", "in water"). The editor runs the same check when the step is saved. | Falling back to the plugin's pick; spawning anyway. | 11.4.5 |
The §5 build raised one more, answered on 2026-09-28:
| # | Decision | Rejected | § |
|---|---|---|---|
| **D209** | **A title rule's "NPC kills" ranks human NPCs only** (scientists and bandits): every NPC kill less the animals, which Beastbane ranks. The public leaderboard's NPC column still counts both (§5.2). | Keeping the old all-NPC count as a 25th category beside Exterminator. | 5.8 |
## 11. First-class optional plugins (D199–D202)
The org lead named four plugins the module supports as first-class integrations. Kits already is one. The other

View File

@@ -461,7 +461,7 @@ Every kind protocol 2 defines, and the hook behind it. **`class` is not a field
| `player.respawned` | `OnPlayerRespawned` | **presence** | steamId |
| `player.death` | `OnPlayerDeath` | **presence** | victim, attacker, attackerType, weapon, distance, grid |
| `player.chat` | `OnPlayerChat` | **presence** | steamId, name, channel, message |
| `player.tally` | *aggregate* — see §8.6 | **presence** | steamId, gathered{}, npcKills, structures |
| `player.tally` | *aggregate* — see §8.6 | **presence** | steamId, gathered{}, npcKills, structures; from protocol 13 the chat-title counts (§19.10) |
| `entity.destroyed` | `OnEntityDeath` on owned building blocks — **and doors, external walls and the cupboard from protocol 7 (§13)** | **staff** | ownerId, prefab, grid, attacker; from protocol 7 also `structure`, `buildingId`, `authorized` |
| `player.reported` | `OnPlayerReported` | **staff** | reporter, target, subject, message, type |
| `player.banned` / `player.unbanned` | `OnUserBanned` / `OnUserUnbanned` | **staff** | id, name, **ip**, reason |
@@ -2245,3 +2245,39 @@ logs its summary instead of replying.
Walked on both rigs, 2026-09-27/28. Oxide gave 85 permissions, every one owned (`RustCore` owns
`oxide.*`), in 148 ms over four ticks. Carbon gave 104, the 30 `adminmodule.*` unowned, in 204 ms. On
both, **`zonemanager.ignoreflag.nokits` belongs to ZoneManager**.
### 19.10 What the tally adds for chat titles
The twenty-three chat-title conditions (`docs/modules/rust/PLAN_REDESIGNS.md` §5, D172–D174) ride on
`player.tally` (rule 2: aggregates, never a frame per shot, craft or swing). Every new field is
**absent when nothing moved**, as the existing three are, so a site on an older build ignores them and a
frame from an older plugin has none:
```json
{"kind":"player.tally","type":"event","steamId":"76561198038695917","name":"whitlocktech",
"npcKills":3,"animalKills":2,"weaponKills":{"bow_hunting.entity":2,"machete.weapon":1},
"headshots":1,"bestPvpM":187.5,"bestPveM":42.1,"apcKills":1,"heliKills":1,
"crafted":{"attire":3,"weapon":1},"built":12,"repaired":4,"healed":2,"rockets":6,
"explosives":3,"missions":1}
```
| Field | Counted from | Notes |
|---|---|---|
| `animalKills` | `OnEntityDeath`, victim a `BaseNpc` | **Also** counted in `npcKills`, which keeps meaning every NPC and animal so the public column does not change mid-wipe. Human NPCs are the difference (D209) |
| `weaponKills` | every credited kill, any victim | By the **same weapon short prefab name the killfeed carries** (`WeaponPrefab`, else `Weapon`). The site classifies them into bow, melee, blade and revolver lists it keeps (§5.3), so a new Rust weapon needs no plugin release |
| `headshots` | the killing blow's `HitInfo.isHeadshot`, any victim | |
| `bestPvpM`, `bestPveM` | the killfeed's distance | **This interval's maximum, not a sum.** The site keeps the running best with `GREATEST`. PvE is NPCs and animals |
| `apcKills`, `heliKills` | `OnEntityDeath`, victim `BradleyAPC`; `PatrolHelicopter` or `CH47Helicopter` | The killing blow's player (D174). A patrol helicopter usually dies by crashing and its blow names nobody, so **the last player who hurt it within 60 s** is credited instead, recorded by `OnEntityTakeDamage` for those three types only |
| `crafted` | `OnItemCraftFinished`, by `item.info.category` lower-cased | Every category is sent; the site reads `attire` and `weapon`. Counts `item.amount` |
| `built` | `OnEntityBuilt` placing a `BuildingBlock` | A deployable is not a structure |
| `repaired` | `OnStructureRepair` | The hook fires on every hammer hit **before** the game decides there is anything to repair, the resources for it, or no recent attack. So the health is read then and again next tick, and only a hit that raised it counts |
| `healed` | `OnHealingItemUse` on **another** player; `OnPlayerRevive` | Yourself never counts (D174). A syringe on a downed player raises both hooks for one use; it counts once |
| `rockets` | `OnRocketLaunched` | |
| `explosives` | `OnExplosiveThrown`, `OnExplosiveDropped` | A `TimedExplosive` that deals damage: a supply signal and a smoke grenade go through the same hooks and do not count |
| `missions` | `OnMissionSucceeded` | Rust's own NPC missions (D173). A failed or abandoned one raises a different hook and never counts |
**All ten hooks are `void`** (§8.7). Four of them can veto — `OnStructureRepair`, `OnHealingItemUse`,
`OnPlayerRevive`, and `OnEntityTakeDamage`, which cancels the damage — and none returns anything. All ten
are in `ExpectedHooks`, so a framework that stops raising one shows as zero in `rg.hooks`.
No sidecar change: the sidecar stores and forwards `player.tally` whole, as it does every frame.