fix(ai): guarding pets never target their owner or the owner's pets (#2665)

Supersedes #2657 (rebased onto #2662, which the original branch conflicted with). Credit to @MithrilHammer for the diagnosis, the fix and the regression tests; their three commits are kept as authored.

Fixes #2656.

## Root cause

When a guarding pet's combatant hides or becomes invalid, its AI calls `AcquireFocusMob` to find a new target. A controlled creature with no `ControlTarget` (Guard normally has none) fell out of `HandleControlled` and into the wild-creature scan, where `IsEnemy` treats any player as an enemy. With `FightMode.Closest`, the owner won. The fall-through dates to #2232. RunUO never let a controlled creature reach the wild scan.

## Changes

- `AcquireFocusMob`: a controlled creature returns whatever `HandleControlled` decides. Guard uses `FindGuardTarget`, Attack uses its `ControlTarget`, and every other order acquires nothing.
- `FindGuardTarget` skips guard allies: the master and creatures the master controls, including controlled summons. Uncontrolled summons (energy vortex, blade spirits) attack their caster, so a guard still defends against them, as in RunUO.
- Friendly fire: `BaseAI.OnAggressiveAction` stands a guard down when an ally is the aggressor. `BaseCreature.Combatant` refuses an ally while guarding, because `Mobile.AggressiveAction` assigns the first aggressor as `Combatant` before any retaliation policy runs. The veto sits on `Combatant` rather than `CanBeHarmful`, so what a pet can harm doesn't depend on its current order. An explicit Attack order and bard provocation can still target an ally.

## Tests

16 regression cases in `GuardFriendlyFireTests` (skipped without client map data), including a real `WhiteWyrm` reproduction. `UOContent.Tests`: 1142 passed, 2 skipped, 0 failed on Windows.
This commit is contained in:
Kamron Batman 2026-09-23 10:13:34 -07:00 • committed by GitHub
parent 28c38168bc
commit 6b910ff089
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 287 additions and 5 deletions

View file

@ -137,6 +137,12 @@ public abstract partial class BaseAI
/// </summary>
public virtual bool OnAggressiveAction(Mobile aggressor)
{
if (Mobile.RefusesGuardTarget(aggressor))
{
DebugSay("Friendly fire; I keep guarding.");
return false;
}
// Only a creature somebody can command has been told anything; wild creatures rest on None.
var toldToStandDown = Mobile.Controlled && Mobile.ControlMaster != null && Mobile.Commandable &&
IsStandDownOrder(Mobile.ControlOrder);
@ -856,7 +862,19 @@ public abstract partial class BaseAI
return false;
}
if (HandleBardProvoked() || HandleControlled() || HandleConstantFocus())
if (HandleBardProvoked())
{
return true;
}
// A controlled creature must not fall through to wild target selection when
// its order has no target. Guard in particular normally has no ControlTarget.
if (Mobile.Controlled)
{
return HandleControlled();
}
if (HandleConstantFocus())
{
return true;
}
@ -911,8 +929,17 @@ public abstract partial class BaseAI
private bool HandleControlled()
{
if (!Mobile.Controlled)
if (Mobile.ControlOrder == OrderType.Guard)
{
Mobile.FocusMob = FindGuardTarget();
return Mobile.FocusMob != null;
}
// Follow and other non-combat orders may target the owner or a friend.
// Only an explicit attack order supplies a combat target.
if (Mobile.ControlOrder != OrderType.Attack)
{
Mobile.FocusMob = null;
return false;
}