From 846119220e714de55be02f2069c5c7d9547395a4 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 6 Oct 2026 01:43:15 -0500 Subject: [PATCH] docs(runicnpc): stage 7 built and walked; API 6 and the dropLoot switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runicnpc/API.md: version 6, the profile's loot block and a spawn's loot.dropTable override. - rust-link/PROTOCOL.md: §19.15, world.place's dropLoot. - runicnpc/PLAN.md: stage 7's "Built" section. It covers the builds it was tested on (Rust 25681086, Oxide 2.0.7801, Carbon 2.0.262), the harness (s7 25/25, all 270/270, both rigs) and the walk on both rigs through the site. It also lists what building it found: Rust's ApplyLoot after the hook, and the inventory already emptied when the hook runs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- runicnpc/API.md | 29 ++++++++++++++++++++++----- runicnpc/PLAN.md | 46 +++++++++++++++++++++++++++++++++++++++++++ rust-link/PROTOCOL.md | 20 +++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/runicnpc/API.md b/runicnpc/API.md index 230e99c..cfa9736 100644 --- a/runicnpc/API.md +++ b/runicnpc/API.md @@ -1,7 +1,8 @@ # RunicNPC — the API -**API version 5** (RunicNPC stage 6, 2026-10-05). This is the reference for other plugins. Why it has this -shape is in [PLAN.md](PLAN.md) §4 and the decisions D221–D238, D249, D253–D272 and D273–D289. **API 5 added** +**API version 6** (RunicNPC stage 7, 2026-10-06). This is the reference for other plugins. Why it has this +shape is in [PLAN.md](PLAN.md) §4 and the decisions D221–D238, D249, D253–D272, D273–D289 and D290–D301. **API 6 +added** the profile's `loot` block (PLAN.md stage 7); nothing of API 5 changed shape. **API 5 added** the profile's `boss` and `use` blocks and the `passive` role with its `stand` movement, the hooks `OnRunicNpcBossSpawned`, `OnRunicNpcBossPhase`, `OnRunicNpcBossDied` and `OnRunicNpcUsed`, and `role` and `boss` in `RunicNpc_List` (PLAN.md stage 6). Nothing of API 4 changed shape. **API 4 added** the faction @@ -19,7 +20,7 @@ what Oxide's `Call` reaches by name (PLAN.md §1.2). [PluginReference] private Plugin RunicNPC; int api = RunicNPC?.Call("RunicNpc_ApiVersion") ?? 0; -if (api < 5) { /* too old for this caller: say so */ } +if (api < 6) { /* too old for this caller: say so */ } BasePlayer npc = RunicNPC.Call("RunicNpc_Spawn", position, "warden", "plugin:MyPlugin", null) as BasePlayer; ``` @@ -49,7 +50,7 @@ Every NPC has an owner, and the owner decides its lifetime (PLAN.md §2). ### `RunicNpc_ApiVersion()` → `int` -The API version: `5`. +The API version: `6`. ### `RunicNpc_Spawn(Vector3 at, string profile, string owner, JObject overrides)` → `BasePlayer` @@ -281,7 +282,16 @@ All are called on every plugin, and **none of them answers**: a return value is { "at": 0.25, "damageDealt": 1.5, "speed": 1.3, "kit": "warden_heavy", "line": "{name} is enraged!" } ] }, - "use": null + "use": null, + "loot": { + "start": "rust", + "always": [ { "item": "scrap", "min": 100, "max": 300, "chance": 1 }, { "item": "rope", "min": 1, "max": 1, "chance": 0.5 } ], + "pool": { "pick": 1, "nothing": 85, "rows": [ { "item": "lmg.m249", "min": 1, "max": 1, "weight": 10 }, { "item": "explosive.timed", "min": 1, "max": 1, "weight": 5 } ] }, + "crate": "locked", + "hackSeconds": 600, + "corpseSeconds": 0, + "dropTable": true + } } ``` @@ -335,6 +345,15 @@ A passive profile has no `boss` and a `use` block: | `phases[].line` | at most 256 characters | D286. Said only to the players near it (within `barDistance`) and to those who have hurt it. | | `use` | API 5. required on a passive profile, refused on any other | D278. What pressing E on it does, from up to 3 m: `mode` `chat` says one of `lines` (picked at random, each at most 256 characters) in that player's chat; `mode` `window` opens a window with `title` (blank: its name), `text` (at most 2,000 characters) and a Close button. `{player}` in either is the player's name. | +| `loot` | API 6. null, or the block below | D290–D301. null keeps Rust's scientist loot, as before stage 7. | +| `loot.start` | `rust` (default), `kit` or `none` | D290, D300. What the corpse holds before the table: Rust's scientist loot and the clothes it wore; what it carried (its kit) and the clothes; or nothing at all, clothes included. | +| `loot.always` | up to 30 rows: `item` (a short name this Rust has), `min` and `max` (1 or more), `chance` (above 0, at most 1), optional `skin` | D291. Each row rolls on its own. | +| `loot.pool` | `pick` (0 to its rows), `nothing` (a weight, 0 or more), up to 30 `rows` with `weight` (above 0) instead of `chance` | D291. At most `pick` draws by weight, never the same row twice; a draw of "nothing" uses one up. A kill never pays out more than `pick` pool rows. | +| `loot.crate` | null (the corpse), `wooden`, `military`, `elite` or `locked` | D292, D301. The table goes into one of Rust's crates beside the corpse, with Rust's own loot off, and it stays until emptied (a locked crate keeps Rust's own 2-hour removal unhacked). The always-rows plus `pick` must fit its slots: 6, 6, 12 or 24 (the corpse has 24). | +| `loot.hackSeconds` | 1 to 86,400; default 900 | D297. A locked crate's hack time, counted once a player starts the hack. | +| `loot.corpseSeconds` | null (Rust's own 600 s), 0 (no corpse) or more | D294. | +| `loot.dropTable` | true (default) or false | D296. False rolls no table; the start and the corpse's time still apply. An event's Place NPCs step sends it false through a spawn's overrides. | + A phase changes **that boss's own copy** of its profile; another NPC of the same profile is not touched. A boss gives no reward itself (D276, D285). diff --git a/runicnpc/PLAN.md b/runicnpc/PLAN.md index 4efaf6f..96283f4 100644 --- a/runicnpc/PLAN.md +++ b/runicnpc/PLAN.md @@ -1414,6 +1414,52 @@ without a client, so the walk checks it. An unhacked locked crate is removed aft 1. **A "nothing" corpse and the clothes it wore. → D300:** truly empty, clothes too. 2. **How long an unopened crate lasts. → D301:** until it is emptied, as Rust's crates do. +**Built (2026-10-06), three branches and this one.** RunicNPC API 6 (`runicnpc-rust` `feat/stage-7`), the bridge +(`Rust-Plugins` `feat/runicnpc-stage7`) and the site (`Module-Rust` `feat/runicnpc-stage7`). The API is +[API.md](API.md) version 6; the wire is [`PROTOCOL.md`](../rust-link/PROTOCOL.md) §19.15. + +| Piece | What it does | +|---|---| +| RunicNPC API 6 | The profile's `loot` block: the corpse's start (`rust`, `kit`, `none`), the always-rows, the pool with its picks and "nothing" weight, the crate and a locked crate's hack time, the corpse's time and `dropTable`; checked on load (items that exist, at most 30 rows, the table fits its crate or the corpse). The corpse is filled in `OnCorpsePopulate`; a spawn's overrides can set `loot.dropTable` false | +| The bridge | `world.place` with a profile reads the step's `dropLoot`; off, it spawns with `{"loot":{"dropTable":false}}`. `runicnpc_api = 6` | +| The site | The profile form's Loot section (D295), checked in RunicNPC's words, whether an item exists left to RunicNPC on the push; the Place NPCs step's `dropLoot` switch (D296), on by default and sent only when off; "own loot" in the profile list | + +**The builds it was tested on (checked against the latest, 2026-10-06):** Rust 2634.289.1 (buildid 25681086, the +latest public build), Oxide 2.0.7801 and Carbon 2.0.262 (both the latest releases). + +**Tested.** The harness's `s7` group passed 25 of 25 on both rigs: each start, the always-rows over 100 kills against +their chances, the pool's cap and its "nothing" weight (bounds at 3.5σ), each crate, the locked crate's timer, the +corpse's time and the event override. `rnt.run all`: 270 of 270 on both rigs. Module-Rust: 494 server and 67 client +tests, and every `check:*` script. + +**Walked, 2026-10-06, through a walk site, signed in to the admin panel in the browser.** No player was on the rigs, +so a probe plugin killed the NPCs as a stand-in player and read back what they left. + +| Row | Oxide | Carbon | +|---|---|---| +| A loot profile made on the form (a kit start; scrap 25–50 and a sewing kit 1–2 always; a pool of one pick from a tarp and a propane tank; the corpse for 120 s), saved and pushed; the rig holds the block as typed, 100% stored as chance 1 | pass | pass | +| Placed by clicking the live map; the kill's corpse: the kit's revolver and its ammunition, the clothes it wore, scrap and a sewing kit in range, one pool item, none of Rust's scientist loot | pass | pass | +| The corpse there at 60 s and gone at 125 s | pass | pass | +| Edited to start with nothing and a military crate: the corpse truly empty, clothes too (D300), the table in the crate | pass | pass | +| Edited to a locked crate of 17 minutes (1,020 s, longer than Rust's 15): the count started at −120 and the crate was hacked at 901 of 900, on time | pass | pass | +| The military crate still there unopened 25 minutes later (D301) | pass | pass | +| An event's Place NPCs step with "drop loot" off: its NPC's corpse empty and no crate, where the placement's NPCs of the same profile dropped one | pass | — | + +**Not walked:** what a player's screen shows for a locked crate set longer than 15 minutes, while its count is below +zero; it waits for the in-game walk with a player. The server's side of it is walked above. + +**What building it found:** + +- **Rust fills the corpse again after `OnCorpsePopulate`.** `CreateCorpse` raises the hook and then calls + `ApplyLoot` unless the hook answered with a corpse. A kit or empty start answers with the corpse; a `rust` start + answers nothing, so Rust's loot goes in as before and the table is added to it. +- **The NPC's inventory is already empty when the hook runs.** A kit start therefore takes the kit's belt and main + items in our NPC's `OnDied`, before Rust empties them, and moves them into the corpse in the hook. +- **A destroyed corpse compares equal to null in Unity.** The harness checks the corpse's removal with + `ReferenceEquals` and the entity's own destroyed flag. +- **The profile list did not say a profile had its own loot**, where a boss says "boss". Found on the walk and fixed + on the same branch. + ### Stage 8 — On the map and in the app Event NPCs and bosses on the live map (protocol 11's `map.live`), a live "left: 3/8" on the event page, and the same diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 08b5b4e..cb6b7f3 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -2582,3 +2582,23 @@ Stage 6's wire changes **join protocol 13** while it is unreleased (D282); no me `rust.boss.killed` (ceiling `everyone`, default `subscribers`) for app, in-app and email rules; Discord hears a boss only through an event's announce step (D277). The feed shows a boss appearing, and its death where the server's presence setting allows. The profile form gains the boss box and the passive role. + +### 19.15 RunicNPC stage 7: loot (runicnpc PLAN.md stage 7) + +Stage 7's wire change **joins protocol 13** while it is unreleased (D299); no message changes shape, and +`PROTOCOL_VERSION` stays 13. The bridge now needs **RunicNPC API 6**: `RunicNpcApiNeeded = 6`, +**`runicnpc_api = 6`** in `overlay.toml`. + +**A profile's `loot` block** travels in `npc.profiles` and `npc.profiles.set` as RunicNPC holds it (runicnpc +`API.md`); the bridge reads none of it. + +**`world.place` with a `profile` takes `dropLoot`** (D296), optional: + +```json +{"cmd":"world.place","runId":"42","key":"…","profile":"bandit","count":4,"server":"main","x":880,"z":-1850, + "dropLoot":false} +``` + +`false` spawns the NPCs with RunicNPC's override `{"loot":{"dropTable":false}}`: they keep what their profile +starts the corpse with, and roll no loot table. Absent or `true`, as before. The site sends it only when an event's +Place NPCs step switches it off.