feat: stage 2, the NPC and its API (API 2)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m38s

RunicNPC now spawns its own NPC: Rust's scientist with its ScientistNPC and
ScientistBrain swapped for ours, copying a fixed field list generated from
Rust's unmodified assembly (D232, tools/fieldlist). Profiles live in
data/RunicNPC/profiles.json in the approved shape (D238); placements persist
with each/group respawn (D236) and wait for a missing profile or route
(D237); roamers wander, follow Rust's monument paths, or walk a route (D233,
D234), with our own chase where Rust's needs an AI zone; sentries hold their
spot; NPCs walk home and sleep past 160 m of any player (D235). Spawns wait
for the navmesh and are spread over frames; caps are off by default and the
cost warning is shown instead (D227). The whole PLAN.md section 4 API and its
hooks are in, documented in docs/runicnpc/API.md.

tools/RunicNpcTest.cs is the stage 2 harness; every group passed on both
rigs, and after a plugin reload and a server restart. checkPlugin no longer
counts an override (RunicNpcPlayer.OnDied) as a hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-30 01:31:04 -05:00
parent 914047f1b0
commit f8084955e8
9 changed files with 2937 additions and 42 deletions

49
tools/managed.js Normal file
View File

@@ -0,0 +1,49 @@
// Downloads a rig's managed assemblies (RustDedicated_Data/Managed/*.dll) into a local directory,
// for tools/fieldlist, which reads Rust's own field declarations from them (D232).
//
// node tools/managed.js <rig> <out dir>
//
// Use the CARBON rig. Oxide's patcher rewrites Assembly-CSharp.dll and makes private fields public,
// so the Oxide rig's copy no longer says which fields Rust itself serialises. Carbon leaves the
// assembly as Facepunch shipped it.
const fs = require('fs')
const path = require('path')
const { load, serverId, unmsys } = require('./panel')
const config = load()
const [rig, out] = process.argv.slice(2).map(unmsys)
if (!rig || !out) {
console.error('usage: node tools/managed.js <rig> <out dir>')
process.exit(2)
}
const base = `${config.panel}/api/client/servers/${serverId(config, rig)}`
const headers = { Authorization: 'Bearer ' + config.key, Accept: 'application/json' }
const dir = '/RustDedicated_Data/Managed'
async function main() {
const list = await fetch(`${base}/files/list?directory=${encodeURIComponent(dir)}`, { headers })
if (!list.ok) throw new Error(`list ${list.status}: ${await list.text()}`)
const files = (await list.json()).data
.map((f) => f.attributes)
.filter((f) => f.is_file && f.name.endsWith('.dll'))
fs.mkdirSync(out, { recursive: true })
let bytes = 0
for (const f of files) {
const target = path.join(out, f.name)
if (fs.existsSync(target) && fs.statSync(target).size === f.size) continue
const link = await fetch(`${base}/files/download?file=${encodeURIComponent(`${dir}/${f.name}`)}`, { headers })
if (!link.ok) throw new Error(`${f.name}: ${link.status} ${await link.text()}`)
const body = await fetch((await link.json()).attributes.url)
fs.writeFileSync(target, Buffer.from(await body.arrayBuffer()))
bytes += f.size
}
console.log(`${files.length} assemblies in ${out} (${(bytes / 1e6).toFixed(1)} MB downloaded)`)
}
main().catch((e) => {
console.error(e.message || e)
process.exit(1)
})