docs(runicnpc): stage 7 built and walked; API 6 and the dropLoot switch #320
@@ -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<int>("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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user