docs(runicnpc): stage 9 decisions D306–D314 and its design #324

Merged
whitlocktech merged 1 commits from docs/runicnpc-stage9-decisions into main 2026-10-06 15:21:41 +00:00
2 changed files with 85 additions and 3 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
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
[`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md) (D214–D242), and the rest of this plan waits for it.**
[`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md) (D214–D314), 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
document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D188**.

View File

@@ -11,7 +11,8 @@ design answered 2026-09-30 and 2026-10-01** (D253–D266, §0), **its spike meas
and **its build's own questions answered 2026-10-01** (D267–D272). **Stage 5 built and tested 2026-10-01, walked on the
site 2026-10-05** on both rigs (§9); its API is [API.md](API.md) version 4. **Stage 6's design answered
2026-10-05** (D273–D282, §0), **its spike measured 2026-10-05** on both rigs, and **its two questions answered the
same day** (D283, D284, §9). **Its build's own questions were answered the same day too** (D285–D289), and **stage 6 was built and walked on the site 2026-10-05** on both rigs (§9); its API is [API.md](API.md) version 5. **Stage 7's design answered 2026-10-05** (D290–D299, §0).
same day** (D283, D284, §9). **Its build's own questions were answered the same day too** (D285–D289), and **stage 6 was built and walked on the site 2026-10-05** on both rigs (§9); its API is [API.md](API.md) version 5. **Stage 7's design answered 2026-10-05** (D290–D299, §0). **Stage 9's design answered 2026-10-06**
(D306–D314, §0 and §9).
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
@@ -63,7 +64,7 @@ architectural or design decision is implemented.
| **D240** | **A route is recorded point by point:** `/rnpc path record <name>` starts it, `/rnpc path point` adds where the admin stands (checked against the navmesh; a bad point is refused with the reason), `/rnpc path undo` drops the last, `/rnpc path save [loop\|back]` writes it, `/rnpc path cancel` discards it. Nothing is drawn on screen (D216). | The same flow with the points drawn on screen; a point dropped every few metres as the admin walks. |
| **D241** | **A placement made in game is named after its profile and a number** (`/rnpc place bandit` answers "Placed bandit-3"), and `/rnpc rename <id> <new>` renames it. `/rnpc near` lists the names. | Bare numbers; the admin naming every placement. |
| **D242** | **`/rnpc place` and `/rnpc here` take `key=value` options in any order after the profile:** `count=`, `respawn=`, `mode=each\|group`, `move=wander\|monument\|route:<name>`, `radius=`. Anything left out takes the profile's default. | `/rnpc place bandit 3 300 group route:gate`; `/rnpc place` then `/rnpc set`. |
| **D243** | **An event's "Place NPCs" picker shows both, grouped:** the site's profiles first, then Rust's own scientists as today. Existing events keep working. A server without RunicNPC offers only Rust's own until stage 9 makes RunicNPC required (stage 4). | Profiles only, flagging old steps to be re-picked; profiles only, with old steps still running silently. |
| **D243** | **An event's "Place NPCs" picker shows both, grouped:** the site's profiles first, then Rust's own scientists as today. Existing events keep working. A server without RunicNPC offers only Rust's own until stage 9 makes RunicNPC required (stage 4; D310 ends that fallback). | Profiles only, flagging old steps to be re-picked; profiles only, with old steps still running silently. |
| **D244** | **A server's own profiles are adopted by the site on its first push.** Before it pushes to a server for the first time, the site reads that server's standalone profiles (`rnpc.profile`) and imports each one as a profile for that server alone, so nothing on the server changes and its placements keep spawning. This refines D221's "replace" for the first push, as the permission manager's adopt does (stage 4). | Replacing them, leaving their placements "profile missing" (D237); listing them for the admin to adopt or discard one by one, holding that server's push until each is decided. |
| **D245** | **On the site, an admin lists, edits and creates placements, a new one by clicking the live map.** The server puts the clicked point on the ground at that spot and checks it against the navmesh, refusing with the reason as in game. A roof or a building top cannot be chosen from the map; that stays an in-game placement (stage 4). | List and edit only; creating at a monument; a read-only list. |
| **D246** | **The map's placement form has `/rnpc place`'s options** (D242): profile, count, respawn, each or group, movement and radius. The server names the placement, as in game (D241) (stage 4). | The profile only, everything else edited afterwards. |
@@ -126,6 +127,15 @@ architectural or design decision is implemented.
| **D303** | **An NPC's marker says its name and its event; a boss is a bigger marker.** No health on the map (stage 8). | A boss's health on its marker; unchanged "Event NPC" dots. |
| **D304** | **While an event runs, its page has a line per Place NPCs step, and a boss as its health:** "Bandits: 3 of 8 left", "The Juggernaut: 62%". A boss's adds are not counted (stage 8). | One total with the adds; bosses only. |
| **D305** | **Core may change as needed, as long as core stays game-agnostic and the UO integration does not break** (stage 8, the org lead, on how the line reaches core's event page and the app). | — |
| **D306** | **The performance check passes when 100 NPCs cost no more than stages 1 and 5 measured**, on both rigs and the 6000 map: about 1 ms of median frame for 100 idle, and about 5 ms for 100 fighting, over the same session's empty baseline. A later release that costs more fails it (stage 9). | A fixed fps floor (30 fps); record the numbers only. |
| **D307** | **The staging drill has its own rig, `rust-staging`: Oxide only, a 2000 map at about 6 GB, running only during a drill.** The panel's Rust image updates to the public branch and installs public Oxide on every start, so this rig keeps a permanent startup of its own: the image's vanilla mode, then a script that updates to `-beta staging`, unpacks Oxide's staging build and starts Rust. The published egg is unchanged. It checks what breaks, not what it costs, so the small map is enough (stage 9; the org lead first chose to move the Oxide rig, then a separate rig once the image's update was found). | Moving the Oxide rig to staging and back; a third 6000-map rig; reusing `egg-oxide`; giving the egg branch variables. |
| **D308** | **The drill is a Gitea CI job in `runicnpc-rust`.** Every day it reads Rust's staging build from Steam and stops if that build was already checked. A new build gets **both halves, automatically**: a static half (staging's managed DLLs only, RunicNPC compiled against them, the swap's field list regenerated and compared) and a live half (`rust-staging` started, the harness run, the rig stopped). Anything that fails or differs opens an issue (stage 9; the org lead asked for CI after the first options). | Static daily and live on a button; static only; the drill by hand. |
| **D309** | **The job drives the panel with its own Pterodactyl client key, created for it and kept in a Gitea org secret.** When both 6000-map rigs are running, memory has no room, so it stops the Carbon rig and starts it again afterwards; it leaves every rig as it found it (stage 9). | The existing client key; no panel access from CI. |
| **D310** | **A server without RunicNPC refuses every Place NPCs step, Rust's own scientists included**, with "this server needs RunicNPC", and Admin → Rust and `doctor` show it as incomplete. This ends D243's "Rust's own only" fallback. With RunicNPC loaded, the picker still offers both (stage 9). | Rust's own still placing without RunicNPC, only profile steps refused. |
| **D311** | **v1.0.0 is published on Gitea only.** uMod and Codefling are decided later, each against its own rules, once 1.0 has run on real servers (stage 9; narrows D224). | Gitea and uMod; Gitea, uMod and Codefling. |
| **D312** | **The player walk is one checklist in one session per rig.** Every in-game check deferred so far is gathered with stage 9's own. I prepare the rigs and the walk site, the org lead plays, and I watch the logs and record the results (stage 9). | A checklist per stage; release without a player walk. |
| **D313** | **v1.0.0 is one coordinated cutover.** After the walk passes, `edge` goes to `main` in all five Rust repos: RunicNPC v1.0.0 first, then the bridge, the sidecar, the installer and the bundle that pins them, and the module's requirement last (stage 9). | RunicNPC alone first, the rest later. |
| **D314** | **Core's public phase label is fixed in stage 9**, in its own website PR. `eventPublic.phaseLabel` matches a phase by `key`, which specs use, and still accepts `id`; its test fixture moves to `key`. It has said "Under way" for every phase since Events Phase 14a, UO's events included (stage 9, from docs#323). | A website issue for later. |
**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
@@ -1563,6 +1573,8 @@ moved there from 1.10.0, past 1.11.0, which it had skipped.
### Stage 9 — Hardening and release
As first planned:
- **Performance to the budget stage 1 measured:** 100 NPCs on a 6000 map without the frame time crossing it.
- **An update drill:** a rig on Rust's `staging` branch loads the current RunicNPC before each forced wipe. The
subclass swap and the brain are what break when Rust changes, so they are kept small and listed in one place.
@@ -1576,6 +1588,76 @@ moved there from 1.10.0, past 1.11.0, which it had skipped.
- the module refuses NPC steps on a server without it, saying why;
- the bridge reports it in hello.
**Its design, answered 2026-10-06 (D306–D314).** Four phases follow this one. Each ends in its PRs, and the org lead
is asked before the next one starts.
**9b. Hardening (the build).**
- **Core's phase label (D314):** a website PR of its own. `eventPublic.phaseLabel` finds the phase by `key`, then
by `id`, and the fixture uses `key` as real specs do. UO's events are tested in the same suite.
- **RunicNPC is required (D310):**
- `module-rust`: on a server without RunicNPC, or with one older than the bridge needs, a Place NPCs step fails
before it is sent, with "this server needs RunicNPC" (the existing `runicnpc-missing` and `runicnpc-old`
reasons), Rust's own scientists included. The picker shows the same reason on such a server instead of a
list. Admin → Rust shows the server as incomplete.
- The bridge refuses an NPC placement the same way when RunicNPC is not loaded, so a module that missed the
hello cannot get one through. The reason already exists, so no protocol change is expected; if one turns out to
be needed, it is asked first.
- The installer: the bundle's RunicNPC entry stops being optional. `install` and `update` always place it, and
`doctor` fails without it. The egg follows the bundle as it does today.
- **Operator documentation:** `docs/runicnpc/INSTALL.md` (with the installer and egg, by hand without Runic
Gateway, Kits and ZoneManager as "report, don't install", what the cost warning means) and
`docs/runicnpc/COMMANDS.md` (every `/rnpc` verb and console command, with its permission). `module-rust`'s
[OPERATING.md](../modules/rust/OPERATING.md) lists RunicNPC as required, next to Kits.
- **The update surface in one place:** every Rust type and member RunicNPC depends on for the swap and the brain is
listed in the repository's README, with where it is used. The static drill checks exactly that list.
- **The performance check (D306):** 100 idle and 100 fighting, on both 6000-map rigs, one at a time, with the
harness of stages 1 and 5. It passes at about +1 ms and +5 ms of median frame over that session's empty baseline.
The numbers go into INSTALL and the cost warning.
**9c. The staging drill (D307–D309).**
- **The rig:** a new Pterodactyl server, `rust-staging`, Oxide only, on a 2000 map at about 6 GB. It keeps a
permanent startup: the image's vanilla mode, then a script that runs `app_update 258550 -beta staging`, unpacks
Oxide's staging build and starts Rust. Nothing in the published egg changes. It is stopped except during a drill.
- **The key:** a Pterodactyl client key made for the job alone, in a Gitea org secret. It is never printed.
- **The job** (`.gitea/workflows/staging-drill.yml` in `runicnpc-rust`), daily:
1. Read Rust's staging build id from Steam. A `drill` branch holds the last build checked and its result, like
the installer's `bundles` branch. Same build: stop.
2. **Static half:** download only `RustDedicated_Data/Managed` from the staging branch, with no server; compile
`plugin/RunicNPC.cs` against it and Oxide's staging build; regenerate the swap's field list with
`tools/fieldlist` and compare it with the one the plugin carries.
3. **Live half:** if both 6000-map rigs are running, stop the Carbon rig. Start `rust-staging`, wait for the
navmesh, load the current RunicNPC and the test harness, run it, and stop the rig. Start Carbon again if the job
stopped it.
4. Anything that fails or differs opens one issue in `runicnpc-rust` for that build, labelled `staging-drill`,
with the compiler output, the field-list diff or the failed harness checks. The result is written to `drill`.
- **Proven in the build first:** that the runner reaches the panel at 192.168.0.12, and that Oxide's staging build
installs over a staging server this way. If either does not hold, the org lead is asked before going round it.
**9d. The player walk (D312).** Both rigs are updated first (Rust, Oxide, Carbon) and their builds stated. One
checklist, published as a page, for one session per rig. I prepare the rigs, the walk site and a linked test
account; the org lead plays, and I watch the logs and record each row. It gathers every in-game check deferred so
far:
- Stage 1: the death screen's killer name and portrait with the `AttackerInfo` override.
- Stage 3: the eight-step `/rnpc` checklist above.
- Stage 4: the killfeed's `attackerNpc` when an NPC kills a player, and a player's NPC kills on Player → Rust.
- Stages 5 and 6: escort and a faction tied to a clan; the boss bar and its redraws, chat lines and popups, a real E
press and its window, and the example profiles on a fresh server's first boot.
- Stage 7: what the client shows for a locked crate whose count is below zero.
- Stage 8: named markers and a boss's bigger marker on the live map, and the event page's lines, in the browser and
the app.
- Because the cutover releases them too, the Rust plan's deferred in-game rows: PLAN_REDESIGNS §3's zone flags and
messages and the dome's default stack, and §5's chat titles.
- Stage 9's own: an event on a server without RunicNPC refused with its reason, and the installer's RunicNPC path
from a real bundle.
**9e. The cutover and release (D311, D313).** Only after the walk passes. In order: `runicnpc-rust` `edge` → `main`
as v1.0.0 (its Gitea release is the only download); `rust-plugins`, `rust-link` and `installer` `edge` → `main`,
and the bundle that pins all four; `module-rust` `edge` → `main` with RunicNPC required. Each release is checked
(checksums, the bundle composing) before the next merge.
**Then the Rust plan resumes** at PLAN_REDESIGNS §9 item 6 (the step editor and the kit weekend) and §11.
### Stage 10 — Custom navigation (only if needed)