docs(runicnpc): stage 5 built, tested and walked; API 4; protocol 13 §19.13
- runicnpc/PLAN.md: stage 5 as built, what the build found that the spike did not (Rust's CanSeeTarget is no line of sight for an NPC target; a destroyed escort target compares equal to null; a rocket's line of sight), the harness results on both rigs, and the site walk; - runicnpc/API.md: API 4 (the faction table, escort, ally, tether, the profile's stage 5 fields, a placement's tether, OnRunicNpcEscortEnded, the new rows of RunicNpc_List, rnpc.faction, /rnpc follow, tether=); - rust-link/PROTOCOL.md §19.13: the faction table on npc.profiles and npc.profiles.set, world.place's escort, ally and tether, the three new world.error reasons; the bridge needs RunicNPC API 4. Joins protocol 13 (D253). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -8,7 +8,8 @@ decided with it. **Stage 1 measured 2026-09-30** (§9): six answers on both rigs
|
||||
(§0). **Stage 3 built and tested 2026-09-30** on both rigs (§9); its in-game walk waits for a player. **Stage 4's
|
||||
design answered 2026-09-30** (D243–D252, §0), **built and walked 2026-09-30** on both rigs (§9). **Stage 5's
|
||||
design answered 2026-09-30 and 2026-10-01** (D253–D266, §0), **its spike measured 2026-10-01** on both rigs (§9),
|
||||
and **its build's own questions answered 2026-10-01** (D267–D272).
|
||||
and **its build's own questions answered 2026-10-01** (D267–D272). **Stage 5 built and tested 2026-10-01, walked on the
|
||||
site 2026-10-05** on both rigs (§9); its API is [API.md](API.md) version 4.
|
||||
|
||||
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
|
||||
@@ -997,6 +998,82 @@ What this adds to the build (D263–D266):
|
||||
RunicNPC itself.
|
||||
6. **Where a tether is set. → D272:** on a placement and on an event step, not on a profile.
|
||||
|
||||
**Built and tested (2026-10-01).** Five PRs and this one, on `edge` (D226) except `docs`: runicnpc-rust
|
||||
(API 4), Rust-Plugins (the bridge), Module-Rust (the site), and `docs`. Rust-Link needs nothing: it forwards
|
||||
`npc.profiles.set` and `world.place` as they come. The installer needs nothing either: it reads the API floor
|
||||
from each release's manifest, and the bridge's is now 4.
|
||||
|
||||
| Piece | As built |
|
||||
|---|---|
|
||||
| Profiles | `role` gains `guard`. New: `faction`, `relations` (its own exceptions), `alertRadius` (40), `turrets` (`default`), `hurtByPlayers` and `hurtsPlayers` (true), `kitUse` (all off). With none set, a profile fights players only (D255). [API.md](API.md) has the rules. |
|
||||
| The faction table | In `profiles.json` beside the profiles, one row per pair, both ways (D268). `RunicNpc_Factions` and `RunicNpc_SetFactions` read and replace it; `rnpc.faction` edits it on a standalone server. On the site: one table for the whole site, on the NPC profiles page, pushed to every server with its profiles, and a standalone server's own pairs adopted at its first push, the site's winning (D244, D251). |
|
||||
| Whom it fights | Rust's design still owns every fight with a player. Ours senses only the factions it hunts, from a registry kept as scientists, animals and ours spawn and die, within its sense range of itself **and its chase range of its spot** (D263); the nearest three get a line-of-sight test, once a second. Its target is made `Horror` for the length of its own shot only (the spike's fix and its nesting guard). |
|
||||
| Shooting back, the alarm | Whoever shoots it is fought back, whatever the table says (D268): an NPC, an animal or a turret by our fight, a player by Rust's own. Allies within the alert radius learn the attacker, seen or not (D256). |
|
||||
| Allies | `RunicNpc_Ally`: a clan (the game's clan id), a team, or a player and that player's team (D270). Their people are never targets (our NPC re-implements `IAISenses.IsTarget`), and whoever damages them or what they own (building blocks, doors, deployables) within its leash is fought (D257), from `OnEntityTakeDamage`, which does nothing while no NPC has an ally or an escort. |
|
||||
| Escort | `RunicNpc_Escort` and `/rnpc follow`: an order any roamer or guard takes (D269). Its roam state follows the charge (within 4 m, running past 12 m), its leash and home follow the charge, and when the charge dies, is destroyed or leaves, it walks back to its spot and `OnRunicNpcEscortEnded` is raised. |
|
||||
| The guard | Holds its spot facing the way it was placed, chases within its chase range, and walks back. |
|
||||
| The tether | `RunicNpc_Tether`, a placement's `tether` and `/rnpc place tether=` (D272): ZoneManager's `IsPositionInZone` bounds its wander, chase and fight, and outside the zone it forgets the fight and walks home. A placement whose zone has gone waits (D237). |
|
||||
| Turrets | `ignore`: `CanBeTargeted` answers false. `always`: the Harmony patch on `NPCAutoTurret.Ignore` and `IsEntityHostile` (D267), patched at load and reported by `rnpc.status`. `default` needs nothing. |
|
||||
| The kit's extras | Each opted in, each used up (D260, D265): a syringe, medkit or bandage below half health after 3 s unhurt; melee within 2.5 m; a flamethrower within 7 m, burning the kit's fuel once its own tank is empty; Rust's own grenade throw at 5–20 m; a rocket at 15–80 m on an arc, launched by RunicNPC (Rust's AI never fires one). The main weapon (the belt's first slot) is always used and never runs out. |
|
||||
| PVE | `CanEntityTakeDamage` answers true both ways with players unless a profile turns a direction off (D261). "Players can hurt it" off zeroes the damage in its own `Hurt`; "it can hurt players" off cancels the damage in `OnEntityTakeDamage`. RunicNPC enforces both itself, so NextGenPVE's ignoring of "deny" does not matter (D271). |
|
||||
| The bridge | `npc.profiles` and `npc.profiles.set` carry `factions`; `world.place` with a profile takes `escort`, `ally` and `tether`, all or nothing ([PROTOCOL.md §19.13](../rust-link/PROTOCOL.md)). `runicnpc_api = 4`. |
|
||||
| The site | The profile form's Sides, Turrets, PVE and kit-extras sections and the guard role; the faction table; the Place NPCs step's `escort`, `allyClan` (from the new `rust.options.clans` source), `allyTeamOf` and `tether`; the placement form's "keep inside zone". |
|
||||
| Cost | The cost warning gains a line for NPCs that fight NPCs (D227). `rnpc.status` reports what our own fight costs a think: **0.27 ms** for the fight and **0.60 ms** a sense over a full test run on the Oxide rig, the spike's order. |
|
||||
|
||||
**Three things the build found that the spike did not:**
|
||||
|
||||
1. **Rust's `CanSeeTarget` is no line of sight for an NPC target.** Its rays are unbounded, so on open ground
|
||||
they run past the target into the terrain behind it, and a probe NPC walked right up to a scientist it had
|
||||
been shooting at from 30 m. Our fight uses a bounded line to the target's centre instead. The spike's
|
||||
prototype used Rust's, and its fights were all at 15 m.
|
||||
2. **A destroyed escort target compares equal to null** (Unity's own `==`), so the first build never noticed a
|
||||
charge that was killed. It now tests the reference itself.
|
||||
3. **A rocket aimed at a player needs Rust's own sensed line of sight**, and one aimed at an NPC needs ours:
|
||||
an NPC is never in Rust's player memory.
|
||||
|
||||
**Tested by `tools/RunicNpcTest.cs` 0.5.0** (`rnt.run s5`, 52 checks), and with stages 2–4 again (`rnt.run all`):
|
||||
|
||||
| | Oxide | Carbon |
|
||||
|---|---|---|
|
||||
| `rnt.run all` (stages 2–5) | 206/207 | 206/207 |
|
||||
| `rnt.run s5` alone, after the last fix | **52/52** | **52/52** |
|
||||
|
||||
What `s5` covers: the faction table's four refusals; red and blue (hostile in the table) both hit; a hunter
|
||||
of `scientists` kills one; a profile with no faction settings beside a scientist never fights it (D255);
|
||||
a leash of 8 m never reaches a scientist 25 m off (D263); a profile shot by another fights back (D268); allies
|
||||
within 40 m learn a shooter and one 60 m off does not (D256); an ally player is never targeted while the same
|
||||
profile allied to nobody targets them, and a raider of the ally's person or foundation is learned (D257); an
|
||||
escort follows its charge 35 m and walks home when it dies, raising the hook (D269); a guard returns to its post
|
||||
and holds it; a 12 m zone holds a 15 m wanderer, a zone that does not exist is refused, and an NPC put outside
|
||||
walks back in (D272); the PVE answers and both opt-outs (D271); `ignore` through the hook; **the map's own
|
||||
sentries**: an `always` NPC is targeted and killed, a `default` one beside it is untouched, and **no sentry on
|
||||
the map ever targeted a stock scientist or bandit guard** (the org lead's condition, D267, sampled four times a
|
||||
second across all 85–94 sentries); the five kit extras each used and used up, and none used by a profile that
|
||||
did not opt in; and **no entity is left `Horror`** afterwards.
|
||||
|
||||
The one failure in each full run was the escort test's layout, not the escort: it walked the charge towards the
|
||||
stand-in that keeps the field awake, and Rust's design then fought that player instead of walking home. Walked east
|
||||
instead, it passes; a probe showed the escort home 0.4 m from its spot 16 s after its charge died. Stages 2–4 were
|
||||
unchanged on both frameworks.
|
||||
|
||||
**Walked on the site (2026-10-05)**, signed in as an admin at the walk core: the NPC profiles page with its new
|
||||
sections and faction table; **bandits ↔ guards: hostile** saved and pushed, and `rnpc.faction list` on the Oxide rig
|
||||
showing it; the Bandit profile given a faction, `scientists: hostile`, a 30 m alert radius, `turrets: ignore` and
|
||||
the heal extra, and `rnpc.profile show bandit` holding exactly that; the Carbon guard made a `guard` of faction
|
||||
`guards` and pushed; a placement given a zone the server lacks, refused on the form in RunicNPC's words; and an
|
||||
event (a zone, then two guards with `tether` and `allyTeamOf`) whose NPCs on the Carbon rig carried faction
|
||||
`guards`, ally `player:76561198000000002` and the run's own zone as their tether, and were given back at
|
||||
teardown. An escort who was not on the server failed the step with the bridge's sentence and was retried, as
|
||||
`escort-offline` is. **Not walked:** a clan ally with a live clan (neither rig has one now; the picker correctly
|
||||
offers none), and anything that needs a player in game (escort and a clan's defence for real, the org lead's
|
||||
walk, as the plan says).
|
||||
|
||||
**Stand-ins are poor players for Rust's own senses**, which the harness works around rather than fixes:
|
||||
Rust often notices only one of two stand-ins close together, and `CanSeeTarget` never sees one. The checks
|
||||
about what happens once a player is known hand the stand-in to the NPC's memory with Rust's own `SetKnown`,
|
||||
which asks our `IsTarget`. An NPC more than 160 m from every stand-in sleeps (D235), which is correct and which
|
||||
the first runs mistook for "no fight".
|
||||
|
||||
### Stage 6 — Roles
|
||||
|
||||
Bosses (a health bar drawn with CUI, phases at thresholds that swap profile values, announcements through the bridge
|
||||
|
||||
Reference in New Issue
Block a user