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++) {