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:
2026-09-30 08:45:21 -05:00
parent c6ba46d692
commit 2ad2043060
4 changed files with 208 additions and 10 deletions

View File

@@ -1437,7 +1437,7 @@ five routes.
|---|---|---|
| `world.monuments` | `world.monuments` | This map's monuments in one stable order (grouped by prefab short name, then by position). Each has `value` (`kind`, or `kind#n` when the kind repeats), `kind`, `instance`, `of`, `label` (the game's display phrase), `x`, `z` and `grid`. Also `worldSize`, the placeable `prefabs` (`key`, `kind`, `label`), `eventsEnabled`, `maxCrates`, `maxNpcs` and `zoneManager` |
| `world.zone` | `world.ok` or `world.error` | Opens a ZoneManager temporary zone owned by the bridge. Needs `runId`, `key`, a location, `radius` (5–150 m) and `holdMs` (1 minute to 7 days); `name` is optional. Protocol 13 adds `flags`, `settings`, `enterMessage`, `leaveMessage`, `delivery`, `format` and `dome` (§19.11) |
| `world.place` | `world.ok` or `world.error` | Places `count` of one allowlisted `prefab` at a location, scattered within `spread` m (0–50, 10 by default for a group). All or nothing: if the game refuses one, the ones already made are killed |
| `world.place` | `world.ok` or `world.error` | Places `count` of one allowlisted `prefab` at a location, scattered within `spread` m (0–50, 10 by default for a group). All or nothing: if the game refuses one, the ones already made are killed. Protocol 13 adds `profile`, a RunicNPC profile in place of a prefab (§19.12) |
| `world.revert` | `world.ok` | Gives back what a run owns: the named `ids`, or else everything under `key`, or else everything the run owns. The answer lists `removed`, `gone` and `refused` |
| `world.owned` | `world.owned` | What the world still holds of what events made, **looked for** by net id or zone id, narrowed by `runId`. Anything gone is pruned from the registry as the walk passes it |
@@ -2383,3 +2383,118 @@ last hello when it is saved and in a dry run; `bad-option` and `dome-unavailable
ZoneManager and ZoneDomes reloads, a server restart and the forced opposite boot order (exactly one set of
spheres each time), revert and expiry removing the dome, and a dome refused without the helper. The
in-game rows (flags on a player, the messages) wait for the later in-game walk.
### 19.12 RunicNPC: the site's profiles and placements (runicnpc PLAN.md stage 4)
RunicNPC is Runic Gateway's own NPC plugin ([`../runicnpc/PLAN.md`](../runicnpc/PLAN.md), [`API.md`](../runicnpc/API.md)).
**The bridge is its only link to the site**: it calls RunicNPC's API for the site's commands and turns
RunicNPC's hooks into frames, and RunicNPC never talks to the sidecar, so "the sidecar is a dumb forwarder"
stays true. RunicNPC is optional until its stage 9. Without it every `npc.*` command answers `npc.error`
**`runicnpc-missing`**, and an event places Rust's own scientists only (D243).
The bridge needs **RunicNPC API 3** (D249), `RunicNpcApiNeeded` in the code and **`runicnpc_api = 3` in
`overlay.toml`**, which the release copies into its manifest and `checkPlugin.js` holds equal to the code.
An older RunicNPC answers `npc.error` **`runicnpc-old`**.
**Hello and status.** `integrations` gains `runicNpc`, like the other optional mods:
```json
"runicNpc": { "loaded": true, "version": "0.2.0", "api": 3 }
```
`api` is present only while RunicNPC is loaded. **The bridge sends hello again when RunicNPC loads or
unloads.**
**Four commands.** Each answers a reply of its own kind, or `npc.error` with a `reason` and RunicNPC's own
sentence in `message`.
| Command | Answers | |
|---|---|---|
| `npc.profiles` | `npc.profiles` | `managed`, `profiles` (name → profile, D238) and `refused` (name → why). The site reads it before its **first** push to a server, to adopt the server's own profiles (D244) |
| `npc.profiles.set` | `npc.ok` | `{"profiles": {name: profile, …}}` replaces the whole set, as a push does, and marks the server managed (D221). The answer's `refused` names each profile RunicNPC would not use, and why (a kit the server lacks) |
| `npc.placements` | `npc.placements` | Every placement: `id`, `placement` (its values), `alive`, `waiting` (D237), `note` (D239), `lastError`. Also `routes`, the names a placement may walk, and `cost`, the cost warning for what the server plans now (D227) |
| `npc.placement` | `npc.ok` | One change to one placement, by **`op`**, below |
`npc.placement`'s ops:
| `op` | Carries | Answers |
|---|---|---|
| `add` | `placement`: `profile`, `position` **x and z only**, and `/rnpc place`'s options (D246): `count`, `respawn`, `respawnMode`, `movement` | `id` (named by RunicNPC as in game, `<profile>-<n>`, D241), `position` (where it landed), `built`, `cost` |
| `set` | `id`, `placement` (a whole x, y, z moves it) | `id`, `cost` |
| `remove` | `id` | `id` |
| `rename` | `id`, `to` | `id` (the new), `previous` |
| `respawn` | `id` | `id`, `respawned` |
**A map point is put on the ground by RunicNPC** (`RunicNpc_AddPlacement`, D245): a ray down on terrain
and rock, never a building or a tree, so a roof is placed in game. Then it is checked against the navmesh
exactly as `/rnpc place` is, and a refusal is RunicNPC's own sentence: off the map, under water, off the
navmesh (`A roamer cannot stand there: …`), no such profile or route.
| `reason` | Means |
|---|---|
| `runicnpc-missing` | RunicNPC is not loaded |
| `runicnpc-old` | RunicNPC answers an API older than 3 |
| `malformed` | a field is missing or `op` is unknown |
| `not-found` | no placement of that id |
| `refused` | RunicNPC would not; `message` says why |
**Three events,** from RunicNPC's hooks, all **staff** class on the site:
```json
{"kind":"npc.died","type":"event","netId":"973440","profile":"bandit","name":"Road Bandit",
"owner":"placement:bandit-1","placement":"bandit-1","x":-2123.7,"z":2282.7,
"killerId":"76561198000000002","killerName":"Marisol","weapon":"rifle.ak.entity",
"contributors":[{"steamId":"76561198000000002","name":"Marisol","damage":180}]}
{"kind":"npc.health","type":"event","netId":"973440","profile":"bandit","name":"Road Bandit",
"threshold":0.5,"health":78.3,"maxHealth":180,"x":-2123.7,"z":2282.7}
{"kind":"npc.placement.changed","type":"event","id":"gate","change":"renamed","previous":"warden-1"}
```
- `owner` is RunicNPC's: `run:<id>`, `placement:<id>` or `plugin:<name>`. An event's NPC also carries
**`runId`**, a placement's **`placement`**. `killerId` and `killerName` are absent when no player landed
the killing blow. `contributors` is every player who took health from it, most first.
- `npc.health` is sent the first time an NPC falls to one of its profile's `healthThresholds`.
- `npc.placement.changed` is sent on every set, remove and rename, from the site or in game (`change` is
`set`, `removed` or `renamed`), so an edit made in game reaches the site at once. The site reads the list
again rather than trusting a diff.
**`world.place` takes a `profile`** instead of a `prefab` (D243): the NPCs are RunicNPC's, owned by the run
(`run:<runId>`), and recorded in the registry with `prefab` `runicnpc:<profile>`. Everything else is as
§15.1 says: the same bounds (`EventsMaxNpcs`), all or nothing, a repeated key answered with the first
call's ids. A scattered point RunicNPC refuses (off the navmesh) is tried again three times, then at the
centre. `world.revert` removes such an NPC **through RunicNPC** (`RunicNpc_Despawn`), which raises its own
despawn hook. Two new `world.error` reasons, both permanent: **`runicnpc-missing`** and
**`unknown-profile`** (the server has no such profile, or RunicNPC refuses it). A placement that names both a
prefab and a profile is `malformed`.
**The killfeed names RunicNPC's NPCs** (runicnpc PLAN.md §1.4). A `player.death` whose attacker is one of
them gains **`attackerNpc`** (its own name, as the victim's death screen shows it) and
**`attackerProfile`**, **beside** `attackerName`, which stays the prefab so a reader that knows only prefab
names still reads a scientist.
**`player.tally` gains `npcProfileKills`**, the player's kills of RunicNPC's NPCs by profile name
(`{"warden": 2}`). They are counted in `npcKills` as well.
**Hooks.** `OnRunicNpcDied`, `OnRunicNpcHealth` and `OnRunicNpcPlacementChanged` join `rg.hooks`' list.
`rg.npc` prints RunicNPC's version and API and the last `npc.*` command.
**The sidecar** forwards four routes, each a correlated round trip that fails while the game is down:
| Route | Command |
|---|---|
| `GET /npc/profiles` | `npc.profiles` |
| `POST /npc/profiles` | `npc.profiles.set` (the body's `cmd` and `reqId` written over, as every forward) |
| `GET /npc/placements` | `npc.placements` |
| `POST /npc/placement` | `npc.placement` |
The three events are filed and served like every other (§8.1).
**The website** (Module-Rust) authors the profiles (Admin → Rust NPC profiles) and pushes each server its
set on a loop of its own. Before the first push to a server it reads `npc.profiles`, and a standalone
server's profiles are adopted as profiles for that server alone; one whose name a site profile already has
there is kept aside as "replaced" (D244, D251). It lists and edits placements live (Admin → Rust NPC
placements), a new one by clicking the live map (D245, D246). It adds the triggers `rust.npc.died` and
`rust.npc.health`, which a phase can wait on, and stores `npcProfileKills` per profile for the leaderboard
and titles (D247, D250, D252).
**Walked on both rigs, 2026-09-30** (runicnpc PLAN.md stage 4).