docs(runicnpc): stage 2's design, D232–D238 #301

Merged
whitlocktech merged 1 commits from docs/runicnpc-stage2-decisions into main 2026-09-30 04:41:07 +00:00
2 changed files with 71 additions and 14 deletions

View File

@@ -8,7 +8,7 @@ they can be read together. **Built so far:** §1, walked 2026-09-28 (§1.9); §5
rig walk still to come (§5.8, D209); §3, built and walked on the rigs and the site 2026-09-29, its in-game rig walk still to come (§5.8, D209); §3, built and walked on the rigs and the site 2026-09-29, its in-game
rows still to come (§3.4); §4, built 2026-09-29 (§4.1). **§6 was spiked on 2026-09-30, and the org lead rows still to come (§3.4); §4, built 2026-09-29 (§4.1). **§6 was spiked on 2026-09-30, and the org lead
chose neither route: Runic Gateway writes its own NPC plugin, RunicNPC, planned in chose neither route: Runic Gateway writes its own NPC plugin, RunicNPC, planned in
[`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md) (D214–D231), and the rest of this plan waits for it.** [`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md) (D214–D238), and the rest of this plan waits for it.**
This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this
document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D188**. document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D188**.

View File

@@ -3,6 +3,7 @@
**Status:** plan, written 2026-09-30. Its eight questions (§11) were answered the same day: D221–D228 (§0). **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 **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. 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).
RunicNPC is Runic Gateway's own NPC plugin for Rust servers, in its own repository, 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 [`RunicGateway/runicnpc-rust`](https://gitea.whitlocktech.com/RunicGateway/runicnpc-rust). It runs on Oxide and
@@ -43,6 +44,13 @@ architectural or design decision is implemented.
| **D229** | **The empty repository was seeded by one `chore:` commit straight to `main`** (the licence and a stub README), `edge` was branched from it, and everything after goes by PR (stage 0). It is the only direct push. | You seeding it in the Gitea UI; the whole scaffold straight to `main`, unreviewed. | | **D229** | **The empty repository was seeded by one `chore:` commit straight to `main`** (the licence and a stub README), `edge` was branched from it, and everything after goes by PR (stage 0). It is the only direct push. | You seeding it in the Gitea UI; the whole scaffold straight to `main`, unreviewed. |
| **D230** | **The layout is `plugin/RunicNPC.cs` and a root `plugin.toml`**: `api`, the framework floors, `requires_plugins` (stage 0). RunicNPC is one file, not an overlay of a server tree. | Mirroring Rust-Plugins' `overlay/oxide/plugins/` and `overlay.toml`. | | **D230** | **The layout is `plugin/RunicNPC.cs` and a root `plugin.toml`**: `api`, the framework floors, `requires_plugins` (stage 0). RunicNPC is one file, not an overlay of a server tree. | Mirroring Rust-Plugins' `overlay/oxide/plugins/` and `overlay.toml`. |
| **D231** | **Each profile (or group) decides whether its NPCs target other NPCs, and which kinds**: Rust's scientists, animals, other profiles, and so on (stage 1; the org lead's words). Rust's AI cannot do this (§9, stage 1, Q3), so it is our own combat state and sensing, and stage 5 opens with a spike for it. | Our NPCs fighting players only, as Rust's scientists do. | | **D231** | **Each profile (or group) decides whether its NPCs target other NPCs, and which kinds**: Rust's scientists, animals, other profiles, and so on (stage 1; the org lead's words). Rust's AI cannot do this (§9, stage 1, Q3), so it is our own combat state and sensing, and stage 5 opens with a spike for it. | Our NPCs fighting players only, as Rust's scientists do. |
| **D232** | **The swap copies a fixed list of fields**, generated from Rust's unmodified assembly by a `tools/` script and carried in the plugin, so Oxide and Carbon copy the same fields. A boot check on Carbon, where Rust's visibility is unmodified, names any field Rust has added since; the staging drill (stage 9) regenerates the list. | Stage 1's rule as it is (Oxide copies runtime state too); the rule with Oxide-only filters. |
| **D233** | **A roamer moves in one of three ways:** `wander` (our own: a walkable point within a radius of its spot, walk, pause, repeat), `monument` (Rust's own AI-zone paths), or `route:<name>` (points an admin records in game, walked in order). **The profile sets the default, `wander`, and each placement may override it** (the org lead: "should be both, and the ability for admins in game to make routes"). | Our wander only; Rust's paths in monuments and ours elsewhere, fixed; the mode fixed per profile. |
| **D234** | **Routes are walked from stage 2 and recorded from stage 3.** Stage 2 builds the follower and the routes file (the harness writes its points); stage 3 adds `/rnpc path record` and setting points in game; stage 5 adds fighting and resuming the route. | Recording in stage 3 and walking in stage 5, as first planned. |
| **D235** | **An NPC sleeps when no player is within 160 m** (Rust's own dormant distance; a profile may change it, 0 = never). **It walks back to its spot before it sleeps** (the org lead: "it should walk back home before going to sleep"), and wakes when a player comes within range. | Being put back at its spot; never sleeping unless a profile opts in. |
| **D236** | **Respawn is chosen per placement:** `each` (the default: every NPC returns its delay after its own death) or `group` (none return until all are dead, then all return together). | One fixed rule. |
| **D237** | **A deleted profile leaves its placements waiting.** Their NPCs despawn; the placements are kept and shown as "profile missing" in game and on the site; they spawn again if the profile returns. | Refusing the delete; deleting the placements with it. |
| **D238** | **A profile's shape** is §2's, as the org lead approved it: `names`, `kits`, `prefab`, `role`, `movement`, `health`, `damageDealt`, `damageTaken`, `aimCone`, `ranges`, `visionCone`, `sleepDistance`, `healthThresholds`, in `data/RunicNPC/profiles.json` with the `managed` flag. Placements and routes have their own files beside it; the optional caps (D227) are in the plugin's config. Stages 5–7 add their own sections. | A smaller stage-2 profile, combat values deferred to stage 5. |
**Borrowing, not copying.** NpcSpawn states no licence at all, so its source grants us nothing and is read only as a **Borrowing, not copying.** NpcSpawn states no licence at all, so its source grants us nothing and is read only as a
description of *what* can be done in Rust. HumanNPC is MIT on uMod, which is GPL-compatible, but §1.2 rules out its description of *what* can be done in Rust. HumanNPC is MIT on uMod, which is GPL-compatible, but §1.2 rules out its
@@ -161,6 +169,39 @@ Placements stay the server's either way (D222): the site reads them and edits th
The kit list is required and non-empty; one kit is picked at random per spawn, for variety within a profile. A kit The kit list is required and non-empty; one kit is picked at random per spawn, for variety within a profile. A kit
the server does not have refuses the profile when it is saved, not when an NPC spawns. the server does not have refuses the profile when it is saved, not when an NPC spawns.
**In the data file (D238)**, `data/RunicNPC/profiles.json`:
```json
{
"managed": false,
"profiles": {
"warden": {
"names": ["Warden", "Old Warden"],
"kits": ["warden_rifle", "warden_smg"],
"prefab": "scientistnpc_roam",
"role": "roamer",
"movement": { "mode": "wander", "radius": 20 },
"health": 250,
"damageDealt": 1.0,
"damageTaken": { "head": 1.0, "body": 1.0, "legs": 1.0 },
"aimCone": 2.0,
"ranges": { "sense": 30, "loseTarget": 40, "chase": 40, "attack": 30 },
"visionCone": -0.8,
"sleepDistance": 160,
"healthThresholds": [0.5]
}
}
}
```
- `role` is `roamer` or `sentry` in stage 2; the other roles of §6 arrive with their stages.
- `movement.mode` is `wander`, `monument` or `route:<name>` (D233). A placement may override it.
- `damageDealt` scales the kit weapon's damage. `damageTaken` scales what the NPC takes, by body part.
- `sleepDistance` 0 means it never sleeps (D235).
- `healthThresholds` are the fractions at which `OnRunicNpcHealth` fires (§4).
**A deleted profile leaves its placements waiting (D237):** their NPCs despawn, and they spawn again if it returns.
--- ---
## 3. Features ## 3. Features
@@ -191,7 +232,7 @@ Borrowed ideas are marked with their source; everything else is new. Stage numbe
|---|---| |---|---|
| Press E to talk: an NPC says a line, or opens a message, when used | 6 | | Press E to talk: an NPC says a line, or opens a message, when used | 6 |
| Lines on greet, hurt and kill | 6 | | Lines on greet, hurt and kill | 6 |
| Follow a player, and walk a recorded path | 5 | | Walk a recorded path (D234), and follow a player | 2, 3, 5 |
### 3.3 Runic Gateway's own ### 3.3 Runic Gateway's own
@@ -274,10 +315,10 @@ Each profile has a **role**, which sets its AI states and defaults:
| Role | Behaviour | | Role | Behaviour |
|---|---| |---|---|
| **Roamer** | Wanders within its roam range of home, chases and fights. Rust's default. | | **Roamer** | Moves in one of three ways (D233): `wander` within a radius of its spot, `monument` (Rust's AI-zone paths), or `route:<name>`; chases and fights. The profile sets the default, a placement may override it. |
| **Sentry** | Stationary: turns and shoots, never walks. The only role allowed off the navmesh. | | **Sentry** | Stationary: turns and shoots, never walks. The only role allowed off the navmesh. |
| **Guard** | Holds a point or an entity; chases only to its leash, then returns. | | **Guard** | Holds a point or an entity; chases only to its leash, then returns. |
| **Patrol** | Walks a path recorded in game (`/rnpc path record`); fights and resumes. | | **Patrol** | A roamer on `route:<name>` (D233): walks a route recorded in game (`/rnpc path record`); fights and resumes (stage 5). |
| **Escort** | Follows an entity or player; defends it. | | **Escort** | Follows an entity or player; defends it. |
| **Boss** | Any of the above, plus a health bar, phases and announcements (§3.3). | | **Boss** | Any of the above, plus a health bar, phases and announcements (§3.3). |
| **Passive** | Never fights; press E to talk. Quest-givers and vendors. | | **Passive** | Never fights; press E to talk. Quest-givers and vendors. |
@@ -488,22 +529,38 @@ What the numbers say:
### Stage 2 — The NPC and its API ### Stage 2 — The NPC and its API
- The subclass, profiles (read from RunicNPC's own data file for now), kits (random pick), appearance, combat values, - **The NPC:** the subclass with a fixed field list (D232) and `AwakeFromInstantiate` before `Spawn` (stage 1, Q1);
roamer and sentry roles, sleep. the `AttackerInfo` override, so its name reaches the death screen (stage 1, Q6); profiles read from RunicNPC's own
- Owners and lifetimes (§2); placements persisted with one dirty flag; standalone profiles and the managed flag data file in §2's shape (D238); a random kit and name per spawn; appearance and combat values, with the sense
(D221); optional caps, off by default, and the cost warning from stage 1's measurements (D227). values set before the brain starts (stage 1, Q3).
- The API (§4) and its hooks; `docs/runicnpc/API.md`. - **Roles and movement:** roamer and sentry. A roamer's three modes (D233): our own `wander`, Rust's `monument`
paths, and `route:<name>` from the routes file (D234; the harness writes its points, stage 3 records them in
game).
- **Sleep (D235):** past the profile's distance from every player, the NPC walks back to its spot, then stops
thinking; a player in range wakes it.
- **Navmesh at boot:** nothing spawns until `RustNavigation.Instance.IsDefaultNavmeshBuilt()` (stage 1, Q4); the
queue waits for it.
- **Owners and lifetimes (§2):** placements persisted with one dirty flag, each with its count, delay and respawn
mode (`each` or `group`, D236) and an optional movement override; a placement whose profile is gone waits (D237);
standalone profiles and the managed flag (D221).
- **Spawning in batches:** large placements and the boot respawn are spread over frames, within a per-frame time
budget (stage 1, Q5: 100 at once is a half-second hitch).
- **Optional caps, off by default,** in the plugin's config, and **the cost warning** from stage 1's table (D227).
- **The API (§4) and its hooks;** `docs/runicnpc/API.md`.
**Tested by** the harness asserting each call's result (PASS/FAIL lines read back over the panel), plus restart **Tested by** the harness asserting each call's result (PASS/FAIL lines read back over the panel), plus restart
and reload checks on both rigs: no NPC saved, every placement back, nothing left behind by an unloaded owner. and reload checks on both rigs: no NPC saved, every placement back, nothing left behind by an unloaded owner. It
also checks each movement mode, sleep and the walk home, both respawn modes, and a placement whose profile is
deleted and restored.
### Stage 3 — In game ### Stage 3 — In game
The chat and console commands (§5), their permissions, the navmesh placement check, respawn delays, and `/rnpc The chat and console commands (§5), their permissions, the navmesh placement check, respawn delays, and recording
path record` for stage 5. routes in game (`/rnpc path record`, then setting points, D234). A placement may name a movement override and a
respawn mode (D233, D236).
**Tested by** a written in-game walk: place, remove, look-at info, respawn, restart. **This is the first stage that **Tested by** a written in-game walk: place, remove, look-at info, respawn, record a route and place a patrol on it,
needs a player on a rig.** restart. **This is the first stage that needs a player on a rig.**
### Stage 4 — Runic Gateway integration ### Stage 4 — Runic Gateway integration