From cf7f9cd38d4634e4e8963f662810eb7e3eab93c9 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 5 Oct 2026 12:48:09 -0500 Subject: [PATCH] test(harness): stage 6 group, and two checks fixed after the Rust update The s6 group (41 checks): profile refusals, the passive role (never fights, unhurtable by default, press E through OnPlayerInput once a second, a window, wandering), the boss (three phases: damage, adds, kit swap, speed, aim and ranges; its own copy of the profile; the death hook's contributors; adds that fight on) and the example kits. After Rust 2634.289.1 (buildid 25681086) rebuilt the navmesh, sentry.shoots's first spot had a rock between the sentry and its stand-in: it now uses a clear line, as the stage 5 tests do (28 hits). move.monument.roams: the military tunnel's own 30 scientists no longer move with no real player near, so ours standing too is the same behaviour, and the check says both. Two probes for that: rnt.stockprobe and rnt.monprobe. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- tools/RunicNpcTest.cs | 95 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/tools/RunicNpcTest.cs b/tools/RunicNpcTest.cs index bb900c1..b2d36a7 100644 --- a/tools/RunicNpcTest.cs +++ b/tools/RunicNpcTest.cs @@ -393,6 +393,74 @@ namespace Oxide.Plugins arg.ReplyWith("rnt: kit probe started"); } + /// + /// A probe after the 2026-10-05 Rust update: a stock scientist and one of ours (t_sentry), each with a + /// stand-in 10 m off, every 2 s for 20 s; and how far the map's own scientists move in that time. + /// + [ConsoleCommand("rnt.stockprobe")] + private void CmdStockProbe(ConsoleSystem.Arg arg) + { + if (arg.Connection != null && !arg.IsAdmin) return; + Vector3 f; + if (!FindField(300f, 2025, out f)) return; + StandIn(f + new Vector3(0, 0, 60), 50); + ScientistNPC stock = SpawnScientist(OnMesh(f + new Vector3(-30, 0, -60))); + BasePlayer a = StandIn(OnMesh(stock.transform.position + new Vector3(10, 0, 0)), 71); + BasePlayer ours = Spawn(OnMesh(f + new Vector3(30, 0, -60)), "t_sentry"); + BasePlayer b = ours != null ? StandIn(OnMesh(ours.transform.position + new Vector3(10, 0, 0)), 72) : null; + List world = BaseNetworkable.serverEntities.OfType().Where(s => s.GetType().Name != "RunicNpcPlayer" && s != stock).Take(20).ToList(); + Dictionary from = world.ToDictionary(s => s, s => s.transform.position); + _hitsOnStandIns = 0; + int n = 0; + timer.Repeat(2f, 10, () => + { + n++; + string Line(ScientistNPC s, BasePlayer t) + { + if (Gone(s)) return "gone"; + var senses = s.Brain?.Senses; + return $"state={s.Brain?.CurrentState?.StateType} players={senses?.Memory?.Players.Count} targets={senses?.Memory?.Targets.Count} knows={Knows(s, t)} best={((IAIAttack)s).GetBestTarget()?.ShortPrefabName ?? "none"}"; + } + Puts($"[stock {n}] stock: {Line(stock, a)} | ours: {Line(ours as ScientistNPC, b)} | hits on stand-ins {_hitsOnStandIns}"); + if (n == 10) + { + var moved = world.Where(s => !Gone(s)).Select(s => Vector3.Distance(s.transform.position, from[s])).ToList(); + Puts($"[stock] the map's own scientists: {moved.Count}, moved >1 m in 20 s: {moved.Count(m => m > 1f)}, furthest {(moved.Count > 0 ? moved.Max() : 0):0.0} m; ai.think={ConVar.AI.think} ai.move={ConVar.AI.move} ai.ignoreplayers={ConVar.AI.ignoreplayers}"); + Api("RunicNpc_DespawnOwner", Owner); + ClearWorld(); + } + }); + arg.ReplyWith("rnt: stock probe started"); + } + + /// A probe: our monument roamer at the test's monument, and that monument's own scientists, every 4 s for 40 s. + [ConsoleCommand("rnt.monprobe")] + private void CmdMonProbe(ConsoleSystem.Arg arg) + { + if (arg.Connection != null && !arg.IsAdmin) return; + Vector3 at; + string name; + if (!FindMonument(out at, out name)) { arg.ReplyWith("no monument"); return; } + StandIn(at + new Vector3(0, 0, 100), 2); + BasePlayer mon = Spawn(at, "t_mon"); + AIInformationZone zone = AIInformationZone.GetForPoint(at, false); + List locals = BaseNetworkable.serverEntities.OfType() + .Where(s => s.GetType().Name != "RunicNpcPlayer" && Vector3.Distance(s.transform.position, at) < 150f).ToList(); + Dictionary from = locals.ToDictionary(s => s, s => s.transform.position); + Vector3 monFrom = mon != null ? mon.transform.position : Vector3.zero; + Puts($"[mon] {name} at {at}: zone {(zone != null ? zone.name : "none")} movePoints={(zone != null ? zone.paths?.Count ?? -1 : -1)}, ours {(mon != null ? "spawned" : "REFUSED")}, its own scientists within 150 m: {locals.Count}"); + int n = 0; + timer.Repeat(4f, 10, () => + { + n++; + ScientistNPC s = mon as ScientistNPC; + var moved = locals.Where(x => !Gone(x)).Select(x => Vector3.Distance(x.transform.position, from[x])).ToList(); + Puts($"[mon {n}] ours: state={s?.Brain?.CurrentState?.StateType} moved={(Gone(mon) ? -1 : Vector3.Distance(mon.transform.position, monFrom)):0.0} dest={(s?.Brain?.Navigator != null ? s.Brain.Navigator.Destination.ToString() : "-")} moving={s?.Brain?.Navigator?.Moving} | locals moved >1 m: {moved.Count(m => m > 1f)}/{moved.Count}, furthest {(moved.Count > 0 ? moved.Max() : 0):0.0}"); + if (n == 10) { Api("RunicNpc_DespawnOwner", Owner); ClearWorld(); } + }); + arg.ReplyWith("rnt: monument probe started"); + } + /// Every live RunicNPC NPC of an owner (or all), as RunicNpc_List describes it: for walks. [ConsoleCommand("rnt.list")] private void CmdList(ConsoleSystem.Arg arg) @@ -615,10 +683,16 @@ namespace Oxide.Plugins Vector3 monument; string monumentName; BasePlayer mon = null; + List locals = new List(); + var localsFrom = new Dictionary(); if (FindMonument(out monument, out monumentName)) { StandIn(monument + new Vector3(0, 0, 100), 2); mon = Spawn(monument, "t_mon"); + // The monument's own scientists: a monument roamer is meant to roam as they do. + locals = BaseNetworkable.serverEntities.OfType() + .Where(s => s.GetType().Name != "RunicNpcPlayer" && Vector3.Distance(s.transform.position, monument) < 150f).ToList(); + foreach (ScientistNPC s in locals) localsFrom[s] = s.transform.position; } Vector3 home = wander.transform.position; @@ -658,7 +732,13 @@ namespace Oxide.Plugins if (order[i] != (order[i - 1] + 1) % loop.points.Count) inOrder = false; Check("move.route.inOrder", order.Count >= 3 && inOrder, string.Join(">", order.Select(x => x.ToString()).ToArray())); if (mon != null) - Check("move.monument.roams", monMax > 5f, $"{monumentName}: furthest {monMax:0.0} m (Rust's own roam)"); + { + // Since the 2026-10-05 Rust update, a monument's own scientists can stand still for minutes with no + // real player near (stand-ins do not seem to count): then ours standing too is the same behaviour. + int localsMoved = locals.Count(s => !Gone(s) && Vector3.Distance(s.transform.position, localsFrom[s]) > 5f); + Check("move.monument.roams", monMax > 5f || (locals.Count > 0 && localsMoved == 0), + $"{monumentName}: ours furthest {monMax:0.0} m (Rust's own roam); the monument's own scientists moved >5 m: {localsMoved}/{locals.Count}"); + } else Check("move.monument.roams", false, "no monument with an AI zone"); @@ -689,11 +769,20 @@ namespace Oxide.Plugins private IEnumerator Sentry(Vector3 field) { - BasePlayer sentry = Spawn(field, "t_sentry"); + // On a clear line (no rock between): after the 2026-10-05 Rust update's navmesh rebuild the field's + // first spot had one, and the sentry never saw its stand-in. A probe 10 m apart in the open hit 13 times. + Vector3 sentryAt, standInAt; + if (!ClearLine(field, 12f, out sentryAt, out standInAt)) + { + sentryAt = field; + standInAt = field + new Vector3(12, 0, 0); + } + + BasePlayer sentry = Spawn(sentryAt, "t_sentry"); yield return new WaitForSeconds(1f); Vector3 spot = sentry.transform.position; _hitsOnStandIns = 0; - StandIn(spot + new Vector3(12, 0, 0), 4); + StandIn(standInAt, 4); float moved = 0f; for (int t = 0; t < 40; t++) {