Pets slowed dramatically after #2591 (issue #2593): the per-step budget grew from half a think interval to the full RunUO-parity move table, and a guarding pet resolves passive (OnCombatantChange clears Warmode whenever the combatant clears), putting Guard/Come at ~1.05s/step for Medium-bucket pets. Order changes and speed-ups also waited out the previously scheduled AITimer wake, because the timer wheel reads Interval only after the next fire. - CurrentMoveSpeed: a controlled pet executing a master's movement order (Come/Follow/Guard, no combatant) paces steps on the think clock — the wild-creature move table no longer slows obedience. Combat chases and herding keep their own pacing. - AITimer: track the pending wake and reschedule (Stop, Delay = remaining, Start) when a speed-up or fresh order moves the earliest deadline up; changes inside a tick still flow through ScheduleNext. New Prod() wakes the AI immediately on player commands, including from a stopped timer (stable claims no longer wait out the random construction stagger). - DoOrderGuard: guard-following routes through WalkMobileRange so it registers a move intent (between-think move wakes) and paths around obstacles instead of bare greedy stepping quantized to the think grid. Closes #2593 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
193 lines
5.6 KiB
C#
193 lines
5.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Server;
|
|
using Server.Mobiles;
|
|
using Xunit;
|
|
|
|
namespace UOContent.Tests.Mobiles.AI;
|
|
|
|
// Pins the pet-obedience pacing policy (issue #2593): a controlled pet executing a
|
|
// master's movement order paces its steps on the think clock, not the wild-creature
|
|
// move table; combat chases and herding keep their own pacing.
|
|
[Collection("Sequential UOContent Tests")]
|
|
public class PetPacingTests : IDisposable
|
|
{
|
|
private readonly List<Mobile> _created = new();
|
|
|
|
private (PlayerMobile master, PetTestStub pet) Spawn(Point3D masterLoc, Point3D petLoc)
|
|
{
|
|
var pair = PetTestSetup.SpawnControlledPet(masterLoc, petLoc);
|
|
_created.Add(pair.master);
|
|
_created.Add(pair.pet);
|
|
return pair;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var m in _created)
|
|
{
|
|
m?.Delete();
|
|
}
|
|
|
|
_created.Clear();
|
|
}
|
|
|
|
[Fact]
|
|
public void ObeyingPet_PacesStepsOnThinkClock()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.SetMoveSpeed(0.3, 0.9); // wild-creature table pace; must not slow obedience
|
|
pet.SetCurrentSpeedToPassive();
|
|
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
|
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
|
|
|
pet.ControlOrder = OrderType.Guard;
|
|
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
|
|
|
pet.ControlTarget = master;
|
|
pet.ControlOrder = OrderType.Follow;
|
|
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
|
}
|
|
|
|
// Boundary guard: a pet chasing a combatant keeps the move table.
|
|
[Fact]
|
|
public void CombatChasingPet_KeepsMoveTable()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var target = new PetTestStub();
|
|
target.MoveToWorld(new Point3D(1003, 1000, 0), Map.Felucca);
|
|
_created.Add(target);
|
|
|
|
pet.SetMoveSpeed(0.3, 0.9);
|
|
pet.ControlOrder = OrderType.Guard;
|
|
pet.Combatant = target;
|
|
pet.SetCurrentSpeedToActive();
|
|
|
|
Assert.Equal(0.3, pet.CurrentMoveSpeed);
|
|
}
|
|
|
|
// Boundary guard: herding overrides obedience pacing.
|
|
[Fact]
|
|
public void HerdedObeyingPet_KeepsHerdingPace()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.SetMoveSpeed(0.45, 0.9);
|
|
pet.SetCurrentSpeedToPassive();
|
|
|
|
pet.TargetLocation = new Point2D(1010, 1010);
|
|
|
|
Assert.Equal(0.3, pet.CurrentMoveSpeed); // fixed herding pace
|
|
}
|
|
|
|
private sealed class ThinkProbe : PetTestStub
|
|
{
|
|
public int Thinks;
|
|
|
|
public override void OnThink()
|
|
{
|
|
Thinks++;
|
|
base.OnThink();
|
|
}
|
|
}
|
|
|
|
private (PlayerMobile master, ThinkProbe pet) SpawnProbe()
|
|
{
|
|
var master = new PlayerMobile(World.NewMobile);
|
|
master.DefaultMobileInit();
|
|
master.MoveToWorld(new Point3D(1000, 1000, 0), Map.Felucca);
|
|
_created.Add(master);
|
|
|
|
var pet = new ThinkProbe();
|
|
pet.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca);
|
|
pet.SetControlMaster(master);
|
|
_created.Add(pet);
|
|
|
|
return (master, pet);
|
|
}
|
|
|
|
// Advances simulated time in 8ms lockstep with the wheel, like the real event loop,
|
|
// so wake schedules and Core.TickCount stay in sync.
|
|
private static void RunFor(long ms)
|
|
{
|
|
var deadline = Core._tickCount + ms;
|
|
|
|
while (Core._tickCount < deadline)
|
|
{
|
|
Core._tickCount += 8;
|
|
Timer.Slice(Core._tickCount);
|
|
}
|
|
}
|
|
|
|
private static bool RunUntil(Func<bool> condition, long maxMs)
|
|
{
|
|
var deadline = Core._tickCount + maxMs;
|
|
|
|
while (Core._tickCount < deadline)
|
|
{
|
|
if (condition())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Core._tickCount += 8;
|
|
Timer.Slice(Core._tickCount);
|
|
}
|
|
|
|
return condition();
|
|
}
|
|
|
|
// Runs past the random spawn-stagger delay to a known think-tick anchor: returns
|
|
// right after a think fires, with the next one a full passive cadence (0.4s) away.
|
|
private ThinkProbe SettledProbe(out PlayerMobile master)
|
|
{
|
|
Core._tickCount = 0;
|
|
Timer.Init(0);
|
|
|
|
var (m, pet) = SpawnProbe();
|
|
master = m;
|
|
pet.ForceIdle = true; // no wandering; pure cadence
|
|
pet.ControlOrder = OrderType.Stay;
|
|
|
|
var settled = RunUntil(() => pet.Thinks >= 2, 8000);
|
|
Assert.True(settled, "the AI must reach a steady think cadence");
|
|
|
|
return pet;
|
|
}
|
|
|
|
[Fact]
|
|
public void OrderChange_WakesStaleThinkTimer()
|
|
{
|
|
var pet = SettledProbe(out var master);
|
|
var thinksBefore = pet.Thinks;
|
|
|
|
// Mid-wait on the passive cadence: the next think is ~200ms out.
|
|
RunFor(200);
|
|
Assert.Equal(thinksBefore, pet.Thinks);
|
|
|
|
// The player issues a command; the pet must not wait out the stale wake.
|
|
pet.ControlTarget = master;
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
RunFor(80);
|
|
Assert.True(pet.Thinks > thinksBefore, "a fresh order must wake the AI promptly");
|
|
}
|
|
|
|
[Fact]
|
|
public void SpeedUp_ReschedulesPendingWake()
|
|
{
|
|
var pet = SettledProbe(out _);
|
|
var thinksBefore = pet.Thinks;
|
|
|
|
// Mid-wait on the passive cadence: the next think is ~200ms out.
|
|
RunFor(200);
|
|
Assert.Equal(thinksBefore, pet.Thinks);
|
|
|
|
// The pet is sped up (e.g. a buff): the next think must move up to the new
|
|
// 0.1s cadence instead of waiting out the stale 0.4s deadline.
|
|
pet.CurrentSpeed = 0.1;
|
|
|
|
RunFor(120);
|
|
Assert.True(pet.Thinks > thinksBefore, "a speed-up must reschedule the pending wake");
|
|
}
|
|
}
|