docs(runicnpc): stage 2 as built, and API.md #302
@@ -98,7 +98,8 @@ requires it once it releases (D220).
|
|||||||
|
|
||||||
| Doc | What it covers |
|
| 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 |
|
| [PLAN.md](runicnpc/PLAN.md) | **The plan** — what the 2026-09-30 spike found (route A, HumanNPC, NpcSpawn), the org lead's decisions D214–D238, the features, the API, the chat commands, and stages 0–10 with how each is tested and what each found |
|
||||||
|
| [API.md](runicnpc/API.md) | **The API** other plugins call (version 2): owners, every `RunicNpc_*` call, the hooks it raises, and the profile, placement and route shapes |
|
||||||
|
|
||||||
### `android/`
|
### `android/`
|
||||||
| Doc | What it covers |
|
| Doc | What it covers |
|
||||||
|
|||||||
234
runicnpc/API.md
Normal file
234
runicnpc/API.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# RunicNPC — the API
|
||||||
|
|
||||||
|
**API version 2** (RunicNPC stage 2, 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.
|
||||||
|
|
||||||
|
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 < 2) { /* 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: `2`.
|
||||||
|
|
||||||
|
### `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. |
|
||||||
|
|
||||||
|
### `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}`. `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.
|
||||||
|
|
||||||
|
### `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).
|
||||||
|
|
||||||
|
### `RunicNpc_RemovePlacement(string id)` → `bool`
|
||||||
|
|
||||||
|
Removes a placement and its NPCs.
|
||||||
|
|
||||||
|
### `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.
|
||||||
|
|
||||||
|
### `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. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
**Status:** plan, written 2026-09-30. Its eight questions (§11) were answered the same day: D221–D228 (§0).
|
**Status:** plan, written 2026-09-30. Its eight questions (§11) were answered the same day: D221–D228 (§0).
|
||||||
**Stage 0 closed 2026-09-30** (runicnpc-rust#1/#2, §9): v0.1.0 released and loaded on both rigs. D229–D230 were
|
**Stage 0 closed 2026-09-30** (runicnpc-rust#1/#2, §9): v0.1.0 released and loaded on both rigs. D229–D230 were
|
||||||
decided with it. **Stage 1 measured 2026-09-30** (§9): six answers on both rigs; D231 was decided with it.
|
decided with it. **Stage 1 measured 2026-09-30** (§9): six answers on both rigs; D231 was decided with it.
|
||||||
**Stage 2's design answered 2026-09-30:** D232–D238 (§0), which reshape stage 2 (§9).
|
**Stage 2's design answered 2026-09-30:** D232–D238 (§0), which reshape stage 2 (§9). **Stage 2 built and tested
|
||||||
|
2026-09-30** on both rigs (§9); its API is [API.md](API.md).
|
||||||
|
|
||||||
RunicNPC is Runic Gateway's own NPC plugin for Rust servers, in its own repository,
|
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
|
[`RunicGateway/runicnpc-rust`](https://gitea.whitlocktech.com/RunicGateway/runicnpc-rust). It runs on Oxide and
|
||||||
@@ -460,7 +461,7 @@ it. That is the other reason our NPC must never be saved (§2).
|
|||||||
| Health, name, aim cone | Hold: set on the NPC and read back (400/400, the name, 0.5). |
|
| Health, name, aim cone | Hold: set on the NPC and read back (400/400, the name, 0.5). |
|
||||||
| Sense range, target-lost range, vision cone, listen range, hostile-only, sense types | **Only if set before the brain starts.** `Senses.Init` copies them once. At 35 m, a range of 50 set before the start saw the target, and the same range set afterwards did not. |
|
| Sense range, target-lost range, vision cone, listen range, hostile-only, sense types | **Only if set before the brain starts.** `Senses.Init` copies them once. At 35 m, a range of 50 set before the start saw the target, and the same range set afterwards did not. |
|
||||||
| The prefab's own values | Sense 30 m, lose target 40 m, vision cone −0.8, listen 10 m, memory 10 s, line-of-sight checks on, hostile-only off, senses players only, health 150, aim cone 2. |
|
| The prefab's own values | Sense 30 m, lose target 40 m, vision cone −0.8, listen 10 m, memory 10 s, line-of-sight checks on, hostile-only off, senses players only, health 150, aim cone 2. |
|
||||||
| Roaming | **Rust's roam only follows an AI zone's move points.** In a monument's zone (Desert Military Base) ours roamed 36–47 m in 60 s. In an open field it stood still (0 m). **A roamer anywhere but a monument needs our own roam state, so stage 2's roamer role includes one.** |
|
| Roaming | **Rust's roam only follows an AI zone's move points** (and so does its chase: stage 2). In a monument's zone (Desert Military Base) ours roamed 36–47 m in 60 s. In an open field it stood still (0 m). **A roamer anywhere but a monument needs our own roam state, so stage 2's roamer role includes one.** |
|
||||||
| Sleep | An NPC outside an AI zone is never put to sleep, and `ai_dormant` does not apply to this AI. So an idle NPC keeps thinking with nobody near, which is what the costs below measure. |
|
| Sleep | An NPC outside an AI zone is never put to sleep, and `ai_dormant` does not apply to this AI. So an idle NPC keeps thinking with nobody near, which is what the costs below measure. |
|
||||||
| **Fighting NPCs** | **Rust's scientist AI never attacks an NPC.** `HumanNPC.IsTarget` is true only for non-NPC players, pets and scarecrows. `IsFriendly` means "same prefab id", which the swap copies, so every stock scientist counts ours as a friend. Re-implementing Rust's internal `IAISenses` puts a scientist in our NPC's target list. But Rust's AI design never runs its attack event for an NPC target. Calling `AttackTick` directly passed line of sight every time and still fired no shot, because the design's cover and facing states win. **NPC-versus-NPC combat needs our own combat state, and sensing NPCs needs our own sensing** (see the cost below). D231 makes it a profile setting, so stage 5 opens with its own spike. |
|
| **Fighting NPCs** | **Rust's scientist AI never attacks an NPC.** `HumanNPC.IsTarget` is true only for non-NPC players, pets and scarecrows. `IsFriendly` means "same prefab id", which the swap copies, so every stock scientist counts ours as a friend. Re-implementing Rust's internal `IAISenses` puts a scientist in our NPC's target list. But Rust's AI design never runs its attack event for an NPC target. Calling `AttackTick` directly passed line of sight every time and still fired no shot, because the design's cover and facing states win. **NPC-versus-NPC combat needs our own combat state, and sensing NPCs needs our own sensing** (see the cost below). D231 makes it a profile setting, so stage 5 opens with its own spike. |
|
||||||
|
|
||||||
@@ -475,7 +476,7 @@ boots load the saved `proceduralmap.<size>.<seed>.<n>.navmesh`. **Placements tha
|
|||||||
| Monuments probed (9 points each) | 144 | 144 |
|
| Monuments probed (9 points each) | 144 | 144 |
|
||||||
| Points on the navmesh | 1,152 / 1,296 (89%) | 1,151 / 1,296 (89%) |
|
| Points on the navmesh | 1,152 / 1,296 (89%) | 1,151 / 1,296 (89%) |
|
||||||
| Of those on a structure (a roof or raised floor) | 132 / 209 (63%) | 130 / 211 (62%) |
|
| Of those on a structure (a roof or raised floor) | 132 / 209 (63%) | 130 / 211 (62%) |
|
||||||
| A player-built floor 4 m up | **not on the mesh** | **not on the mesh** |
|
| A player-built floor 4 m up | **not on the mesh** in the frame it spawned; **on it 0.1–0.3 s later** (stage 2) | the same |
|
||||||
| One check | 13.6 µs | 8.3 µs |
|
| One check | 13.6 µs | 8.3 µs |
|
||||||
|
|
||||||
Monument roofs (Launch Site, Airfield, Trainyard, the warehouses) are mostly walkable. Player-built floors and
|
Monument roofs (Launch Site, Airfield, Trainyard, the warehouses) are mostly walkable. Player-built floors and
|
||||||
@@ -553,6 +554,46 @@ and reload checks on both rigs: no NPC saved, every placement back, nothing left
|
|||||||
also checks each movement mode, sleep and the walk home, both respawn modes, and a placement whose profile is
|
also checks each movement mode, sleep and the walk home, both respawn modes, and a placement whose profile is
|
||||||
deleted and restored.
|
deleted and restored.
|
||||||
|
|
||||||
|
**Built (2026-09-30, runicnpc-rust#4, API version 2).** The API as built is [API.md](API.md). What else landed:
|
||||||
|
|
||||||
|
- `tools/fieldlist` generates the swap's field list from the Carbon rig's assemblies (`tools/managed.js`
|
||||||
|
downloads them). It picks **64 NPC and 32 brain fields**, exactly stage 1's Carbon count. On both rigs every
|
||||||
|
name resolves, and Carbon's boot check finds no field Rust has added.
|
||||||
|
- `tools/RunicNpcTest.cs`, the harness: `rnt.run api|hooks|move|sentry|sleep|place|all`, then `rnt.after` after a
|
||||||
|
reload or restart.
|
||||||
|
|
||||||
|
| Group | What it proves | Oxide | Carbon |
|
||||||
|
|---|---|---|---|
|
||||||
|
| api | every call's answer; seven refusals (bad owner, `placement:` owner, unknown or refused profile, bad override, off the navmesh) | 24/24 | 24/24 |
|
||||||
|
| hooks | spawned, a health threshold once, body damage ×0.5, died with contributors (250 of 250), despawned | 8/8 | 8/8 |
|
||||||
|
| move | wander within its radius; a route in order (3>0>1>2>3>0); a monument roamer 36 m on Rust's roam; our chase to 9.6–9.8 m of a 10 m chase range, and giving up twice | 8/8 | 8/8 |
|
||||||
|
| sentry | holds 0.00 m for 40 s and hits a target 12 m off 29–34 times; stands 40 m up, off the navmesh, for 20 s | 7/7 | 7/7 |
|
||||||
|
| sleep | wanders 8.6 m off, walks home at ≤2.8 m/s when no player is within 160 m, sleeps 1.7 m from its spot, stays still, wakes | 7/7 | 7/7 |
|
||||||
|
| place | `each` returns only the dead one; `group` waits for all, then all return; a deleted profile waits and returns; a missing route waits | 14/14 | 14/14 |
|
||||||
|
| a plugin unloads | its NPCs are removed with it | pass | pass |
|
||||||
|
| RunicNPC reloads | every placement back, the world equals the registry, no plain scientists at the spots | 5/5 | 5/5 |
|
||||||
|
| the server restarts | the same, after `server.save`, a restart, and the navmesh load | 5/5 | 5/5 |
|
||||||
|
|
||||||
|
What building it found:
|
||||||
|
|
||||||
|
- **Outside a monument, Rust's scientists never chase.** Rust's chase state looks for an AI zone's move points and
|
||||||
|
returns an error without them, as its roam does (stage 1, Q3). A `wander` or `route` roamer therefore has our
|
||||||
|
own chase. It closes to three quarters of the attack range, never past the profile's chase range from home, and
|
||||||
|
if the target stays out of reach at that edge for 5 s it gives up and walks home. A `monument` roamer keeps
|
||||||
|
Rust's chase.
|
||||||
|
- **Rust's navmesh covers a new player-built floor in 0.1–0.3 s** (2–3 frames), on both rigs. Stage 1's "a
|
||||||
|
player-built floor is never on the mesh" (Q4) sampled in the same frame the floor spawned, so it is wrong: a
|
||||||
|
roamer can stand on a player-built floor a moment after it exists. §5's sentry rule, §8 and stage 10 were
|
||||||
|
written on stage 1's reading. **Open for the org lead.**
|
||||||
|
- **Rust's navmesh sampler reaches further down than across.** A roamer asked to stand on a roof found the
|
||||||
|
ground 4.7 m below. A roamer's spot must now be within 2 m, up or down, of the navmesh it is put on.
|
||||||
|
- **A brain can think before Unity has started it.** For that moment it has no navigator, so RunicNPC leaves that
|
||||||
|
think to Rust.
|
||||||
|
- **A hit with no body part reports every part at once** (`(HitArea)(-1)`: fire, explosions, falls). Only an
|
||||||
|
exact head or leg hit uses those scales; everything else is body.
|
||||||
|
- **The game manifest's entity list leaves out the NPC prefabs.** A profile's `prefab` is looked up in its prefab
|
||||||
|
list instead.
|
||||||
|
|
||||||
### Stage 3 — In game
|
### Stage 3 — In game
|
||||||
|
|
||||||
The chat and console commands (§5), their permissions, the navmesh placement check, respawn delays, and recording
|
The chat and console commands (§5), their permissions, the navmesh placement check, respawn delays, and recording
|
||||||
|
|||||||
Reference in New Issue
Block a user