docs(runicnpc): plan RunicNPC, our own Rust NPC plugin (D214–D220) #298
11
README.md
11
README.md
@@ -11,6 +11,7 @@ website/ docs from the website core (Node/Express + MariaDB + React/Vite)
|
||||
modules/ docs for installable game modules — one directory per module id
|
||||
link/ docs from the ServUO bridge (C# plugin + Rust sidecar + Node WS)
|
||||
rust-link/ docs from the Rust bridge (Oxide plugin + Rust sidecar)
|
||||
runicnpc/ docs for RunicNPC, the Rust NPC plugin (runicnpc-rust)
|
||||
android/ docs from the native Android client (Kotlin + Jetpack Compose)
|
||||
installer/ docs for the installer that deploys a shard's bridge components
|
||||
ci/ cross-cutting CI/quality notes
|
||||
@@ -89,6 +90,16 @@ share a shape and nothing else, so neither document is a fallback for the other.
|
||||
| [INTEGRATION.md](rust-link/INTEGRATION.md) | Standing the bridge up by hand, and which of the three components is wrong when it does not work |
|
||||
| [PLAYER_WALK.md](rust-link/PLAYER_WALK.md) | The half of the read path a console cannot reach: ten minutes on a rig with a player, step by step, with what each hook should produce |
|
||||
|
||||
### `runicnpc/`
|
||||
|
||||
RunicNPC, Runic Gateway's own NPC plugin for Rust (Oxide and Carbon), in its own repository. Admins place NPCs
|
||||
with it in game, events place them through the bridge, and other plugins drive it through its API. `module-rust`
|
||||
requires it once it releases (D220).
|
||||
|
||||
| Doc | What it covers |
|
||||
|---|---|
|
||||
| [PLAN.md](runicnpc/PLAN.md) | **The plan** — what the 2026-09-30 spike found (route A, HumanNPC, NpcSpawn), the org lead's decisions D214–D220, the features, the API, the chat commands, and stages 0–10 with how each is tested |
|
||||
|
||||
### `android/`
|
||||
| Doc | What it covers |
|
||||
|---|---|
|
||||
|
||||
@@ -6,7 +6,9 @@ It is step 3 of [`PLAN_FIXES.md`](PLAN_FIXES.md) §6: the six changes the org le
|
||||
player walk, each "planned in detail before code". The org lead asked for all six in one plan (2026-09-27), so
|
||||
they can be read together. **Built so far:** §1, walked 2026-09-28 (§1.9); §5, built 2026-09-28 with its
|
||||
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).
|
||||
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–D228), 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**.
|
||||
@@ -18,7 +20,7 @@ document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D1
|
||||
| 3 | Zones: ZoneManager's options, and a dome | §4.4, D166, D167 | Rust-Plugins (bridge and helper), Module-Rust |
|
||||
| 4 | The live map's marker types | §4.5, D165 | Module-Rust |
|
||||
| 5 | Chat titles: twenty-three conditions | §4.6, D172–D175 | Rust-Plugins, Module-Rust |
|
||||
| 6 | NPCs | §4.7 | Rust-Plugins, Module-Rust |
|
||||
| 6 | NPCs — now RunicNPC, its own plan ([`runicnpc/PLAN.md`](../../runicnpc/PLAN.md)) | §4.7 | **runicnpc-rust**, Rust-Plugins, Rust-Link, Module-Rust, installer |
|
||||
| 11 | First-class optional plugins: Economics, Backpacks, RaidableBases (Kits stays required) | — (added 2026-09-28) | Rust-Plugins, Module-Rust, installer, Android-app |
|
||||
|
||||
**Protocol.** Every wire change here joins **protocol 13**, which is still unreleased on `edge` in all three
|
||||
@@ -747,6 +749,12 @@ new hooks need a player: that is §5.7's walk.
|
||||
|
||||
## 6. NPCs (§4.7)
|
||||
|
||||
> **Superseded 2026-09-30.** The spike below was run on the Oxide rig. Route A kept its kit and fought with it
|
||||
> but cannot be named; HumanNPC counted its NPCs as players in the killfeed and tallies. The org lead then chose
|
||||
> **neither route**: Runic Gateway writes its own NPC plugin, **RunicNPC**, in its own repository (D214–D220). The
|
||||
> spike's results, two corrections to the text below, and the staged plan are in
|
||||
> [`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md). The text below is kept as what was planned.
|
||||
|
||||
**D197: the spike tries both routes on both rigs, then the org lead picks one.** The goal is the one the org
|
||||
lead named: NPCs with different kits and names. NPC Loadouts is out, because it cannot dress one spawn
|
||||
differently from the next (§0.10). The spike reports on each route before any site work.
|
||||
@@ -815,7 +823,8 @@ Each item is built on `edge`, walked on both rigs, and PR'd with its spec, like
|
||||
2. **Chat titles** (§5), including the mission hook.
|
||||
3. **Zones and the dome** (§3), with the zone half of D193's popups and the domes helper.
|
||||
4. **The map's marker types** (§4).
|
||||
5. **NPCs** (§6): the two-route spike, then the org lead's pick, then the build.
|
||||
5. **NPCs** (§6): spiked 2026-09-30; now **RunicNPC**, built and released through its own stages 0–9
|
||||
([`runicnpc/PLAN.md`](../../runicnpc/PLAN.md) §9) **before items 6 and 7** (D220).
|
||||
6. **The step editor and the kit weekend** (§2). It is core's, so it can be built alongside any of the above.
|
||||
It goes out as its own website PR with the Module-uo proof.
|
||||
7. **The first-class optional plugins** (§11): Economics, then Backpacks, then RaidableBases, each starting
|
||||
@@ -875,6 +884,13 @@ The §4 build raised one more, answered on 2026-09-29:
|
||||
| # | Decision | Rejected | § |
|
||||
|---|---|---|---|
|
||||
| **D213** | **Eight labels the 6000 map showed also start hidden:** Canyon B, Canyon C, Lake A, Oasis A, Oasis C, Mountain (terrain, not places to loot), Train Tunnel Link (like Train Tunnel), and Abandoned Cabins (the third swamp, like Wild Swamp). | Leaving them drawn under "every other label is on"; hiding the terrain and tunnel but drawing the cabins for their loot. | 4.1 |
|
||||
| **D214** | **Runic Gateway writes its own NPC plugin, RunicNPC**, API-style like NpcSpawn, in its own repository `runicnpc-rust`; behaviours may be borrowed, code may not. | Route A alone; HumanNPC; depending on NpcSpawn. | 6, [RunicNPC §0](../../runicnpc/PLAN.md) |
|
||||
| **D215** | **RunicNPC is not only for events:** admins place NPCs in game for normal play, through chat commands. | Event steps only. | 6, RunicNPC §5 |
|
||||
| **D216** | **No visual editor:** profiles are authored on the website; in game, commands only. | NpcSpawn's in-game GUI. | 6, RunicNPC §0 |
|
||||
| **D217** | **Kits is how an NPC is equipped, and Kits is required.** | Item lists in the profile. | 6, RunicNPC §2 |
|
||||
| **D218** | **Loot tables are RunicNPC's own for now;** links to other loot plugins later. | A loot plugin as a dependency. | 6, RunicNPC §7 |
|
||||
| **D219** | **Rust's navmesh is used;** a custom navigation mesh is a later stage, only if needed. | NpcSpawn-style point meshes from the start. | 6, RunicNPC §8 |
|
||||
| **D220** | **RunicNPC is built and tested in stages before this plan resumes, then `module-rust` requires it.** | Building it alongside items 6 and 7. | 9, RunicNPC §9 |
|
||||
|
||||
## 11. First-class optional plugins (D199–D202)
|
||||
|
||||
|
||||
512
runicnpc/PLAN.md
Normal file
512
runicnpc/PLAN.md
Normal file
@@ -0,0 +1,512 @@
|
||||
# RunicNPC — the plan
|
||||
|
||||
**Status:** plan, written 2026-09-30. No code yet. **Its eight questions (§11) were answered the same day: D221–D228 (§0).**
|
||||
|
||||
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. |
|
||||
| **D221** | **RunicNPC works without Runic Gateway too** (Q1). Standalone, its profiles live in its data file and console commands edit them. On a Runic Gateway server, 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". | Runic Gateway only. |
|
||||
| **D222** | **Placements live on the server** (Q2). They keep respawning while the site is offline; the site catches up when it returns, and lists and edits them from then on. | The site as the only record. |
|
||||
| **D223** | **Our NPC is its own subclass of Rust's scientist, with its own brain** (Q3). The brain is kept small and tested on Rust's `staging` branch before each forced wipe. | Rust's scientist plus patches. |
|
||||
| **D224** | **The installer and the egg ship it from the bundle, pinned and checksummed** (Q6). It is also published as a release on Gitea, and possibly on other sites for anyone to download. | Operators installing it themselves. |
|
||||
| **D225** | **Per-profile kills are public and usable in titles** (Q4), like the NPC kills column today. | Admins only. |
|
||||
| **D226** | **`edge` → `main`**, like the other Rust repositories (Q5, D18). | PRs straight to `main`. |
|
||||
| **D227** | **No default caps.** Instead, a realistic warning of what a number of NPCs costs and its impact on the server, measured in stage 1, shown wherever NPCs are added (Q7). Caps exist only when an admin sets them. | 100 / 50 / 10 per second by default. |
|
||||
| **D228** | **`RunicNPC.cs`, console name `RunicNPC`, chat command `/rnpc`, permissions `runicnpc.*`** (Q8). | — |
|
||||
|
||||
**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.
|
||||
|
||||
**Standalone or managed (D221).** Without a site, RunicNPC reads its profiles from its own data file and console
|
||||
commands edit them. With a site, the bridge pushes the site's profiles, which replace the file; the file records that
|
||||
it is managed, and in-game profile edits are refused with "this server's profiles are managed by its website".
|
||||
Placements stay the server's either way (D222): the site reads them and edits them through the bridge.
|
||||
|
||||
**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 |
|
||||
| **Cost warnings, not default caps (D227)**: what N NPCs cost the server, measured in stage 1, shown when an admin places them, on the site's profile and placement pages, and in `doctor`. Caps (per server, owner or profile, and a spawn rate) exist only when an admin sets them | 2, 4 |
|
||||
| **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 (D221) | 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. Replacing marks the server as managed by a site (D221). |
|
||||
| `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.profile <create\|set\|delete> ...` (console) | Edits a profile on a standalone server; refused when the site manages them (D221) | `runicnpc.admin` |
|
||||
| `/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).** Every placement also answers with the
|
||||
cost warning (D227): how many RunicNPC NPCs the server now has and what stage 1 measured that number to cost.
|
||||
|
||||
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` (D226).
|
||||
- **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. These numbers
|
||||
are the cost warning's (D227), so they are measured on both rigs and on the 6000 map.
|
||||
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; standalone profiles and the managed flag
|
||||
(D221); optional caps, off by default, and the cost warning from stage 1's measurements (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
|
||||
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
|
||||
(D224);
|
||||
- `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.
|
||||
- **Publishing (D224):** the Gitea release is the source of record. Other download sites (uMod, Codefling) are
|
||||
decided then, each against its own rules, for example uMod's review guidelines.
|
||||
- **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
|
||||
|
||||
**All eight were answered on 2026-09-30, as recommended except Q6 (D224 adds Gitea and other download sites)
|
||||
and Q7 (D227: no default caps, a cost warning instead). They are kept as asked.**
|
||||
|
||||
**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.**
|
||||
Reference in New Issue
Block a user