docs, test: the update surface in the README, and rnt.cost (stage 9, D306)
- README: status is stage 9; the installer and egg install it now; API 6; the swap reports 55/55 npc fields; COMMANDS.md and INSTALL.md are linked; a new "update surface" table lists every Rust type and member the swap and the brain depend on, where, and what catches a change (compile, the field list, or only a running server). The staging drill checks exactly that list. - tools/RunicNpcTest.cs 0.7.0: rnt.cost measures 100 of RunicNPC's own NPCs against the same session's empty server, idle (awake) and fighting 20 invulnerable stand-ins, with a second empty baseline at the end and the bar taken against the lower. `idle` skips the fight, `sentry` makes them stand still, `asleep` leaves out the keeper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
66
README.md
66
README.md
@@ -7,9 +7,11 @@ works on its own, and on a [Runic Gateway](https://gitea.whitlocktech.com/RunicG
|
||||
website authors its NPC profiles and events use its NPCs. The plan of record, stage by stage, is
|
||||
[`docs/runicnpc/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/PLAN.md).
|
||||
|
||||
> **Status: stage 4.** The NPC, its profiles, placements and routes, the API other plugins call, and
|
||||
> the `/rnpc` commands admins use in game. On a Runic Gateway server, the website takes over the
|
||||
> profiles and lists, edits and creates placements through the bridge (API 3).
|
||||
> **Status: stage 9, hardening for v1.0.0.** The NPC, its profiles, placements and routes; factions,
|
||||
> fights, escorts and tethers; bosses and passive NPCs; loot. Admins use the `/rnpc` commands, other
|
||||
> plugins the API (version 6). On a Runic Gateway server RunicNPC is **required** (D310): the website
|
||||
> authors its profiles and faction table, edits its placements, and every NPC an event places is
|
||||
> RunicNPC's.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -22,8 +24,8 @@ website authors its NPC profiles and events use its NPCs. The plan of record, st
|
||||
## Installing it
|
||||
|
||||
On a Runic Gateway server, the [installer](https://gitea.whitlocktech.com/RunicGateway/installer)
|
||||
and the Pterodactyl egg will install it from the Rust bundle, pinned and checksummed (D224, from
|
||||
stage 4). Until then, and on any other server:
|
||||
and the Pterodactyl egg install it from the Rust bundle, pinned and checksummed (D224). On any other
|
||||
server:
|
||||
|
||||
1. Download `runicnpc-<version>.tar.gz` and `SHA256SUMS` from this repository's
|
||||
[releases](https://gitea.whitlocktech.com/RunicGateway/runicnpc-rust/releases), and check the
|
||||
@@ -61,8 +63,10 @@ renamed it, because neither framework reports a hook that matches nothing.
|
||||
`place` uses the spot you look at, `here` the spot you stand on; the server console gives
|
||||
`at=x,y,z`. A roamer must stand on Rust's navmesh; only a sentry may stand off it. Every placement
|
||||
answers with what the server's NPCs cost. On a standalone server, profiles are made from a console
|
||||
with `rnpc.profile create|set|delete`. The full list is in
|
||||
[PLAN.md §5](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/PLAN.md).
|
||||
with `rnpc.profile create|set|delete`. Every command and its permission is in
|
||||
[`docs/runicnpc/COMMANDS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/COMMANDS.md),
|
||||
and installing and running it in
|
||||
[`docs/runicnpc/INSTALL.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/INSTALL.md).
|
||||
|
||||
## For other plugins
|
||||
|
||||
@@ -74,10 +78,11 @@ Every call is prefixed `RunicNpc_` and reached through `Call`:
|
||||
int api = RunicNPC?.Call<int>("RunicNpc_ApiVersion") ?? 0;
|
||||
```
|
||||
|
||||
The API is version 3, documented in
|
||||
The API is version 6, documented in
|
||||
[`docs/runicnpc/API.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/runicnpc/API.md):
|
||||
spawning and removing NPCs by owner, profiles, placements (created and named as in game, from a map point),
|
||||
routes, the cost warning, and the hooks it raises.
|
||||
spawning and removing NPCs by owner, profiles and the faction table, placements (created and named as in
|
||||
game, from a map point), routes, escorts, allies and tethers, bosses, loot, the cost warning, and the hooks it
|
||||
raises.
|
||||
The API version moves when a call or a raised hook changes shape, not on every release.
|
||||
|
||||
## Repository layout
|
||||
@@ -87,12 +92,12 @@ The API version moves when a call or a raised hook changes shape, not on every r
|
||||
| `plugin/RunicNPC.cs` | The plugin. The only file a server gets. |
|
||||
| `plugin.toml` | Its declarations: API version, framework floors, required plugins. The release copies them into the manifest. |
|
||||
| `scripts/checkPlugin.js` | The static checks run on every pull request and again before a release (see its header). |
|
||||
| `tools/` | Developer scaffolding for the test rigs, never shipped: the panel scripts (see `tools/rigs.example.json`); `RunicNpcHarness.cs`, the stage 1 measurement plugin; `RunicNpcTest.cs`, the stage 2, 3 and 4 test harness (`rnt.run all`, then `rnt.after` after a reload or restart); and `fieldlist/` with `managed.js`, which regenerate the swap's field list (below). |
|
||||
| `tools/` | Developer scaffolding for the test rigs, never shipped: the panel scripts (see `tools/rigs.example.json`); `RunicNpcHarness.cs`, the stage 1 and 5 measurement plugin; `RunicNpcTest.cs`, the test harness (`rnt.run all`, then `rnt.after` after a reload or restart) and stage 9's performance check (`rnt.cost`, below); and `fieldlist/` with `managed.js`, which regenerate the swap's field list (below). |
|
||||
|
||||
## After a Rust update: the swap's field list
|
||||
|
||||
Our NPC is Rust's scientist with two components swapped, and the swap copies a fixed list of fields (D232),
|
||||
generated from Rust's unmodified assembly. `rnpc.status` reports it as `swap fields: npc=64/64 brain=32/32
|
||||
generated from Rust's unmodified assembly. `rnpc.status` reports it as `swap fields: npc=55/55 brain=32/32
|
||||
missing=- added=-`. On Carbon, where the assembly is unmodified, `added` names any field Rust has added since
|
||||
the list was made. To regenerate it:
|
||||
|
||||
@@ -104,6 +109,43 @@ dotnet run --project tools/fieldlist -- <dir> # rewrites the block
|
||||
Use the Carbon rig: Oxide's patcher makes Rust's private fields public, so its assembly no longer says which
|
||||
fields Rust itself serialises.
|
||||
|
||||
## The update surface: what a Rust update can break
|
||||
|
||||
Everything RunicNPC takes from Rust's own classes for the swap and the brain, in one place. A Rust update
|
||||
that changes any of it breaks RunicNPC; the staging drill (PLAN.md stage 9, D308) checks exactly this list
|
||||
against every new staging build. **Caught by** says how: *compile* means the plugin no longer compiles
|
||||
against the new assembly; *field list* means `tools/fieldlist`'s regenerated list differs from the
|
||||
plugin's (and `rnpc.status` says `missing=` or `added=` at run time); *run time* means only a running server
|
||||
shows it, so the drill's live half and `rnpc.status` are what catch it.
|
||||
|
||||
| Rust type | What RunicNPC depends on | Where (`plugin/RunicNPC.cs`) | Caught by |
|
||||
|---|---|---|---|
|
||||
| `ScientistNPC` (prefabs `scientistnpc_*`) | Subclassed by `RunicNpcPlayer`; the swap replaces the prefab's component before spawn and copies `SwapNpcFields` (55 fields up its chain to `BaseNetworkable`) | `#region The swap`, `CreateNpc` | compile; field list |
|
||||
| `ScientistBrain` | Subclassed by `RunicNpcBrain`; the swap copies `SwapBrainFields` (32 fields of `BaseAIBrain`) | `#region The swap` | compile; field list |
|
||||
| `ScientistNPC` / `HumanNPC` virtuals | `displayName`, `AttackerInfo(PlayerLifeStory.DeathInfo)`, `ShotTest(float)`, `TriggerDown()`, `Hurt(HitInfo)`, `OnDied(HitInfo)` overridden | `RunicNpcPlayer` | compile |
|
||||
| `IAIAttack`, `IAISenses` | Re-implemented on `RunicNpcPlayer`: `IsTargetInRange`, `EngagementRange`, `GetBestTarget`, `AttackTick`, `IsTarget`, `IsThreat`, `IsFriendly`. Rust's `HumanNPC.IsTarget` is not virtual, which is why | `RunicNpcPlayer` | compile; a changed meaning only at run time |
|
||||
| `BaseAIBrain` | `AddStates()` and `Think(float)` overridden; `states`, `CurrentState`, `Navigator`, `Senses`, `Events` used; `AIState` values `Roam`, `Chase`, `Combat`, `TakeCover`, `Cover`, `MoveTowards`, `MoveToVector3`, `FollowPath`, `NavigateHome`, `Flee`, `Blinded` replaced or held | `RunicNpcBrain` | compile; a state Rust stops adding only at run time |
|
||||
| `BaseAIBrain.BasicAIState` | Subclassed six times (`WanderState`, `RnRoamState`, `RnGuardState`, `RouteState`, `PursueState`, `HoldState`): `StateEnter`, `StateThink`, `StateLeave` | `#region The NPC` | compile |
|
||||
| `BaseNavigator` | `SetDestination`, `Stop`, `Moving`, `Agent`, `IsOnNavMeshLink`, `NavigationSpeed` | the states | compile |
|
||||
| `AIMemory` / `AIBrainSenses` | `Memory.Targets`, `SetKnown`, `IsLOS`, `Entity`; `DelaySenseUpdate` | the states, the fight | compile |
|
||||
| `BaseCombatEntity.faction` | Our NPC's target is set to `Faction.Horror` for the length of our own shot, because `BaseProjectile.ServerUse` drops NPC-on-NPC hits otherwise (stage 5) | `RunicNpcPlayer.Mark` | run time (the drill's fight checks) |
|
||||
| `NPCAutoTurret` | Harmony prefixes on the private `Ignore(BasePlayer)` and `IsEntityHostile(BaseCombatEntity)`, found by reflection (D267) | `#region Factions`, `SentryPatch` | run time: `rnpc.status` says whether the patch applied |
|
||||
| `HackableLockedCrate` | The private `hackSeconds`, set by reflection for a loot table's locked crate (stage 7) | `#region Loot` | run time |
|
||||
| `RelationshipManager`, `ClanManager` | Teams and clans for allies (D257) | `#region Factions` | compile |
|
||||
| `Rust.Ai.Gen2.RustNavMeshHelpers` | `SamplePosition` for every placement's navmesh check | `#region Rust's facts` | compile |
|
||||
|
||||
The **hooks** RunicNPC listens to are listed by `rnpc.status`, which names any that has never fired; one
|
||||
that stays silent after an update has probably been renamed (neither framework reports a hook that matches
|
||||
nothing).
|
||||
|
||||
## Performance: `rnt.cost`
|
||||
|
||||
Stage 9's check (D306): 100 of RunicNPC's NPCs, idle and fighting 20 stand-in players, against the same
|
||||
session's empty server, on a 6000 map. It passes when they add no more than stage 1 measured, +1.6 ms idle
|
||||
and +5.2 ms fighting to the median frame. Load `tools/RunicNpcTest.cs` on a rig (it needs the hidden Kits kit
|
||||
`rnhrevolver`), back up `data/RunicNPC/` (it replaces the profiles), stop the other rig, and run
|
||||
`rnt.cost [seconds]`; the result is in `data/RunicNpcTest.json`.
|
||||
|
||||
## Releases
|
||||
|
||||
Work lands on `edge` and is cut over to `main`; every releasable push to `main` tags and publishes a
|
||||
|
||||
Reference in New Issue
Block a user