Files
docs/runicnpc/API.md
wtclaude 2ad2043060 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
2026-09-30 08:45:21 -05:00

273 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RunicNPC — the API
**API version 3** (RunicNPC stage 4, 2026-09-30). This is the reference for other plugins. Why it has this
shape is in [PLAN.md](PLAN.md) §4 and the decisions D221–D238 and D249. **API 3 added** `RunicNpc_AddPlacement`,
`RunicNpc_RenamePlacement`, `RunicNpc_RespawnPlacement` and the hook `OnRunicNpcPlacementChanged`, for a
website that lists, edits and creates placements (PLAN.md stage 4); nothing of API 2 changed.
RunicNPC is called by name through `Call`, like any Oxide or Carbon plugin API. Every call is prefixed
`RunicNpc_`, so no hook of another plugin is ever matched by accident. Every call is a non-public method, which is
what Oxide's `Call` reaches by name (PLAN.md §1.2).
```csharp
[PluginReference] private Plugin RunicNPC;
int api = RunicNPC?.Call<int>("RunicNpc_ApiVersion") ?? 0;
if (api < 3) { /* too old for this caller: say so */ }
BasePlayer npc = RunicNPC.Call("RunicNpc_Spawn", position, "warden", "plugin:MyPlugin", null) as BasePlayer;
```
**The version moves when a call or a raised hook changes shape**, not on every release. `plugin.toml` in the
repository declares the same number, and the release's `manifest.json` carries it, so an installer or the
bridge can refuse a RunicNPC that is too old before it is loaded.
A call that cannot do what it was asked **returns null (or 0, or false) and says why in the server log**. Calls
that change data return the reason as a string instead, and null means success.
---
## Owners
Every NPC has an owner, and the owner decides its lifetime (PLAN.md §2).
| Owner | Who uses it | Lifetime |
|---|---|---|
| `run:<id>` | An event run, through the bridge | Never saved. Removed by the caller, or by `RunicNpc_DespawnOwner`. |
| `plugin:<name>` | Any other plugin | Never saved. **Removed when that plugin unloads.** Use your plugin's own name. |
| `placement:<id>` | RunicNPC itself | The NPC is never saved; the placement is, and spawns a fresh one at boot. Callers cannot spawn with this owner. |
---
## Calls
### `RunicNpc_ApiVersion()` → `int`
The API version: `3`.
### `RunicNpc_Spawn(Vector3 at, string profile, string owner, JObject overrides)` → `BasePlayer`
Spawns one NPC now, or returns null.
- `owner` must be `run:<id>` or `plugin:<name>`.
- `overrides` may change any profile value for this NPC only, in the profile's own shape (below). It may be null.
For example: `{"health": 500, "names": ["Captain"], "movement": {"mode": "route:gate", "radius": 0}}`. Arrays
are replaced, not merged. The merged profile is validated like a saved one.
- A roamer must stand on Rust's navmesh: within 3 m across and 2 m up or down of `at`. A sentry may stand
anywhere.
- It is refused until the map's navmesh is built, which takes 8–10 minutes on a map's first boot.
Refusals: an unknown or refused profile, a bad owner, a failed override, a spot off the navmesh, a cap an admin has
set, or a navmesh that is not built yet.
### `RunicNpc_Despawn(ulong netId)` → `int`
Removes one NPC by its net id. Returns 1, or 0 if it is not one of RunicNPC's. A placement's NPC comes back after
its respawn delay.
### `RunicNpc_DespawnOwner(string owner)` → `int`
Removes every NPC of `owner`, and returns how many it removed.
### `RunicNpc_List(string owner)` → `List<Dictionary<string, object>>`
The live NPCs of `owner`; null lists them all. Each has:
| Key | Type | |
|---|---|---|
| `netId` | `ulong` | |
| `profile`, `name`, `owner`, `kit` | `string` | The kit is the one picked for this NPC. |
| `placement` | `string` | Its placement's id, or null. |
| `position`, `home` | `{x, y, z}` | `home` is where it spawned: where it wanders around, and walks back to before it sleeps. |
| `health`, `maxHealth` | `float` | |
| `movement` | `string` | `wander`, `monument` or `route:<name>`. |
| `rest` | `string` | `Awake`, `GoingHome` or `Asleep` (D235). |
| `state` | `string` | Rust's AI state: `Idle`, `Roam`, `Chase`, `Combat` and so on. |
| `leashes` | `int` | How many times it has given up a chase at its chase range. |
| `regrounded` | `int` | How many times its ground went from under it and it was put on the nearest navmesh (D239). |
### `RunicNpc_Profiles()` → `JObject`
`{"managed": bool, "profiles": {name: profile, …}, "refused": {name: reason, …}}`. A refused profile is kept in
the file but never spawns (a kit the server lacks, for example).
### `RunicNpc_SetProfiles(JObject all)` → `Dictionary<string, string>`
Replaces the whole profile set with `all` (`{name: profile, …}`), as a site does on every push, and **marks the
server managed** (D221). It returns the profiles refused and why; the rest are in use at once. A placement whose
profile has gone waits, and spawns again when the profile returns (D237).
### `RunicNpc_Placements()` → `List<Dictionary<string, object>>`
Each placement as `{id, placement, alive, waiting, lastError, note}`. `placement` is the placement's JSON (below),
`alive` is how many of its NPCs are alive, and `waiting` says why it cannot spawn right now, or is null. `note` is
set while its NPCs stand somewhere other than its spot because the ground there went (D239, below), or is null.
### `RunicNpc_SetPlacement(string id, JObject placement)` → `string`
Adds or replaces a persistent placement (D222). Null on success, or the reason. Replacing one despawns its NPCs and
spawns them again from the new values. Every set logs the cost warning (D227).
A roamer's spot must be on Rust's navmesh, within 2 m up or down, when it is set; otherwise the answer says how far
off it is, as `rnpc place` does (D219). The check is skipped while the map's navmesh is still being built, and
for a profile that does not exist yet. **If the ground under a placement goes later** (a player-built floor is
destroyed, D239), a live NPC is put on the nearest navmesh within 2 s, and the placement respawns its NPCs on the
nearest navmesh until its spot is walkable again; `note` says so meanwhile. Rust itself leaves such an NPC standing
in the air.
### `RunicNpc_RemovePlacement(string id)` → `bool`
Removes a placement and its NPCs.
### `RunicNpc_AddPlacement(JObject placement)` → `Dictionary<string, object>`
API 3. **Creates a placement and names it as `rnpc place` does**: the profile's name and the first free
number, `warden-3` (D241, D246). It is checked and refused exactly as `rnpc place` is, with the same
sentences, so a placement made from a website and one made in game can never disagree.
- `placement` is a placement (below) without an id. **A `position` without `y` is a point on a map**
(D245): it is put on the ground there by a ray down on terrain and rock, never a building or a tree (a
roof is placed in game), then checked against the navmesh like any other. A point off the map, or under
water, is refused.
- The answer is `{id, position, built, cost}`: the name given, where it landed, whether that spot is on
something players built (D239's warning), and the cost warning (D227). Or `{error}`, the sentence
`rnpc place` would have said.
### `RunicNpc_RenamePlacement(string from, string to)` → `string`
API 3. Renames a placement, as `rnpc rename`: its live NPCs keep living under the new name. Null on
success, or why not (no such placement, the name taken, not a name).
### `RunicNpc_RespawnPlacement(string id)` → `int`
API 3. Removes a placement's NPCs and spawns them again at once, as `rnpc respawn`. How many, or `-1` if
there is no such placement.
### `RunicNpc_Routes()` → `JObject`
`{name: route, …}`.
### `RunicNpc_SetRoute(string name, JObject route)` → `string`
Adds or replaces a route (D234). Every point must be on Rust's navmesh. Null on success, or the reason. Unlike
`rnpc path` (D240), it does not check that an NPC can walk from each point to the next: a leg Rust cannot path
leaves the NPC standing at the last point it reached, so a caller that builds routes should check its own legs.
### `RunicNpc_RemoveRoute(string name)` → `bool`
Removes a route. Placements that walk it wait until it exists again.
### `RunicNpc_IsRunicNpc(BaseEntity entity)` → `bool`
Whether `entity` is one of RunicNPC's NPCs.
### `RunicNpc_ProfileOf(BaseEntity entity)` → `string`
Its profile's name, or null.
### `RunicNpc_CostWarning(int adding)` → `string`
The cost warning (D227) for the NPCs the server plans, plus `adding`. It is built from stage 1's measurements
(PLAN.md §9), and says what the NPCs add to a server frame, idle and fighting, and how slowly each then reacts. A
caller that adds NPCs shows it to whoever added them.
---
## Hooks it raises
All are called on every plugin, and **none of them answers**: a return value is ignored.
| Hook | When |
|---|---|
| `OnRunicNpcSpawned(BasePlayer npc, string profile, string owner)` | After an NPC spawns and is equipped. |
| `OnRunicNpcHealth(BasePlayer npc, string profile, float threshold)` | The first time its health falls to one of the profile's `healthThresholds`. |
| `OnRunicNpcDied(BasePlayer npc, string profile, string owner, HitInfo info, Dictionary<ulong, float> contributors)` | When it dies. `contributors` maps each player's Steam id to the health that player took from it, the killing blow included, after Rust's protection. It never adds up to more than its health. |
| `OnRunicNpcDespawned(BasePlayer npc, string owner)` | When it is removed without dying. Not raised as RunicNPC itself unloads. |
| `OnRunicNpcPlacementChanged(string id, string change, string previous)` | API 3. Whenever a placement is set, removed or renamed, through the API or in game. `change` is `set`, `removed` or `renamed`; `previous` is the old name for a rename and null otherwise. |
---
## Data shapes
### A profile (D238)
```json
{
"names": ["Warden", "Old Warden"],
"kits": ["warden_rifle", "warden_smg"],
"prefab": "scientistnpc_roam",
"role": "roamer",
"movement": { "mode": "wander", "radius": 20 },
"health": 250,
"damageDealt": 1.0,
"damageTaken": { "head": 1.0, "body": 1.0, "legs": 1.0 },
"aimCone": 2.0,
"ranges": { "sense": 30, "loseTarget": 40, "chase": 40, "attack": 30 },
"visionCone": -0.8,
"sleepDistance": 160,
"healthThresholds": [0.5]
}
```
| Field | Rule | Meaning |
|---|---|---|
| `names` | at least one | One is picked for each NPC. |
| `kits` | at least one; each must exist in Kits | One is picked for each NPC, and it is equipped with it (D217). |
| `prefab` | a Rust `scientistnpc_*` prefab that is a plain scientist | What it is built from. |
| `role` | `roamer` or `sentry` | A sentry never moves, and may stand off the navmesh. |
| `movement.mode` | `wander`, `monument` or `route:<name>` | D233. A placement may override it. |
| `movement.radius` | above 0 for `wander` | How far from home it wanders. |
| `health` | above 0 | Its health, and its maximum. |
| `damageDealt` | 0 or more | Multiplies its weapon's damage; 1 is the weapon's full damage. |
| `damageTaken` | each 0 or more | Multiplies the damage it takes to the head, the legs (legs and feet), and the body (everything else, including hits with no body part, like fire and explosions). |
| `aimCone` | 0 or more | How much its aim spreads; Rust's scientists use 2. |
| `ranges.sense` | above 0 | How far it notices players. |
| `ranges.loseTarget` | at least `sense` | How far a target must get before it is forgotten. |
| `ranges.chase` | 0 or more | How far from home it will chase (0: no limit). At that distance it waits 5 s for the target to come within range, then gives up and walks home. |
| `ranges.attack` | above 0 | How far it shoots from. This replaces the weapon's own range. |
| `visionCone` | −1 to 1 | Rust's vision cone. |
| `sleepDistance` | 0 or more | D235: with no player this close it walks home, then sleeps. 0: it never sleeps. |
| `healthThresholds` | fractions between 0 and 1 | Where `OnRunicNpcHealth` is raised. |
### A placement
```json
{
"profile": "warden",
"position": { "x": 100.0, "y": 12.5, "z": -340.0 },
"yaw": 90,
"count": 3,
"respawn": 300,
"respawnMode": "each",
"movement": { "mode": "route:gate", "radius": 0 }
}
```
- `count` NPCs spawn at the spot, and all but the first are scattered within 3 m of it.
- `respawn` is in seconds, at least 1. `respawnMode` is `each` (every NPC returns `respawn` after its own death)
or `group` (none returns until all are dead, then all return together) (D236).
- `movement` is optional and overrides the profile's.
### A route
```json
{ "points": [ { "x": 1, "y": 2, "z": 3 }, { "x": 4, "y": 2, "z": 6 } ], "loop": true }
```
At least two points, each on Rust's navmesh. With `loop`, the last point leads back to the first; without it, the
route is walked back and forth. An NPC joins the route at its nearest point.
---
## Files and config
- `data/RunicNPC/profiles.json` (`managed` and `profiles`), `placements.json` and `routes.json`, under `oxide/`
or `carbon/`. On a standalone server profiles are edited in the file, then `rnpc.reload` reads them again. On a
managed one the site pushes them.
- `config/RunicNPC.json`:
- `caps`: `total`, `perOwner`, `perProfile` and `spawnsPerSecond`. **Each is 0, meaning no cap, until an admin
sets it** (D227).
- `spawnBudgetMs`: how many milliseconds of each server frame placements may spend spawning. The default is 8.
- `rnpc.status` at the console reports the swap's field check, the NPCs by owner and rest, profiles refused,
placements waiting, the caps, and the cost warning.