docs(runicnpc): stage 2 as built, and API.md

runicnpc/API.md is the reference for other plugins: owners, every
RunicNpc_* call, the four hooks, and the profile, placement and route
shapes. PLAN.md records stage 2's results on both rigs and what building it
found: Rust's chase needs an AI zone like its roam, and a player-built floor
joins the navmesh 0.1-0.3 s after it spawns, which corrects stage 1's Q4
(left open for the org lead). README indexes API.md.

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 01:31:44 -05:00
parent 5c44ce437a
commit 1e00c5da6e
3 changed files with 280 additions and 4 deletions

View File

@@ -3,7 +3,8 @@
**Status:** plan, written 2026-09-30. Its eight questions (§11) were answered the same day: D221–D228 (§0).
**Stage 0 closed 2026-09-30** (runicnpc-rust#1/#2, §9): v0.1.0 released and loaded on both rigs. D229–D230 were
decided with it. **Stage 1 measured 2026-09-30** (§9): six answers on both rigs; D231 was decided with it.
**Stage 2's design answered 2026-09-30:** D232–D238 (§0), which reshape stage 2 (§9).
**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).
RunicNPC is Runic Gateway's own NPC plugin for Rust servers, in its own repository,
[`RunicGateway/runicnpc-rust`](https://gitea.whitlocktech.com/RunicGateway/runicnpc-rust). It runs on Oxide and
@@ -460,7 +461,7 @@ it. That is the other reason our NPC must never be saved (§2).
| Health, name, aim cone | Hold: set on the NPC and read back (400/400, the name, 0.5). |
| Sense range, target-lost range, vision cone, listen range, hostile-only, sense types | **Only if set before the brain starts.** `Senses.Init` copies them once. At 35 m, a range of 50 set before the start saw the target, and the same range set afterwards did not. |
| The prefab's own values | Sense 30 m, lose target 40 m, vision cone −0.8, listen 10 m, memory 10 s, line-of-sight checks on, hostile-only off, senses players only, health 150, aim cone 2. |
| Roaming | **Rust's roam only follows an AI zone's move points.** In a monument's zone (Desert Military Base) ours roamed 36–47 m in 60 s. In an open field it stood still (0 m). **A roamer anywhere but a monument needs our own roam state, so stage 2's roamer role includes one.** |
| Roaming | **Rust's roam only follows an AI zone's move points** (and so does its chase: stage 2). In a monument's zone (Desert Military Base) ours roamed 36–47 m in 60 s. In an open field it stood still (0 m). **A roamer anywhere but a monument needs our own roam state, so stage 2's roamer role includes one.** |
| Sleep | An NPC outside an AI zone is never put to sleep, and `ai_dormant` does not apply to this AI. So an idle NPC keeps thinking with nobody near, which is what the costs below measure. |
| **Fighting NPCs** | **Rust's scientist AI never attacks an NPC.** `HumanNPC.IsTarget` is true only for non-NPC players, pets and scarecrows. `IsFriendly` means "same prefab id", which the swap copies, so every stock scientist counts ours as a friend. Re-implementing Rust's internal `IAISenses` puts a scientist in our NPC's target list. But Rust's AI design never runs its attack event for an NPC target. Calling `AttackTick` directly passed line of sight every time and still fired no shot, because the design's cover and facing states win. **NPC-versus-NPC combat needs our own combat state, and sensing NPCs needs our own sensing** (see the cost below). D231 makes it a profile setting, so stage 5 opens with its own spike. |
@@ -475,7 +476,7 @@ boots load the saved `proceduralmap.<size>.<seed>.<n>.navmesh`. **Placements tha
| Monuments probed (9 points each) | 144 | 144 |
| Points on the navmesh | 1,152 / 1,296 (89%) | 1,151 / 1,296 (89%) |
| Of those on a structure (a roof or raised floor) | 132 / 209 (63%) | 130 / 211 (62%) |
| A player-built floor 4 m up | **not on the mesh** | **not on the mesh** |
| A player-built floor 4 m up | **not on the mesh** in the frame it spawned; **on it 0.1–0.3 s later** (stage 2) | the same |
| One check | 13.6 µs | 8.3 µs |
Monument roofs (Launch Site, Airfield, Trainyard, the warehouses) are mostly walkable. Player-built floors and
@@ -553,6 +554,46 @@ and reload checks on both rigs: no NPC saved, every placement back, nothing left
also checks each movement mode, sleep and the walk home, both respawn modes, and a placement whose profile is
deleted and restored.
**Built (2026-09-30, runicnpc-rust#4, API version 2).** The API as built is [API.md](API.md). What else landed:
- `tools/fieldlist` generates the swap's field list from the Carbon rig's assemblies (`tools/managed.js`
downloads them). It picks **64 NPC and 32 brain fields**, exactly stage 1's Carbon count. On both rigs every
name resolves, and Carbon's boot check finds no field Rust has added.
- `tools/RunicNpcTest.cs`, the harness: `rnt.run api|hooks|move|sentry|sleep|place|all`, then `rnt.after` after a
reload or restart.
| Group | What it proves | Oxide | Carbon |
|---|---|---|---|
| api | every call's answer; seven refusals (bad owner, `placement:` owner, unknown or refused profile, bad override, off the navmesh) | 24/24 | 24/24 |
| hooks | spawned, a health threshold once, body damage ×0.5, died with contributors (250 of 250), despawned | 8/8 | 8/8 |
| move | wander within its radius; a route in order (3>0>1>2>3>0); a monument roamer 36 m on Rust's roam; our chase to 9.6–9.8 m of a 10 m chase range, and giving up twice | 8/8 | 8/8 |
| sentry | holds 0.00 m for 40 s and hits a target 12 m off 29–34 times; stands 40 m up, off the navmesh, for 20 s | 7/7 | 7/7 |
| sleep | wanders 8.6 m off, walks home at ≤2.8 m/s when no player is within 160 m, sleeps 1.7 m from its spot, stays still, wakes | 7/7 | 7/7 |
| place | `each` returns only the dead one; `group` waits for all, then all return; a deleted profile waits and returns; a missing route waits | 14/14 | 14/14 |
| a plugin unloads | its NPCs are removed with it | pass | pass |
| RunicNPC reloads | every placement back, the world equals the registry, no plain scientists at the spots | 5/5 | 5/5 |
| the server restarts | the same, after `server.save`, a restart, and the navmesh load | 5/5 | 5/5 |
What building it found:
- **Outside a monument, Rust's scientists never chase.** Rust's chase state looks for an AI zone's move points and
returns an error without them, as its roam does (stage 1, Q3). A `wander` or `route` roamer therefore has our
own chase. It closes to three quarters of the attack range, never past the profile's chase range from home, and
if the target stays out of reach at that edge for 5 s it gives up and walks home. A `monument` roamer keeps
Rust's chase.
- **Rust's navmesh covers a new player-built floor in 0.1–0.3 s** (2–3 frames), on both rigs. Stage 1's "a
player-built floor is never on the mesh" (Q4) sampled in the same frame the floor spawned, so it is wrong: a
roamer can stand on a player-built floor a moment after it exists. §5's sentry rule, §8 and stage 10 were
written on stage 1's reading. **Open for the org lead.**
- **Rust's navmesh sampler reaches further down than across.** A roamer asked to stand on a roof found the
ground 4.7 m below. A roamer's spot must now be within 2 m, up or down, of the navmesh it is put on.
- **A brain can think before Unity has started it.** For that moment it has no navigator, so RunicNPC leaves that
think to Rust.
- **A hit with no body part reports every part at once** (`(HitArea)(-1)`: fire, explosions, falls). Only an
exact head or leg hit uses those scales; everything else is body.
- **The game manifest's entity list leaves out the NPC prefabs.** A profile's `prefab` is looked up in its prefab
list instead.
### Stage 3 — In game
The chat and console commands (§5), their permissions, the navmesh placement check, respawn delays, and recording