fix: a stop order silently cancelled a standing follow order (#2616)
## The bug Pet is set to follow. It gets attacked and defends itself (the order flips to `Attack`), or the player tells it to `come`. The player then says `stop`. The pet quietly stops following and starts idle-wandering — the player has to re-issue the order to get it back. `ResolveStop` clears `ControlTarget`, since the transient order that is ending owns it, and then resumes the standing order. Nothing restores a target, so the resumed `Follow` has none. `DoOrderFollow` checks for a target before anything else, finds none on the very next think, says "I have no one to follow" and cancels the order to `None`: ``` follow standing order=Follow target=set persist=Follow cur=0.2 => move=0.3 attacked (defends) order=Attack target=set persist=Follow cur=0.2 => move=0.3 stop issued order=Follow target=null persist=Follow cur=0.2 => move=0.3 next think order=None target=null persist=Follow cur=0.4 => move=0.9 ``` Two things go wrong at once: - `PersistentOrder` still reads `Follow` while `ControlOrder` is `None`, so the pet never recovers on its own. - The pace falls from the active clock to the passive one. A shard that tunes `SetMoveSpeed(active, passive)` sees a following pet drop from its active move speed to its passive one for no visible reason — which is how this surfaced. Era-independent. Guard is unaffected: `DoOrderGuard` works off `ControlMaster`, not `ControlTarget`. ## The fix A standing `Follow` does not always mean "follow the master" — a pet friend can point it elsewhere with `all follow me` / `*follow me` (0x163, 0x16C) or `*follow` plus a target pick. So `SetPersistentOrder` remembers the target the standing order was given, and `ResumePersistentOrder` restores it, falling back to the master only when that target is gone. The field is runtime-only, like `PersistentOrder` itself. Doing this in `ResumePersistentOrder` rather than `ResolveStop` also covers the Friend/Unfriend/Transfer resumes, where `ControlTarget` points at a third party and the pet would otherwise have resumed its follow on *them*. It matches what `HandleInvalidControlTarget` already does before its own resume. ## Tests `Stop_WhileAttacking_FallsBackToPersistentFollow` already existed and passed, because it asserted the resumed order without checking for a target or driving a think tick. Three tests added alongside it: - `Stop_WhileAttacking_ResumedFollowTargetsTheMaster` - `Stop_WhileAttacking_ResumedFollowKeepsAPetFriendAsItsTarget` — a friend's follow order is not hijacked back to the owner - `Stop_WhileAttacking_ResumedFollowSurvivesTheNextThink` — the one that catches the cancellation Each was confirmed to fail without the change (`Expected: Follow, Actual: None`; wrong target). Full suites green: 782 UOContent, 869 Server.
This commit is contained in:
parent
93e46a88b2
commit
114dbba6e2
2 changed files with 67 additions and 0 deletions
|
|
@ -67,6 +67,59 @@ public class PetOrderTests : IDisposable
|
|||
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stop_WhileAttacking_ResumedFollowTargetsTheMaster()
|
||||
{
|
||||
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
pet.ControlTarget = master;
|
||||
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
||||
pet.ControlOrder = OrderType.Attack; // transient
|
||||
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
||||
|
||||
pet.ControlOrder = OrderType.Stop;
|
||||
|
||||
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
||||
Assert.Equal(master, pet.ControlTarget);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stop_WhileAttacking_ResumedFollowKeepsAPetFriendAsItsTarget()
|
||||
{
|
||||
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
var friend = new PlayerMobile(World.NewMobile);
|
||||
friend.DefaultMobileInit();
|
||||
friend.MoveToWorld(new Point3D(1002, 1000, 0), pet.Map);
|
||||
_created.Add(friend);
|
||||
|
||||
var victim = new PetTestStub();
|
||||
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
||||
_created.Add(victim);
|
||||
|
||||
pet.ControlTarget = friend; // a pet friend said "all follow me"
|
||||
pet.ControlOrder = OrderType.Follow;
|
||||
pet.ControlTarget = victim; // then "all kill" — the attack order takes the target
|
||||
pet.ControlOrder = OrderType.Attack;
|
||||
|
||||
pet.ControlOrder = OrderType.Stop;
|
||||
|
||||
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
||||
Assert.Equal(friend, pet.ControlTarget); // still the friend, not the owner
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stop_WhileAttacking_ResumedFollowSurvivesTheNextThink()
|
||||
{
|
||||
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||
pet.ControlOrder = OrderType.Follow;
|
||||
pet.ControlOrder = OrderType.Attack;
|
||||
pet.ControlOrder = OrderType.Stop; // resumes Follow
|
||||
|
||||
pet.AIObject.Obey();
|
||||
|
||||
Assert.Equal(OrderType.Follow, pet.ControlOrder); // not dropped to idle
|
||||
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stop_WhileFollowing_CancelsToIdleNone()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,16 +28,30 @@ public abstract partial class BaseAI
|
|||
// never re-derives the persistent command or re-anchors Home. See OnCurrentOrderChanged.
|
||||
private bool _resolvingOrder;
|
||||
|
||||
// Who a standing Follow follows: usually the master, but a pet friend can point it
|
||||
// elsewhere ("all follow me"). Runtime-only, like PersistentOrder.
|
||||
private Mobile _persistentFollowTarget;
|
||||
|
||||
// The controlled-pet wander anchor (Home) is a pure function of the persistent command.
|
||||
internal void SetPersistentOrder(OrderType order)
|
||||
{
|
||||
PersistentOrder = order;
|
||||
_persistentFollowTarget = order == OrderType.Follow ? Mobile.ControlTarget ?? Mobile.ControlMaster : null;
|
||||
Mobile.Home = order is OrderType.Follow or OrderType.Guard ? Point3D.Zero : Mobile.Location;
|
||||
}
|
||||
|
||||
// Resume the persistent command without re-deriving the persistent order or anchor.
|
||||
private void ResumePersistentOrder()
|
||||
{
|
||||
// The ending order owns ControlTarget and leaves it cleared or pointing elsewhere;
|
||||
// without a target DoOrderFollow cancels itself to idle on the next think.
|
||||
if (PersistentOrder == OrderType.Follow)
|
||||
{
|
||||
Mobile.ControlTarget = _persistentFollowTarget?.Deleted == false
|
||||
? _persistentFollowTarget
|
||||
: Mobile.ControlMaster;
|
||||
}
|
||||
|
||||
_resolvingOrder = true;
|
||||
Mobile.ControlOrder = PersistentOrder;
|
||||
_resolvingOrder = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue