The PLAN_REDESIGNS §6 spike ran on the Oxide rig on 2026-09-30. Route A kept its kit and fought with it but cannot be named; HumanNPC counted its NPCs as players in the killfeed and tallies; NpcSpawn worked but costs a boot hitch, a phone-home and an unbundleable dependency. The org lead chose to write our own API-style plugin in runicnpc-rust. runicnpc/PLAN.md records the spike, the decisions, the features, the API, the chat commands, and stages 0-10 with how each is tested, plus eight questions. PLAN_REDESIGNS §6, §9 and §10 point at it; the index lists it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
489 lines
28 KiB
Markdown
489 lines
28 KiB
Markdown
# RunicNPC — the plan
|
||
|
||
**Status:** plan, written 2026-09-30. No code yet. **Its questions (§11) are open.**
|
||
|
||
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
|
||
Carbon. Other plugins drive it through an API, as NpcSpawn is used, and admins use it directly in game through chat
|
||
commands. **It is a prerequisite of the rest of the Rust plan:** the redesigns resume only after its stage 9
|
||
(§9), and from then on `module-rust` requires it, as it requires Kits.
|
||
|
||
It replaces [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §6's "two routes". That section's spike was
|
||
run on 2026-09-30, and the org lead chose neither route (D214). What the spike found is §1 here, because every
|
||
choice below rests on it.
|
||
|
||
Its decisions continue the Rust workstream's numbering at **D214** ([`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §10).
|
||
The repository carries the same conditions as every Runic Gateway repository: **GPL-3.0-or-later**, Conventional
|
||
Commits, AI-assisted work disclosed, branches cut from an up-to-date base, and the org lead's approval before any
|
||
architectural or design decision is implemented.
|
||
|
||
---
|
||
|
||
## 0. The direction, in the org lead's words (2026-09-30)
|
||
|
||
| # | Decision | Rejected |
|
||
|---|---|---|
|
||
| **D214** | **Runic Gateway writes its own NPC plugin**, an API-style plugin like NpcSpawn, in its own repository. It may borrow *behaviours* the other plugins implement, never their code. | Extending `rust.npc.place` on Rust's scientists alone (route A); HumanNPC (route B); depending on NpcSpawn. |
|
||
| **D215** | **It is not only for events.** Admins place NPCs in game, where they want them, for normal play, so it has chat commands. | NPCs only as event steps. |
|
||
| **D216** | **No visual editor.** Profiles are authored on the website; in game there are commands only. | NpcSpawn's in-game GUI (and the 22 images it needs). |
|
||
| **D217** | **Kits is how an NPC is equipped, and Kits is required.** A profile names kits; it has no item lists of its own. | Wear and belt lists in the profile. |
|
||
| **D218** | **Loot tables are RunicNPC's own for now.** Links to other loot plugins can come later. | AlphaLoot, CustomLoot, Loottable or LootManager as a dependency. |
|
||
| **D219** | **Rust's own navmesh is used. A custom navigation mesh is a later stage, built only if a real need appears** (§8, stage 10). | Shipping NpcSpawn-style point meshes from the start. |
|
||
| **D220** | **The plugin is built and tested in stages before the Rust plan resumes, and it then becomes a plugin `module-rust` requires.** | Building it alongside the remaining redesigns. |
|
||
|
||
**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
|
||
design, so there is nothing of it worth carrying. RunicNPC's code is written from how Rust's own classes behave (the
|
||
HOOKS and OXIDE_API mirrors, and rig probes), and every borrowed idea is credited in §3 by what it is, not by copied
|
||
text.
|
||
|
||
---
|
||
|
||
## 1. What the spike found (2026-09-30, the Oxide rig)
|
||
|
||
The probe was a throwaway plugin, `RgNpcSpike.cs`. It spawned NPCs by each route, watched them for minutes, made
|
||
them fight, killed them, and read what the bridge published through the sidecar's `/events`. The org lead stopped
|
||
the Carbon half: one server was enough to see that HumanNPC does not behave as needed.
|
||
|
||
### 1.1 Rust's own scientist, stripped and given a kit (route A)
|
||
|
||
| Question | Answer |
|
||
|---|---|
|
||
| Does the kit stick? | **Yes.** `Strip()`, Kits' `GiveKit`, then `EquipWeapon()`: after three minutes of combat it still wore the kit. Rust does not re-dress it. |
|
||
| Does it fight with the kit's weapon? | **Yes.** Every hit it landed was `pistol_revolver.entity`, from a revolver kit. Its prefab loadout was an LR-300. |
|
||
| Can it be named? | **No.** `ScientistNPC` overrides `displayName` with a get-only property that answers "Scientist". Setting `_displayName` stores the name and changes nothing. |
|
||
| Is it counted correctly? | Yes. `IsNpc` is true, so every bridge path treats it as an NPC. |
|
||
| Cost | Under Oxide's slow-call warning; no measurable hitch. |
|
||
|
||
### 1.2 HumanNPC 0.6.8 (route B) — ruled out
|
||
|
||
- **Its NPCs are counted as players.** It spawns `player.prefab` with a made-up user id and Harmony-patches
|
||
`BasePlayer.IsBot` for every player on the server. `IsNpc` stays **false**, and that is what the bridge tests. The
|
||
sidecar recorded a `player.death` for "Hobb" (`steamId` 11419803648) and a `player.tally` crediting "Hobb2" with an
|
||
NPC kill: a fake player in the public killfeed, the leaderboard and the titles.
|
||
- **The API the old plan named does not exist.** There is no `SpawnHumanNPC` or `SetHumanNPCInfo`: only
|
||
`CreateNPCHook(position, rotation, name, clone, saved)`. Kit, hostility and invulnerability can be set only by
|
||
reflection into its private types. Its `RefreshNPC` and `RemoveNPC` are public without `[HookMethod]`, so Oxide's
|
||
`Call` cannot reach them either.
|
||
- **Its combat is simulated.** Its hits carried no weapon and a flat ~7 "Bullet" damage, even with a bow.
|
||
- **Its persistence leaks.** `saved: false` holds only until anything else saves: one saved NPC wrote every unsaved
|
||
one to its data file. A dead NPC came back on the next reload. `RemoveNPC` never marks the file dirty, so removed
|
||
NPCs return at the next restart.
|
||
- **It is slow.** ~250 ms to create, ~250 ms to apply settings, ~250–320 ms to remove, each on the main thread.
|
||
- **It breaks on Rust updates.** Nearly every release since 2023 is "fix for Rust update"; 0.6.0 fixed servers that
|
||
failed to load.
|
||
|
||
### 1.3 NpcSpawn 3.4.8 (the org lead's suggestion)
|
||
|
||
It worked, and it is the model RunicNPC borrows from:
|
||
|
||
- It **replaces the scientist's component with its own subclass**, `CustomScientistNpc : ScientistNPC`, copying the
|
||
serialised fields across (`EntityManager.CopySerializableFields`). No Harmony. `IsNpc` is true, so the bridge
|
||
counts it correctly unchanged.
|
||
- **The name sticks**, and it is written into the victim's death screen (`AttackerInfo`).
|
||
- **Kits work**, and it fought with the kit's revolver (with `NpcAttackMode: 1`; its default only attacks a whitelist).
|
||
- **It is never saved** (`enableSaving = false`). After a hard kill and restart, none of three came back. Reloading
|
||
NpcSpawn kills all of its NPCs.
|
||
- Spawning took 4–106 ms.
|
||
|
||
Its costs, which RunicNPC avoids by design: a 2.1–2.8 s main-thread hitch at every boot (it scans ten million
|
||
candidate ids and generates spawn points); a version check over plain HTTP to a bare IP on every boot; it
|
||
**unloads itself** if any of its 22 GUI images is missing; it is not on uMod and states no licence, so it cannot be
|
||
bundled; and it has had 78 releases.
|
||
|
||
### 1.4 Two corrections to the old plan
|
||
|
||
- **"`displayName` already flows into the killfeed" is wrong for every route.** The bridge names an NPC attacker by
|
||
`initiator.ShortPrefabName` (`RunicGateway.cs`, `DescribeAttacker`), so the feed says `scientistnpc_roam`. Stage 4
|
||
fixes it.
|
||
- **HumanNPC's API, as described, does not exist** (§1.2).
|
||
|
||
### 1.5 A rig trap found on the way
|
||
|
||
A data directory created by the panel's file API is **not writable by the game** (`IOException: Permission denied`
|
||
from `Directory.CreateDirectory`). Let a plugin create its own data directory, then write files into it. The
|
||
installer and the egg must do the same with any data RunicNPC ships.
|
||
|
||
---
|
||
|
||
## 2. What RunicNPC is
|
||
|
||
**One plugin file, `RunicNPC.cs`**, compiled by Oxide or Carbon from source, like the bridge. It declares
|
||
`// Requires: Kits`, so neither framework loads it without Kits (D217).
|
||
|
||
**Its NPC is its own type,** a subclass of Rust's `ScientistNPC` with its own brain. This follows NpcSpawn's approach
|
||
(§1.3), because only a subclass can:
|
||
|
||
- override `displayName`;
|
||
- override the aim cone;
|
||
- write its name into the victim's death screen;
|
||
- carry behaviours Rust's brain does not have (guard, escort, boss phases).
|
||
|
||
Stage 1 proves the swap on both frameworks before anything is built on it.
|
||
|
||
**It owns nothing it is not asked for.** Every NPC has an **owner**, and the owner decides its lifetime:
|
||
|
||
| Owner | Who | Survives a restart? |
|
||
|---|---|---|
|
||
| `run:<runId>` | An event run, through the bridge | **No.** Never saved; the bridge's registry reverts it, as `world.place` does today. |
|
||
| `placement:<id>` | An admin's placement in game, or one made from the site | **Yes, as a placement:** RunicNPC stores the spot, profile and respawn delay, and spawns a fresh NPC there at boot. The NPC itself is never saved. |
|
||
| `plugin:<name>` | Any other plugin using the API | No. It is killed when that plugin unloads. |
|
||
|
||
This split is what HumanNPC got wrong: the *placement* is persistent data, the *NPC* never is. There is exactly one
|
||
save path, a flag marks it dirty on every change (including removal), and nothing else is written.
|
||
|
||
**A profile is a named description of an NPC:**
|
||
|
||
- **Appearance:** name (or a list to pick from), the kits it wears, body type, and the prefab it starts from.
|
||
- **Combat:** health, damage scales (overall, and by head, body, legs and melee), aim spread, and ranges (sense,
|
||
chase, roam, attack).
|
||
- **Behaviour:** its role and AI states (§6).
|
||
- **Loot:** its loot table (§7).
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## 3. Features
|
||
|
||
Borrowed ideas are marked with their source; everything else is new. Stage numbers are §9's.
|
||
|
||
### 3.1 From NpcSpawn (behaviour only)
|
||
|
||
| Feature | Stage |
|
||
|---|---|
|
||
| A scientist subclass with its own brain; never saved | 2 |
|
||
| Names, a random name from a list; body type and skin tone chosen by the NPC's id | 2 |
|
||
| Health, overall and per-body-part damage scales, aim cone, headshot rule | 2 |
|
||
| Sense, listen, chase, roam and attack ranges; a vision cone; hostile-only; ignore safe-zone, sleeping or wounded players | 2, 5 |
|
||
| Sleep when no player is within a distance, and walk home | 2 |
|
||
| States: roam, chase, combat, idle, stationary combat | 2, 5 |
|
||
| NPC-versus-NPC and NPC-versus-animal rules, with allow and deny lists | 5 |
|
||
| Group alert: nearby allies join a fight | 5 |
|
||
| Turrets may target them, and they fight back | 5 |
|
||
| Healing with syringes; grenades, smoke, rockets, flamethrowers, melee (by the weapon the kit gives) | 5 |
|
||
| Parenting to a moving entity | 10 |
|
||
| Loot tables, a crate on death, removing the corpse | 7 |
|
||
| TruePVE / NextGenPVE compatibility (`CanEntityTakeDamage`) | 5 |
|
||
|
||
### 3.2 From HumanNPC (behaviour only)
|
||
|
||
| Feature | Stage |
|
||
|---|---|
|
||
| Press E to talk: an NPC says a line, or opens a message, when used | 6 |
|
||
| Lines on greet, hurt and kill | 6 |
|
||
| Follow a player, and walk a recorded path | 5 |
|
||
|
||
### 3.3 Runic Gateway's own
|
||
|
||
| Feature | Stage |
|
||
|---|---|
|
||
| **Profiles authored on the website** and pushed to each server, per server, shared, or fleet-wide (the zone-presets pattern, D210). The site checks every kit exists before it saves. | 4 |
|
||
| **Admin placement in game** by chat command (§5), persistent, with respawn delays; the site lists and edits the placements | 3, 4 |
|
||
| **Correct accounting**: an NPC is never counted as a player; its name reaches the killfeed, event log and death screen; kills are counted per profile | 2, 4 |
|
||
| **Per-profile stats and titles**: "Warden kills", boss kills; the killing blow and every player who did damage | 4 |
|
||
| **Caps**: per server, per owner, per profile, a spawn-rate limit, and a performance budget | 2 |
|
||
| **Event ownership**: run-owned NPCs with the bridge's restart and teardown guarantees | 4 |
|
||
| **Waves and phases** without a core change: a phase advances on "n NPCs of profile X died", and on "the boss fell below 50%" | 4, 6 |
|
||
| **Zone tethering**: an NPC held inside a ZoneManager zone (from redesign §3) | 5 |
|
||
| **Guard and escort**: hold a point or an entity; follow a convoy or a player | 5 |
|
||
| **Factions**: profiles hostile or allied to each other; allied to a team or clan, for defend-your-base events | 5 |
|
||
| **Bosses**: an on-screen health bar, phases at health thresholds, announcements (popup, chat, Discord and the app through engagement), a reward on the kill | 6 |
|
||
| **Quest-giver or vendor**: passive, press E for a site-written message | 6 |
|
||
| **The live map and the app**: markers for event NPCs and bosses, "guards left: 3/8" | 8 |
|
||
| **Operator-friendly**: shipped in the bundle, no phone-home, no image files, no boot scan; `doctor` reports it; RunicNPC's permissions appear in the site's permission manager like any plugin's | 2, 4 |
|
||
| **An API for other plugins**, versioned, documented, and usable without Runic Gateway at all if §11 Q1 says so | 2, 9 |
|
||
|
||
---
|
||
|
||
## 4. The API
|
||
|
||
Other plugins call RunicNPC by name through `Call`, which on Oxide reaches **non-public** methods and public ones
|
||
marked `[HookMethod]` (§1.2 is what happens otherwise). Every name is prefixed, so no hook of another plugin is ever
|
||
matched by accident. The first draft, fixed in stage 2 and published as `docs/runicnpc/API.md`:
|
||
|
||
| Call | Does |
|
||
|---|---|
|
||
| `RunicNpc_ApiVersion()` → `int` | The API's version. The bridge refuses a RunicNPC older than it needs, and says so in hello. |
|
||
| `RunicNpc_Spawn(Vector3 at, string profile, string owner, JObject overrides)` → `BasePlayer` | Spawns one NPC, or returns null and logs why. `overrides` may change any profile value for this NPC only. |
|
||
| `RunicNpc_Despawn(ulong netId)` / `RunicNpc_DespawnOwner(string owner)` → `int` | Removes one, or all of an owner's. |
|
||
| `RunicNpc_List(string owner)` → `List<Dictionary<string, object>>` | The owner's live NPCs: id, profile, name, position, health. |
|
||
| `RunicNpc_Profiles()` / `RunicNpc_SetProfiles(JObject all)` | Reads, or replaces, the profile set (§11 Q1 decides who may). |
|
||
| `RunicNpc_Placements()` / `RunicNpc_SetPlacement(...)` / `RunicNpc_RemovePlacement(string id)` | The persistent placements (§2). |
|
||
| `RunicNpc_IsRunicNpc(BaseEntity e)` → `bool`, `RunicNpc_ProfileOf(BaseEntity e)` → `string` | Lets another plugin tell our NPCs apart. |
|
||
|
||
**Hooks it raises** (void; a returning hook can cancel a death, see PROTOCOL.md §8.7):
|
||
`OnRunicNpcSpawned(npc, profile, owner)`, `OnRunicNpcDied(npc, profile, owner, HitInfo, contributors)`,
|
||
`OnRunicNpcHealth(npc, profile, fraction)` (at the thresholds a profile names), `OnRunicNpcDespawned(npc, owner)`,
|
||
`OnRunicNpcUsed(npc, player)`.
|
||
|
||
**The bridge is its only link to the site.** RunicNPC never talks to the sidecar. The bridge calls the API and
|
||
listens to the hooks, which keeps "the sidecar is a dumb forwarder" true.
|
||
|
||
---
|
||
|
||
## 5. In game: the chat commands (D215, D216)
|
||
|
||
`/rnpc` (and the same verbs in the server console as `rnpc.<verb>`), each behind a RunicNPC permission. Those
|
||
permissions reach the site's permission manager through the inventory, like any plugin's.
|
||
|
||
| Command | Does | Permission |
|
||
|---|---|---|
|
||
| `/rnpc place <profile> [count] [respawn]` | Places at the spot you are looking at, persistent | `runicnpc.place` |
|
||
| `/rnpc here <profile> [count]` | The same, where you stand | `runicnpc.place` |
|
||
| `/rnpc remove` | Removes the placement of the NPC you are looking at | `runicnpc.place` |
|
||
| `/rnpc near [radius]` | Lists placements and live NPCs near you | `runicnpc.place` |
|
||
| `/rnpc info` | The NPC you are looking at: profile, owner, health, target, state | `runicnpc.place` |
|
||
| `/rnpc profiles` | The profiles this server has | `runicnpc.place` |
|
||
| `/rnpc tp <placement>` | Teleports you to a placement | `runicnpc.admin` |
|
||
| `/rnpc respawn [placement\|all]` | Respawns a placement's NPC now | `runicnpc.admin` |
|
||
| `/rnpc clear <owner>` | Removes every NPC of an owner (e.g. a stuck run) | `runicnpc.admin` |
|
||
|
||
**Every placement is checked against Rust's navmesh (D219).** On the navmesh, the NPC roams, chases and fights
|
||
normally. Off it (a roof, inside a base, a pasted structure), a **stationary** profile is placed and a roaming one
|
||
is refused with the reason. An event step gets the same answer, as a blocked RaidableBases spot does (D208).
|
||
|
||
---
|
||
|
||
## 6. Behaviour
|
||
|
||
Each profile has a **role**, which sets its AI states and defaults:
|
||
|
||
| Role | Behaviour |
|
||
|---|---|
|
||
| **Roamer** | Wanders within its roam range of home, chases and fights. Rust's default. |
|
||
| **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. |
|
||
| **Patrol** | Walks a path recorded in game (`/rnpc path record`); fights and resumes. |
|
||
| **Escort** | Follows an entity or player; defends it. |
|
||
| **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. |
|
||
|
||
**Factions** are a relation table between profiles, plus optional ties to a team or clan: *hostile*, *neutral*
|
||
or *allied*. Rust's scientists are one more faction, so a profile can be told to leave them alone.
|
||
|
||
**Zone tethering** keeps an NPC inside a ZoneManager zone. At the edge it turns back, and if it ends up outside (it
|
||
was pushed, or the zone moved) it is returned home. ZoneManager is already required by `module-rust`; to RunicNPC it
|
||
is optional, and a profile that asks for a tether on a server without it is refused at save.
|
||
|
||
---
|
||
|
||
## 7. Loot (D218)
|
||
|
||
A profile's loot table: items with chances and amounts, optionally clearing the corpse's default loot, dropping a
|
||
crate, or removing the corpse. Tables are authored on the site with the profile. Adapters to AlphaLoot and the
|
||
others are a later stage and not planned in detail here.
|
||
|
||
---
|
||
|
||
## 8. Navigation (D219)
|
||
|
||
**Rust's navmesh is the only one in stages 1–9.** Every spike NPC walked and fought on it, and NpcSpawn itself
|
||
shipped no custom mesh. Where it does not reach, a profile is a sentry (§5).
|
||
|
||
**Stage 10, only if needed:** a point graph recorded in game. An admin walks the route, `/rnpc mesh record <name>`
|
||
samples it, and NPCs move between the points directly, as NpcSpawn's custom meshes do, for:
|
||
|
||
- pasted or player-built bases, and roofs;
|
||
- custom map prefabs Rust's mesh does not cover;
|
||
- moving platforms, together with parenting.
|
||
|
||
Stage 10 starts only when a real event or placement needs one, and its own spike decides the file format and cost.
|
||
|
||
---
|
||
|
||
## 9. The stages
|
||
|
||
Each stage is built, then tested on the **Oxide rig**, then on the **Carbon rig**, then PR'd with its docs. A stage
|
||
that touches the site also has a browser walk, signed in as an admin, on every new page. A stage whose acceptance
|
||
needs a player in game writes that walk down, and it is done before the stage closes.
|
||
|
||
### Stage 0 — The repository
|
||
|
||
- `LICENSE.md` (GPL-3.0-or-later), `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md` (with the AI-disclosure rule),
|
||
`SECURITY.md`, `README.md`, PR template.
|
||
- Branches `main` and `edge` (§11 Q5).
|
||
- **CI on every PR:** a static reader like Rust-Plugins' `checkPlugin.js`: every hook it declares is known, none
|
||
returns a value where one would cancel a game action, and the API version in the source equals the manifest's.
|
||
- **Release on `main`:** `runicnpc-<ver>.tar.gz` holding `runicnpc/RunicNPC.cs` and a `manifest.json` (version, API
|
||
version, sha256), plus `SHA256SUMS`, using the same release engine as Rust-Plugins.
|
||
- `tools/`, never shipped: the rig harness (stage 1) and the panel scripts (`rig.js`, `con.js`), reading tokens at
|
||
call time.
|
||
- Workspace and org updates: the `CLAUDE.md` repository table and the org landing page (`.profile`), because the
|
||
project's shape changes.
|
||
|
||
**Done when** an empty plugin releases through CI and loads on both rigs.
|
||
|
||
### Stage 1 — The spike
|
||
|
||
A harness plugin (`tools/RunicNpcHarness.cs`) answers, on **both** rigs, what stage 2 depends on:
|
||
|
||
1. The subclass swap (`CopySerializableFields` into our `ScientistNPC` subclass and brain) spawns, walks, fights
|
||
and dies cleanly on Oxide **and Carbon**.
|
||
2. `enableSaving = false` holds across a hard kill and restart (§1.3 did this for NpcSpawn only).
|
||
3. Which ranges and scales hold on the brain without our own states, and which need them.
|
||
4. Navmesh coverage: a placement check at every monument of the 6000 map, and on a roof.
|
||
5. The cost of spawning 1, 10 and 100, and of 100 idle and 100 fighting, as the server's frame time.
|
||
6. The name in the death screen, and what the bridge publishes for a kill by and of our NPC.
|
||
|
||
**Done when** each has a measured answer written into this plan, and the org lead has picked anything the answers
|
||
leave open.
|
||
|
||
### 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,
|
||
roamer and sentry roles, sleep.
|
||
- Owners and lifetimes (§2); placements persisted with one dirty flag; caps and a spawn-rate limit.
|
||
- 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
|
||
and reload checks on both rigs: no NPC saved, every placement back, nothing left behind by an unloaded owner.
|
||
|
||
### Stage 3 — In game
|
||
|
||
The chat and console commands (§5), their permissions, the navmesh placement check, respawn delays, and `/rnpc
|
||
path record` for stage 5.
|
||
|
||
**Tested by** a written in-game walk: place, remove, look-at info, respawn, restart. **This is the first stage that
|
||
needs a player on a rig.**
|
||
|
||
### Stage 4 — Runic Gateway integration
|
||
|
||
Across four repositories, split into 4a–4c if it grows:
|
||
|
||
- **Rust-Plugins (the bridge):**
|
||
- hello reports RunicNPC's presence and API version;
|
||
- `world.place` for an NPC takes a profile, and the placing is RunicNPC's;
|
||
- the killfeed names NPCs by their name (§1.4);
|
||
- new frames: an NPC died, with profile, name, killer and contributors; a health threshold; placements changed;
|
||
- `player.tally` counts NPC kills per profile;
|
||
- profiles and placements pushed from the site, as the permission sync is.
|
||
- **Rust-Link:** routes for profiles and placements, and the frames forwarded.
|
||
- **Module-Rust:**
|
||
- an **NPC profiles** admin page (per server, shared or fleet-wide) with the kit check;
|
||
- a **placements** view per server;
|
||
- `rust.npc.place` takes a profile, through a new option source `rust.options.npc_profiles`;
|
||
- triggers `rust.npc.died` and `rust.npc.health`, so a phase can wait for "8 guards died" (waves) or "the boss
|
||
below 50%";
|
||
- per-profile stats and titles.
|
||
- **Installer and egg:**
|
||
- RunicNPC becomes a third artefact in the Rust bundle, pinned and checksummed like the plugin and the sidecar
|
||
(§11 Q6);
|
||
- `doctor` reports it missing or edited;
|
||
- the egg installs it.
|
||
|
||
The wire changes join protocol 13 if it is still unreleased, or open 14.
|
||
|
||
**Tested by** the usual trio: each repository's suites, both rigs through the site, and a browser walk of both new
|
||
pages. An event run places profile NPCs, waves advance on their deaths, and teardown removes them.
|
||
|
||
### Stage 5 — Behaviour
|
||
|
||
Guard, patrol and escort roles; factions (between profiles, with Rust's scientists, and with teams or clans); group
|
||
alert; zone tethering; turrets; the weapon behaviours a kit's items imply; TruePVE compatibility.
|
||
|
||
**Tested by** harness fights between profiles on the rig (who shoots whom, with what, and within which leash), and
|
||
an in-game walk for escort and faction ties to a clan.
|
||
|
||
### Stage 6 — Roles
|
||
|
||
Bosses (a health bar drawn with CUI, phases at thresholds that swap profile values, announcements through the bridge
|
||
to engagement, a reward on the kill through the existing reward verbs) and passive NPCs (press E, site-written lines).
|
||
|
||
**Tested by** an in-game walk (the bar, a phase change, the announcement arriving on Discord and in the app), and
|
||
the site's trigger firing at the threshold.
|
||
|
||
### Stage 7 — Loot
|
||
|
||
Loot tables authored on the site, the corpse's default loot cleared or kept, a crate on death, corpse removal.
|
||
|
||
**Tested by** harness kills whose corpses are read back, 100 times each, against the table's chances.
|
||
|
||
### Stage 8 — On the map and in the app
|
||
|
||
Event NPCs and bosses on the live map (protocol 11's `map.live`), a live "left: 3/8" on the event page, and the same
|
||
in the Android app.
|
||
|
||
**Tested by** a browser walk and an emulator walk.
|
||
|
||
### Stage 9 — Hardening and release
|
||
|
||
- **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.
|
||
- **The whole acceptance walk on both rigs, with a player.**
|
||
- **Operator documentation:** `docs/runicnpc/` gets INSTALL and a command reference; `module-rust`'s OPERATING notes
|
||
list it as required.
|
||
- **v1.0.0 released, and `module-rust` then requires it:**
|
||
- the bundle lists it;
|
||
- the module refuses NPC steps on a server without it, saying why;
|
||
- the bridge reports it in hello.
|
||
|
||
**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)
|
||
|
||
§8. Not scheduled.
|
||
|
||
---
|
||
|
||
## 10. Maintenance, honestly
|
||
|
||
| What breaks | When | How we know | Cost |
|
||
|---|---|---|---|
|
||
| The subclass swap and the brain states | A Rust update that changes `ScientistNPC`, its brain, or `BaseNavigator` | The staging-branch rig, before the wipe | Our release timing, not an upstream author's. This is NpcSpawn's churn, but for the states we use, not all of its twenty. |
|
||
| Kits' API | A Kits release that renames `GiveKit` | Stage 2's harness | Low: one call. |
|
||
| ZoneManager's API | A ZoneManager release | Stage 5's harness | Low, and the tether is optional. |
|
||
| Carbon's compatibility layer | A Carbon release | The Carbon rig | Seen before; recorded in CARBON.md. |
|
||
|
||
A RunicNPC fix ships as its own release. It moves the bundle's pin only when the bridge needs it to, so an NPC fix
|
||
does not wait for a bridge release, or the reverse.
|
||
|
||
---
|
||
|
||
## 11. Questions for the org lead
|
||
|
||
**Q1. Can RunicNPC be used without Runic Gateway?** Picture a server owner who installs only RunicNPC, from uMod.
|
||
|
||
- **(a, recommended) Yes.** Without a site, profiles live in RunicNPC's data file and console commands edit them.
|
||
With a site, the site's profiles replace that file on every push, and in-game profile edits are refused with "this
|
||
server's profiles are managed by its website".
|
||
- **(b) No.** Profiles come only from a site, and the plugin is only ever installed by our installer.
|
||
|
||
**Q2. An admin places three guards with `/rnpc place`, then the site is offline for a day. What happens?**
|
||
|
||
- **(a, recommended)** The placements live on the server, so the guards keep respawning. The site catches up when it
|
||
returns, and shows and edits them from then on.
|
||
- **(b)** The site is the only record, so a placement made while it is offline is refused.
|
||
|
||
**Q3. What should our NPC be?**
|
||
|
||
- **(a, recommended)** Its own subclass of Rust's scientist with its own brain, as NpcSpawn does. It can be named,
|
||
and it can guard, escort and run boss phases. The brain is the part a Rust update can break.
|
||
- **(b)** Rust's scientist left as it is, plus small patches for the name and a few values. There is less to break,
|
||
but there is no guard, escort or boss behaviour.
|
||
|
||
**Q4. A player kills your "Warden" boss. Where does "Warden kills: 3" show?**
|
||
|
||
- **(a, recommended)** On their public profile and as a title condition, like the NPC kills column today.
|
||
- **(b)** Only to admins.
|
||
|
||
**Q5. Branches.** Like the other Rust repositories (D18), build on `edge` and cut over to `main` for releases?
|
||
**Recommended: yes.**
|
||
|
||
**Q6. How does RunicNPC reach a server?**
|
||
|
||
- **(a, recommended)** The installer and the egg install it from our bundle, pinned and checksummed like the bridge.
|
||
Kits and ZoneManager stay "report, don't install" (D153).
|
||
- **(b)** Operators install it themselves, like Kits, and `doctor` only reports it.
|
||
|
||
**Q7. How many NPCs by default?** Recommended defaults, each editable per server:
|
||
|
||
- 100 NPCs on a server in all;
|
||
- 50 per owner (one event run, or all of one admin's placements);
|
||
- 10 spawns per second.
|
||
|
||
**Q8. The name.** The plugin file is `RunicNPC.cs`, so its console name is `RunicNPC`, with the command `/rnpc` and
|
||
permissions `runicnpc.*`. **Recommended: yes.**
|