docs(runicnpc): stage 4 built and walked; protocol 13 §19.12; API 3
PROTOCOL.md §19.12: RunicNPC over the bridge — integrations.runicNpc, the npc.profiles, npc.profiles.set, npc.placements and npc.placement commands and their refusals, the npc.died, npc.health and npc.placement.changed frames, world.place with a profile, attackerNpc and attackerProfile, the tally's npcProfileKills, the four sidecar routes, and overlay.toml's runicnpc_api. API.md: API 3 — RunicNpc_AddPlacement, RunicNpc_RenamePlacement, RunicNpc_RespawnPlacement and OnRunicNpcPlacementChanged. PLAN.md stage 4: what was built in five repositories, the walk on both rigs row by row, what was not walked and why, and what building it found. rust-link/INSTALL.md: RunicNPC as the bundle's third file, by hand, the egg, doctor and uninstall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -7,12 +7,13 @@ the design of record is [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §3
|
||||
## What this installs
|
||||
|
||||
Two components per Rust server, released together as a **bundle** — an exact pair CI has checked
|
||||
speaks one protocol, never "the latest of each":
|
||||
speaks one protocol, never "the latest of each" — and, from RunicNPC's stage 4, a third:
|
||||
|
||||
| Component | What it is | Released from |
|
||||
|---|---|---|
|
||||
| **The plugin** | `RunicGateway.cs`, one file that runs unchanged on Oxide and Carbon — and beside it, from protocol 13, the optional **ZoneManager helper** `RunicGatewayZones.cs` (PLAN_FIXES D181, D182), installed by default | [Rust-Plugins](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins/releases) |
|
||||
| **The sidecar** | `rust-link-sidecar`, which the plugin dials on loopback and the website reaches over HTTP | [Rust-Link](https://gitea.whitlocktech.com/RunicGateway/Rust-Link/releases) |
|
||||
| **RunicNPC** | `RunicNPC.cs`, Runic Gateway's NPC plugin ([`../runicnpc/PLAN.md`](../runicnpc/PLAN.md)), placed beside the bridge **when the bundle carries it**: from RunicNPC's stage 4, the latest RunicNPC release that answers the API the bridge needs (D224). Optional until RunicNPC's stage 9; it needs **Kits**, like the bridge | [runicnpc-rust](https://gitea.whitlocktech.com/RunicGateway/runicnpc-rust/releases) |
|
||||
|
||||
The game server opens no port for the bridge: the plugin is the client and the sidecar the
|
||||
listener, on `127.0.0.1`. **One sidecar serves one Rust server.** A community running six servers
|
||||
@@ -121,7 +122,8 @@ When you create a server from it:
|
||||
|
||||
**The install** downloads the sidecar, its launcher and the plugin from the bundle, checks each
|
||||
against the bundle's checksum, and only then places them: the sidecar in `rust-link/`, the plugin in
|
||||
`oxide/plugins/` or `carbon/plugins/`. Any mismatch fails the install with the reason, before
|
||||
`oxide/plugins/` or `carbon/plugins/`. When the bundle carries RunicNPC, `RunicNPC.cs` goes beside the plugin
|
||||
(its data directory is left for RunicNPC to make). Any mismatch fails the install with the reason, before
|
||||
anything is placed.
|
||||
|
||||
**The first boot** prints, in the console, the token the sidecar generated — **once** — and a line
|
||||
@@ -154,7 +156,10 @@ history, and the token is unchanged.
|
||||
[`v2/rust/current.json`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/bundles/v2/rust/current.json)
|
||||
on the installer's `bundles` branch. It names the sidecar binary for your platform and the
|
||||
plugin tarball, each with a `sha256`.
|
||||
2. **Download and verify** both against those checksums (`sha256sum -c`, or `Get-FileHash`).
|
||||
2. **Download and verify** both against those checksums (`sha256sum -c`, or `Get-FileHash`) — and, when
|
||||
the bundle has an `npc` entry, RunicNPC's tarball too. Copy `runicnpc/RunicNPC.cs` from it into the same
|
||||
plugins directory as the bridge. **Do not create `data/RunicNPC/` yourself**: RunicNPC makes it on first
|
||||
load, and one made from outside the game (a panel's file manager) is not writable by it.
|
||||
3. **The plugin:** copy `runicgateway-rust-plugin/RunicGateway.cs` from the tarball into
|
||||
`oxide/plugins/` or `carbon/plugins/`, and every other `.cs` the tarball's `manifest.json` lists
|
||||
in `files` beside it — from protocol 13 that is `RunicGatewayZones.cs`, which lets ZoneManager count a
|
||||
@@ -220,9 +225,9 @@ With the installer:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `doctor --game rust [--server-id <id>]` | Per server: the framework; whether the plugin file is still the one deployed; each helper deployed beside it, as a **warning** when missing or edited (the bridge runs without one, and the row says what that costs); that the plugin's config names this server; the required uMod plugins; the service; and `/health` through to **plugin connected**. A stopped server is a warning; a running one whose plugin never connected is a failure, printed with the framework versions the plugin is known good on |
|
||||
| `doctor --game rust [--server-id <id>]` | Per server: the framework; whether the plugin file is still the one deployed; each helper deployed beside it, as a **warning** when missing or edited (the bridge runs without one, and the row says what that costs); RunicNPC, when the bundle carried it, the same way (without it the site's NPC profiles and placements and events' profile NPCs are off); that the plugin's config names this server; the required uMod plugins; the service; and `/health` through to **plugin connected**. A stopped server is a warning; a running one whose plugin never connected is a failure, printed with the framework versions the plugin is known good on |
|
||||
| `update --game rust` | Moves the sidecar and every server's plugin to the current bundle, and restarts the sidecars. Always all servers together — they share one binary |
|
||||
| `uninstall --game rust [--server-id <id>] [--purge]` | Removes the service and the plugin file. **Keeps the plugin's config** — it is the website's, and it names the server. `--purge` also removes the sidecar config (the token) and the database. Removing the last server removes the shared binary too |
|
||||
| `uninstall --game rust [--server-id <id>] [--purge]` | Removes the service, the plugin file, its helpers and RunicNPC. **Keeps the plugin's config** and RunicNPC's `data/RunicNPC/` (an admin's placements and routes) — it is the website's, and it names the server. `--purge` also removes the sidecar config (the token) and the database. Removing the last server removes the shared binary too |
|
||||
|
||||
With the egg: reinstall to update (above); the console is the diagnosis.
|
||||
|
||||
|
||||
@@ -1437,7 +1437,7 @@ five routes.
|
||||
|---|---|---|
|
||||
| `world.monuments` | `world.monuments` | This map's monuments in one stable order (grouped by prefab short name, then by position). Each has `value` (`kind`, or `kind#n` when the kind repeats), `kind`, `instance`, `of`, `label` (the game's display phrase), `x`, `z` and `grid`. Also `worldSize`, the placeable `prefabs` (`key`, `kind`, `label`), `eventsEnabled`, `maxCrates`, `maxNpcs` and `zoneManager` |
|
||||
| `world.zone` | `world.ok` or `world.error` | Opens a ZoneManager temporary zone owned by the bridge. Needs `runId`, `key`, a location, `radius` (5–150 m) and `holdMs` (1 minute to 7 days); `name` is optional. Protocol 13 adds `flags`, `settings`, `enterMessage`, `leaveMessage`, `delivery`, `format` and `dome` (§19.11) |
|
||||
| `world.place` | `world.ok` or `world.error` | Places `count` of one allowlisted `prefab` at a location, scattered within `spread` m (0–50, 10 by default for a group). All or nothing: if the game refuses one, the ones already made are killed |
|
||||
| `world.place` | `world.ok` or `world.error` | Places `count` of one allowlisted `prefab` at a location, scattered within `spread` m (0–50, 10 by default for a group). All or nothing: if the game refuses one, the ones already made are killed. Protocol 13 adds `profile`, a RunicNPC profile in place of a prefab (§19.12) |
|
||||
| `world.revert` | `world.ok` | Gives back what a run owns: the named `ids`, or else everything under `key`, or else everything the run owns. The answer lists `removed`, `gone` and `refused` |
|
||||
| `world.owned` | `world.owned` | What the world still holds of what events made, **looked for** by net id or zone id, narrowed by `runId`. Anything gone is pruned from the registry as the walk passes it |
|
||||
|
||||
@@ -2383,3 +2383,118 @@ last hello when it is saved and in a dry run; `bad-option` and `dome-unavailable
|
||||
ZoneManager and ZoneDomes reloads, a server restart and the forced opposite boot order (exactly one set of
|
||||
spheres each time), revert and expiry removing the dome, and a dome refused without the helper. The
|
||||
in-game rows (flags on a player, the messages) wait for the later in-game walk.
|
||||
|
||||
### 19.12 RunicNPC: the site's profiles and placements (runicnpc PLAN.md stage 4)
|
||||
|
||||
RunicNPC is Runic Gateway's own NPC plugin ([`../runicnpc/PLAN.md`](../runicnpc/PLAN.md), [`API.md`](../runicnpc/API.md)).
|
||||
**The bridge is its only link to the site**: it calls RunicNPC's API for the site's commands and turns
|
||||
RunicNPC's hooks into frames, and RunicNPC never talks to the sidecar, so "the sidecar is a dumb forwarder"
|
||||
stays true. RunicNPC is optional until its stage 9. Without it every `npc.*` command answers `npc.error`
|
||||
**`runicnpc-missing`**, and an event places Rust's own scientists only (D243).
|
||||
|
||||
The bridge needs **RunicNPC API 3** (D249), `RunicNpcApiNeeded` in the code and **`runicnpc_api = 3` in
|
||||
`overlay.toml`**, which the release copies into its manifest and `checkPlugin.js` holds equal to the code.
|
||||
An older RunicNPC answers `npc.error` **`runicnpc-old`**.
|
||||
|
||||
**Hello and status.** `integrations` gains `runicNpc`, like the other optional mods:
|
||||
|
||||
```json
|
||||
"runicNpc": { "loaded": true, "version": "0.2.0", "api": 3 }
|
||||
```
|
||||
|
||||
`api` is present only while RunicNPC is loaded. **The bridge sends hello again when RunicNPC loads or
|
||||
unloads.**
|
||||
|
||||
**Four commands.** Each answers a reply of its own kind, or `npc.error` with a `reason` and RunicNPC's own
|
||||
sentence in `message`.
|
||||
|
||||
| Command | Answers | |
|
||||
|---|---|---|
|
||||
| `npc.profiles` | `npc.profiles` | `managed`, `profiles` (name → profile, D238) and `refused` (name → why). The site reads it before its **first** push to a server, to adopt the server's own profiles (D244) |
|
||||
| `npc.profiles.set` | `npc.ok` | `{"profiles": {name: profile, …}}` replaces the whole set, as a push does, and marks the server managed (D221). The answer's `refused` names each profile RunicNPC would not use, and why (a kit the server lacks) |
|
||||
| `npc.placements` | `npc.placements` | Every placement: `id`, `placement` (its values), `alive`, `waiting` (D237), `note` (D239), `lastError`. Also `routes`, the names a placement may walk, and `cost`, the cost warning for what the server plans now (D227) |
|
||||
| `npc.placement` | `npc.ok` | One change to one placement, by **`op`**, below |
|
||||
|
||||
`npc.placement`'s ops:
|
||||
|
||||
| `op` | Carries | Answers |
|
||||
|---|---|---|
|
||||
| `add` | `placement`: `profile`, `position` **x and z only**, and `/rnpc place`'s options (D246): `count`, `respawn`, `respawnMode`, `movement` | `id` (named by RunicNPC as in game, `<profile>-<n>`, D241), `position` (where it landed), `built`, `cost` |
|
||||
| `set` | `id`, `placement` (a whole x, y, z moves it) | `id`, `cost` |
|
||||
| `remove` | `id` | `id` |
|
||||
| `rename` | `id`, `to` | `id` (the new), `previous` |
|
||||
| `respawn` | `id` | `id`, `respawned` |
|
||||
|
||||
**A map point is put on the ground by RunicNPC** (`RunicNpc_AddPlacement`, D245): a ray down on terrain
|
||||
and rock, never a building or a tree, so a roof is placed in game. Then it is checked against the navmesh
|
||||
exactly as `/rnpc place` is, and a refusal is RunicNPC's own sentence: off the map, under water, off the
|
||||
navmesh (`A roamer cannot stand there: …`), no such profile or route.
|
||||
|
||||
| `reason` | Means |
|
||||
|---|---|
|
||||
| `runicnpc-missing` | RunicNPC is not loaded |
|
||||
| `runicnpc-old` | RunicNPC answers an API older than 3 |
|
||||
| `malformed` | a field is missing or `op` is unknown |
|
||||
| `not-found` | no placement of that id |
|
||||
| `refused` | RunicNPC would not; `message` says why |
|
||||
|
||||
**Three events,** from RunicNPC's hooks, all **staff** class on the site:
|
||||
|
||||
```json
|
||||
{"kind":"npc.died","type":"event","netId":"973440","profile":"bandit","name":"Road Bandit",
|
||||
"owner":"placement:bandit-1","placement":"bandit-1","x":-2123.7,"z":2282.7,
|
||||
"killerId":"76561198000000002","killerName":"Marisol","weapon":"rifle.ak.entity",
|
||||
"contributors":[{"steamId":"76561198000000002","name":"Marisol","damage":180}]}
|
||||
{"kind":"npc.health","type":"event","netId":"973440","profile":"bandit","name":"Road Bandit",
|
||||
"threshold":0.5,"health":78.3,"maxHealth":180,"x":-2123.7,"z":2282.7}
|
||||
{"kind":"npc.placement.changed","type":"event","id":"gate","change":"renamed","previous":"warden-1"}
|
||||
```
|
||||
|
||||
- `owner` is RunicNPC's: `run:<id>`, `placement:<id>` or `plugin:<name>`. An event's NPC also carries
|
||||
**`runId`**, a placement's **`placement`**. `killerId` and `killerName` are absent when no player landed
|
||||
the killing blow. `contributors` is every player who took health from it, most first.
|
||||
- `npc.health` is sent the first time an NPC falls to one of its profile's `healthThresholds`.
|
||||
- `npc.placement.changed` is sent on every set, remove and rename, from the site or in game (`change` is
|
||||
`set`, `removed` or `renamed`), so an edit made in game reaches the site at once. The site reads the list
|
||||
again rather than trusting a diff.
|
||||
|
||||
**`world.place` takes a `profile`** instead of a `prefab` (D243): the NPCs are RunicNPC's, owned by the run
|
||||
(`run:<runId>`), and recorded in the registry with `prefab` `runicnpc:<profile>`. Everything else is as
|
||||
§15.1 says: the same bounds (`EventsMaxNpcs`), all or nothing, a repeated key answered with the first
|
||||
call's ids. A scattered point RunicNPC refuses (off the navmesh) is tried again three times, then at the
|
||||
centre. `world.revert` removes such an NPC **through RunicNPC** (`RunicNpc_Despawn`), which raises its own
|
||||
despawn hook. Two new `world.error` reasons, both permanent: **`runicnpc-missing`** and
|
||||
**`unknown-profile`** (the server has no such profile, or RunicNPC refuses it). A placement that names both a
|
||||
prefab and a profile is `malformed`.
|
||||
|
||||
**The killfeed names RunicNPC's NPCs** (runicnpc PLAN.md §1.4). A `player.death` whose attacker is one of
|
||||
them gains **`attackerNpc`** (its own name, as the victim's death screen shows it) and
|
||||
**`attackerProfile`**, **beside** `attackerName`, which stays the prefab so a reader that knows only prefab
|
||||
names still reads a scientist.
|
||||
|
||||
**`player.tally` gains `npcProfileKills`**, the player's kills of RunicNPC's NPCs by profile name
|
||||
(`{"warden": 2}`). They are counted in `npcKills` as well.
|
||||
|
||||
**Hooks.** `OnRunicNpcDied`, `OnRunicNpcHealth` and `OnRunicNpcPlacementChanged` join `rg.hooks`' list.
|
||||
`rg.npc` prints RunicNPC's version and API and the last `npc.*` command.
|
||||
|
||||
**The sidecar** forwards four routes, each a correlated round trip that fails while the game is down:
|
||||
|
||||
| Route | Command |
|
||||
|---|---|
|
||||
| `GET /npc/profiles` | `npc.profiles` |
|
||||
| `POST /npc/profiles` | `npc.profiles.set` (the body's `cmd` and `reqId` written over, as every forward) |
|
||||
| `GET /npc/placements` | `npc.placements` |
|
||||
| `POST /npc/placement` | `npc.placement` |
|
||||
|
||||
The three events are filed and served like every other (§8.1).
|
||||
|
||||
**The website** (Module-Rust) authors the profiles (Admin → Rust NPC profiles) and pushes each server its
|
||||
set on a loop of its own. Before the first push to a server it reads `npc.profiles`, and a standalone
|
||||
server's profiles are adopted as profiles for that server alone; one whose name a site profile already has
|
||||
there is kept aside as "replaced" (D244, D251). It lists and edits placements live (Admin → Rust NPC
|
||||
placements), a new one by clicking the live map (D245, D246). It adds the triggers `rust.npc.died` and
|
||||
`rust.npc.health`, which a phase can wait on, and stores `npcProfileKills` per profile for the leaderboard
|
||||
and titles (D247, D250, D252).
|
||||
|
||||
**Walked on both rigs, 2026-09-30** (runicnpc PLAN.md stage 4).
|
||||
|
||||
Reference in New Issue
Block a user