From fe85901195fb86e34ec13e833857b636d343e8da Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 5 Oct 2026 06:23:36 -0500 Subject: [PATCH] docs(runicnpc): stage 6 spike, measured on both rigs What rnt.spike6 found on the Oxide and Carbon rigs: press E has no hook but OnPlayerInput; the bar can redraw in place with CUI's update; a phase's kit swap keeps the target but needs EquipWeapon; adds cost 4-8 ms each in one frame; Kits' data file takes an added kit on reload and keeps players' usage. Found on the way: RunicNPC's "// Requires: Kits" makes every Kits reload take RunicNPC and its NPCs down. Two questions for the org lead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- runicnpc/PLAN.md | 126 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/runicnpc/PLAN.md b/runicnpc/PLAN.md index 4dea4c1..cec5e98 100644 --- a/runicnpc/PLAN.md +++ b/runicnpc/PLAN.md @@ -1120,6 +1120,132 @@ greet, hurt or kill lines (D281). A boss's loot table is stage 7's. the announcement in game and in the app, an event's reward reaching the boss's killers, the site's triggers firing, press E on a passive NPC, and the example profiles working on a server with no kits of its own. +**Measured (2026-10-05, `tools/RunicNpcTest.cs` 0.6.0: `rnt.spike6`).** Both rigs, the same 6000 map, with stage 5's +RunicNPC (API 4) loaded, so the fights and adds are RunicNPC's own NPCs. Nobody was connected; the harness used +stand-ins where a player was needed. Each rig ran the whole spike twice, **26 of 26 checks passing on the final run +on each**. Every answer held on both frameworks unless it says otherwise. + +**1. Press E (D278).** + +- **Neither framework has a hook for it.** The Oxide rig's patched `Assembly-CSharp.dll` and Carbon's + `Carbon.Hooks.Oxide.dll` were searched: there is no `OnUseNPC` (the hook older NPC plugins used), and nothing + else that fires on a use aimed at an NPC. +- **Rust does nothing with it either.** A use on an entity is a remote call the client makes when it sees + something to use, and it sees nothing on a scientist. The server's `BasePlayer` never reads the use button. +- **So it is `OnPlayerInput`, which both frameworks raise on every player tick.** RunicNPC checks whether E was + *just* pressed, and only then casts a 3 m ray from the player's eyes. The check costs 7–8 ns a tick (one run on + Carbon read 113 ns); a ray costs 5–7 µs. +- **The ray finds our NPC.** Our NPC is one capsule on Rust's `Player (Server)` layer. Rays at its head, chest and + legs from 1.5, 2.5 and 3.5 m hit it 9 times in 9 on both rigs, with or without the world's layers in the mask. +- A real client pressing E is a walk item: no client was connected. + +**2. The health bar (D274).** + +- **Oxide's and Carbon's CUI both carry `update`**, so a redraw sends only the fill and the text, changed in place, + instead of destroying and re-adding the bar. The whole bar is 580 bytes; a redraw is 434. +- **Building it costs 115–200 µs.** That is once per redraw, not per player, because every viewer gets the same + bar. +- **Finding who sees it costs 6–8 µs as a distance loop over the players** (29 of 40 within 100 m). Rust's sphere + query costs 316–357 µs for the same answer, so the bar uses the loop. +- **Sending it was not measured:** with no client connected, nothing is sent. +- **Clean-up has one place to live.** RunicNPC already removes an NPC from its registry in one method on death, + despawn and unload. The bar is cleared there, for every player shown it. A client that disconnects drops its own + UI. + +**3. A phase's kit swap mid-fight (D275).** Ours fought a stock scientist 15 m away with the revolver kit. Hits on +the scientist were counted by weapon. The swap stripped the inventory, gave the MP5 kit through Kits' `GiveKit`, +and called Rust's `EquipWeapon`. + +| | Oxide | Carbon | +|---|---|---| +| Kept its target through the swap | yes | yes | +| Fired the new weapon | MP5 hit 5–6 times in 6–7 s; the revolver never again | MP5 hit 5–6 times in 8–9 s; the same | +| Without `EquipWeapon` | held nothing for 16 s and never fired | the same | +| A swap's cost | 2.7–7 ms; **100 ms the first time the server made an MP5** | 2.8–4 ms; **111 ms the first time** | + +- **A phase must call `EquipWeapon` itself.** Rust's brain does not pick up a new weapon on its own. +- The one slow swap is Rust loading the MP5's prefab for the first time, as it would for a player's first craft. + +**4. Summoned adds (D275).** While ours fought, 4 and then 10 more of the same profile were placed on a 6 m ring +around it, each point snapped to the navmesh. + +| | Oxide | Carbon | +|---|---|---| +| 4 adds | all placed, on the mesh, fighting its target, in 25.6 ms | the same, in 18.6–30.3 ms | +| 10 adds | all placed, on the mesh, fighting, in 60.3 ms | the same, in 43.8–53.4 ms | + +- **An add costs 4.4–7.6 ms to place (kit included), all in one frame.** Ten at once is a visible 44–60 ms hitch, so + the build places **one add per server frame**. +- No ring point fell off the navmesh in open ground. A ring point that does is snapped to the nearest point within + 3 m, or skipped. + +**5. Kits' data file (D280).** + +- **It is `data/Kits/kits_data.json` on both frameworks** (`oxide/data`, `carbon/data`), one object `_kits` keyed by + kit name. Each kit has its fields plus `MainItems`, `WearItems` and `BeltItems`. The stage 1 harness kit's format + works as written. +- **Kits reads the file only when it loads**, so a kit written to it does nothing until Kits reloads. +- **Kits never writes this file on load or unload.** It writes it only when an admin edits a kit. Its unload saves + players' usage (`player_data.json`), unless the server is shutting down. +- **So a reload keeps every player's usage.** A stand-in claimed a kit with a 1-hour cooldown, held only in Kits' + memory. After the reload it still had 1 use and its cooldown. A real player's usage on the Carbon rig (`walkauto`, + 3 uses) came through too. Every kit already in the file stayed. +- **A reload takes 3.5–15 s on Oxide, which recompiles. On Carbon (`c.reload`, W3) Kits is back in 0.4–2.3 s.** +- **`IsHidden` does not stop a claim.** It hides a kit from Kits' menu, but a player who types its name gets it: the + harness claimed a hidden kit that way. `RequiredPermission` and `RequiredAuth` do stop players. Kits' `GiveKit` + call, which RunicNPC uses, skips both. + +**6. Found on the way: a Kits reload takes RunicNPC down with it.** + +- **RunicNPC's `// Requires: Kits` makes both frameworks unload RunicNPC when Kits unloads, and load it again + after.** + - On Oxide this happens every time; the two compile together ("Kits and RunicNPC were compiled"). + - On Carbon it happens whenever RunicNPC was freshly compiled, which includes every boot: "Unloading 'RunicNPC' + because parent 'Kits' has been unloaded". RunicNPC is back 5–6 s later. After one such reload, Carbon stops + linking the two until RunicNPC is compiled again. +- **What that does:** every NPC RunicNPC has spawned is killed. In the harness, two NPCs a plugin had placed were + there before a Kits reload and gone after it, as an event's would be. Placements come back when RunicNPC loads + again. An event's NPCs do not. +- **Anything that reloads Kits does this:** an admin's `oxide.reload Kits`, the site's config write for Kits (the + bridge reloads the plugin it wrote, `modules/rust/PLAN.md` §21), and D280's own write of the example kits. +- **RunicNPC needs nothing from Kits at compile time.** It reaches Kits only through `[PluginReference]` and `Call`. + The `Requires` line exists only to enforce D217. +- **One recovery trap on Carbon.** Unloading Kits and RunicNPC by hand with `c.unload`, then `c.load`, left both + "requested for compilation" and never loaded. `c.reload` did the same. A restart of the server cleared it. + +**What the answers leave open, for the org lead:** + +1. **Should a Kits reload stop taking RunicNPC down?** For example: an event has placed 8 raiders, and an admin + edits a Kits kit and reloads Kits. Today all 8 vanish at once on both frameworks, and the event cannot get them + back. The choices: + - **Drop `// Requires: Kits` (proposed).** RunicNPC stays loaded through a Kits reload, and its NPCs keep the + gear they already have. For the 1–15 s that Kits is away, a spawn is refused with a warning. When Kits comes + back, RunicNPC checks the profiles' kits again. D217 still holds: with Kits not installed at all, RunicNPC + refuses every profile and says why. + - **Keep it**, and document "never reload Kits while an event runs". +2. **When are the example kits written (D280)?** For example: an admin installs RunicNPC, and its four kits are + written once and Kits reloads. Later the admin deletes `rnpc_raider`, or edits `rnpc_juggernaut`. What happens + at the next restart? + - **Only on the first install (proposed).** A flag in RunicNPC's data records that they were written, so a kit + the admin deleted stays deleted and an edit is never overwritten. + - **On every load, for any example kit that is missing.** + - **Only while the example profile that uses it still exists.** + + Proposed with it: + - the kits are named `rnpc_raider`, `rnpc_campguard`, `rnpc_sniper` and `rnpc_juggernaut`; + - they carry `RequiredPermission: runicnpc.examplekits`, which nobody is granted, so no player can claim one by + name (`IsHidden` alone does not stop that); + - on Oxide the one-time reload takes RunicNPC down and back once (finding 6) unless question 1 drops the + `Requires` line, so the kits are written before RunicNPC spawns anything. + +**What this sets for the build, with nothing to decide:** + +- Press E goes through `OnPlayerInput`. +- The bar redraws only on damage, at most 4 times a second per boss, with `update`. Its viewers come from a + distance loop over connected players, checked again each second for players who walk in or out. +- A phase's kit swap is strip, `GiveKit`, `EquipWeapon`. +- Adds are placed one per frame. + ### Stage 7 — Loot Loot tables authored on the site, the corpse's default loot cleared or kept, a crate on death, corpse removal. -- 2.49.1