docs(runicnpc): stage 4 built and walked; protocol 13 §19.12; API 3
PROTOCOL.md §19.12: RunicNPC over the bridge — integrations.runicNpc, the npc.profiles, npc.profiles.set, npc.placements and npc.placement commands and their refusals, the npc.died, npc.health and npc.placement.changed frames, world.place with a profile, attackerNpc and attackerProfile, the tally's npcProfileKills, the four sidecar routes, and overlay.toml's runicnpc_api. API.md: API 3 — RunicNpc_AddPlacement, RunicNpc_RenamePlacement, RunicNpc_RespawnPlacement and OnRunicNpcPlacementChanged. PLAN.md stage 4: what was built in five repositories, the walk on both rigs row by row, what was not walked and why, and what building it found. rust-link/INSTALL.md: RunicNPC as the bundle's third file, by hand, the egg, doctor and uninstall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
# RunicNPC — the API
|
||||
|
||||
**API version 2** (RunicNPC stage 2, 2026-09-30). This is the reference for other plugins. Why it has this
|
||||
shape is in [PLAN.md](PLAN.md) §4 and the decisions D221–D238.
|
||||
**API version 3** (RunicNPC stage 4, 2026-09-30). This is the reference for other plugins. Why it has this
|
||||
shape is in [PLAN.md](PLAN.md) §4 and the decisions D221–D238 and D249. **API 3 added** `RunicNpc_AddPlacement`,
|
||||
`RunicNpc_RenamePlacement`, `RunicNpc_RespawnPlacement` and the hook `OnRunicNpcPlacementChanged`, for a
|
||||
website that lists, edits and creates placements (PLAN.md stage 4); nothing of API 2 changed.
|
||||
|
||||
RunicNPC is called by name through `Call`, like any Oxide or Carbon plugin API. Every call is prefixed
|
||||
`RunicNpc_`, so no hook of another plugin is ever matched by accident. Every call is a non-public method, which is
|
||||
@@ -11,7 +13,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 < 2) { /* too old for this caller: say so */ }
|
||||
if (api < 3) { /* too old for this caller: say so */ }
|
||||
|
||||
BasePlayer npc = RunicNPC.Call("RunicNpc_Spawn", position, "warden", "plugin:MyPlugin", null) as BasePlayer;
|
||||
```
|
||||
@@ -41,7 +43,7 @@ Every NPC has an owner, and the owner decides its lifetime (PLAN.md §2).
|
||||
|
||||
### `RunicNpc_ApiVersion()` → `int`
|
||||
|
||||
The API version: `2`.
|
||||
The API version: `3`.
|
||||
|
||||
### `RunicNpc_Spawn(Vector3 at, string profile, string owner, JObject overrides)` → `BasePlayer`
|
||||
|
||||
@@ -117,6 +119,30 @@ in the air.
|
||||
|
||||
Removes a placement and its NPCs.
|
||||
|
||||
### `RunicNpc_AddPlacement(JObject placement)` → `Dictionary<string, object>`
|
||||
|
||||
API 3. **Creates a placement and names it as `rnpc place` does**: the profile's name and the first free
|
||||
number, `warden-3` (D241, D246). It is checked and refused exactly as `rnpc place` is, with the same
|
||||
sentences, so a placement made from a website and one made in game can never disagree.
|
||||
|
||||
- `placement` is a placement (below) without an id. **A `position` without `y` is a point on a map**
|
||||
(D245): it is put on the ground there by a ray down on terrain and rock, never a building or a tree (a
|
||||
roof is placed in game), then checked against the navmesh like any other. A point off the map, or under
|
||||
water, is refused.
|
||||
- The answer is `{id, position, built, cost}`: the name given, where it landed, whether that spot is on
|
||||
something players built (D239's warning), and the cost warning (D227). Or `{error}`, the sentence
|
||||
`rnpc place` would have said.
|
||||
|
||||
### `RunicNpc_RenamePlacement(string from, string to)` → `string`
|
||||
|
||||
API 3. Renames a placement, as `rnpc rename`: its live NPCs keep living under the new name. Null on
|
||||
success, or why not (no such placement, the name taken, not a name).
|
||||
|
||||
### `RunicNpc_RespawnPlacement(string id)` → `int`
|
||||
|
||||
API 3. Removes a placement's NPCs and spawns them again at once, as `rnpc respawn`. How many, or `-1` if
|
||||
there is no such placement.
|
||||
|
||||
### `RunicNpc_Routes()` → `JObject`
|
||||
|
||||
`{name: route, …}`.
|
||||
@@ -157,6 +183,7 @@ All are called on every plugin, and **none of them answers**: a return value is
|
||||
| `OnRunicNpcHealth(BasePlayer npc, string profile, float threshold)` | The first time its health falls to one of the profile's `healthThresholds`. |
|
||||
| `OnRunicNpcDied(BasePlayer npc, string profile, string owner, HitInfo info, Dictionary<ulong, float> contributors)` | When it dies. `contributors` maps each player's Steam id to the health that player took from it, the killing blow included, after Rust's protection. It never adds up to more than its health. |
|
||||
| `OnRunicNpcDespawned(BasePlayer npc, string owner)` | When it is removed without dying. Not raised as RunicNPC itself unloads. |
|
||||
| `OnRunicNpcPlacementChanged(string id, string change, string previous)` | API 3. Whenever a placement is set, removed or renamed, through the API or in game. `change` is `set`, `removed` or `renamed`; `previous` is the old name for a rename and null otherwise. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ decided with it. **Stage 1 measured 2026-09-30** (§9): six answers on both rigs
|
||||
**Stage 2's design answered 2026-09-30:** D232–D238 (§0), which reshape stage 2 (§9). **Stage 2 built and tested
|
||||
2026-09-30** on both rigs (§9); its API is [API.md](API.md). **Stage 3's design answered 2026-09-30:** D239–D242
|
||||
(§0). **Stage 3 built and tested 2026-09-30** on both rigs (§9); its in-game walk waits for a player. **Stage 4's
|
||||
design answered 2026-09-30** (D243–D252, §0), **built and walked 2026-09-30** on both rigs (§9). **Stage 4's
|
||||
design answered 2026-09-30:** D243–D252 (§0).
|
||||
|
||||
RunicNPC is Runic Gateway's own NPC plugin for Rust servers, in its own repository,
|
||||
@@ -730,6 +731,56 @@ The wire changes join protocol 13, which is still unreleased (D248).
|
||||
**Tested by** the usual trio: each repository's suites, both rigs through the site, and a browser walk of both new
|
||||
pages, placing from the map included. An event run places profile NPCs, waves advance on their deaths, and teardown removes them.
|
||||
|
||||
**Built (2026-09-30), five PRs and this one.** RunicNPC API 3 (`runicnpc-rust` `feat/stage-4-api3`), the bridge
|
||||
(`Rust-Plugins` `feat/runicnpc-stage4`), the sidecar and the egg (`Rust-Link` `feat/runicnpc-stage4`), the site
|
||||
(`Module-Rust` `feat/runicnpc-stage4`) and the installer (`installer` `feat/runicnpc-stage4`). The wire is
|
||||
[`PROTOCOL.md`](../rust-link/PROTOCOL.md) §19.12, and API 3 is in [API.md](API.md).
|
||||
|
||||
| Piece | What it does |
|
||||
|---|---|
|
||||
| RunicNPC API 3 (D249) | `RunicNpc_AddPlacement` names a placement as `rnpc place` does and grounds a map point (terrain and rock only); `RunicNpc_RenamePlacement`, `RunicNpc_RespawnPlacement`; the hook `OnRunicNpcPlacementChanged` on every set, remove and rename. `rnpc place` and `here` now share one function with the API, so both refuse with the same sentences. The harness gained an `api3` group; `rnt.run all` passes 157/157 on both rigs |
|
||||
| The bridge | `integrations.runicNpc`; `npc.profiles`, `npc.profiles.set`, `npc.placements`, `npc.placement` (add, set, remove, rename, respawn); the frames `npc.died`, `npc.health`, `npc.placement.changed`; `world.place` with a `profile`, reverted through RunicNPC; the killfeed's `attackerNpc` and `attackerProfile`; the tally's `npcProfileKills`; `rg.npc`. `overlay.toml` declares `runicnpc_api = 3` |
|
||||
| The sidecar | Four forwards: `GET`/`POST /npc/profiles`, `GET /npc/placements`, `POST /npc/placement` |
|
||||
| The site | **Admin → Rust NPC profiles** (the profile form, per server, shared or fleet, each server's push state and refusals, replaced profiles with Restore); **Admin → Rust NPC placements** (the live map: click to place, a pin per placement; edit, rename, respawn, remove); the push loop with adoption; the event picker (D243); the triggers `rust.npc.died` and `rust.npc.health`; per-profile kills, the profile leaderboard, the opened row (D252) and Player → Rust; the title category "Kills of an NPC profile". 481 server and 66 client tests |
|
||||
| Shipping (D224) | The installer's compose job carries the latest RunicNPC release whose API is at least the bridge's `runicnpc_api` (none when the bridge needs none, RunicNPC has not released, or its API is too old). The installer places `RunicNPC.cs` before the bridge, records it, restores it on `update`, reports it in `doctor` and removes it on `uninstall`, never its data directory. The egg installs it. RunicNPC's release asks the installer to recompose |
|
||||
|
||||
**Walked, 2026-09-30, on both rigs through a walk site, in the browser as a signed-in admin.** There was no player
|
||||
on the rigs, so a throwaway probe plugin killed NPCs as a stand-in player, as stages 1 to 3's harness did.
|
||||
|
||||
| Row | Oxide | Carbon |
|
||||
|---|---|---|
|
||||
| A standalone server's own profiles adopted on the first push (`bandit`); one whose name a site profile had (`warden`) kept as replaced, and Restore refused with the reason while the site's covers it (D244, D251). Its placement kept both NPCs through it | pass | (managed already; nothing to adopt, pushed 0 then 1) |
|
||||
| A kit the server lacks refused on save, naming the server | — | pass |
|
||||
| A profile edited in the browser, pushed within the tick | pass | "push now" |
|
||||
| Placed from the live map: grounded (8 m up), named `warden-1` by the server, cost warning shown (D245, D246). A click on the sea refused with RunicNPC's sentence | pass | pass (API) |
|
||||
| Rename, edit, respawn and remove (with its confirm) from the placements page | pass | — |
|
||||
| A rename made at the server console reaching the site as `npc.placement.changed` | pass | pass |
|
||||
| Kills: `npc.died` (name, placement, killer, contributors), `npc.health` at 0.5, the tally's `npcProfileKills`, credited to the site profile pushed under that name (D247) | pass | pass |
|
||||
| "Rank by" a profile's kills; opening a row shows the player's kills by profile (D250, D252) | pass | — |
|
||||
| A title rule on a profile's kills ("Warden Slayer"), and one on a profile not on the server refused | pass | — |
|
||||
| The step editor's picker: the site's profiles, then Rust's own, grouped (D243) | pass | — |
|
||||
| An event placing two profile NPCs; its phase waited on `rust.npc.died` where `profile` is `warden` and `byEvent` is true, ignored a placement's warden dying, and advanced on the event's two; a second run cancelled removed its two live NPCs | pass | pass (place, teardown) |
|
||||
|
||||
**Not walked:** the killfeed's `attackerNpc`, which needs an NPC to kill a player, and a player's own kills on
|
||||
Player → Rust, which needs a linked account. Both wait for the in-game walk with a player. The installer's and the
|
||||
egg's RunicNPC path runs only against real releases, so it is walked at the cutover. The compose job and the egg
|
||||
were run against a mock Gitea for the three cases (a RunicNPC answering API 3, one answering API 2, none released).
|
||||
|
||||
What building it found:
|
||||
|
||||
- **A condition on a phase gate cannot name the run.** Core counts a gate's firings from phase entry and does not
|
||||
know which run an NPC belongs to, so `rust.npc.died` carries `byEvent` and `runId` for the gate's `where`. A
|
||||
gate that must count only its own run's NPCs uses a profile only events place, or `byEvent`. Noted for stage 6's
|
||||
bosses.
|
||||
- **The web feed lowercased an NPC's name** (it read a prefab name, `attacker()` in `format.js`), so "Old Warden 2"
|
||||
would have read "Old warden". The bridge sends the name in a field of its own, `attackerNpc`, and the feed shows it
|
||||
as typed.
|
||||
- **A site profile is labelled by its first NPC name** ("Warden"), where the leaderboard and titles need a word a
|
||||
player reads; `warden` stays the name events and `/rnpc` use. No new field.
|
||||
- **Rig notes, not code:** the walk site's `rust-oxide` row held the token of the server before it was recreated,
|
||||
and its ingest cursor pointed past the new sidecar's ids (both reset). After swapping a running sidecar binary
|
||||
on Carbon, a panel **restart** did not relaunch it; a stop and a start did.
|
||||
|
||||
### Stage 5 — Behaviour
|
||||
|
||||
Guard, patrol and escort roles; factions (between profiles, with Rust's scientists, and with teams or clans); group
|
||||
|
||||
Reference in New Issue
Block a user