fix: check Controlled before reading ControlMaster (#2668)

Readers of `ControlMaster` that act on "this creature is a player's pet" now also require
`Controlled`. Nothing sets a master on an uncontrolled creature today, so behavior does not
change, except that the ML notoriety branch was the one read with nothing in front of it.
The guard keeps these readers correct if a creature ever keeps a master without being
controlled (a suspended-control state such as a charm or turn-pet ability, or folding
SummonMaster into one master field).

- Notoriety (ML): a creature only takes its master's notoriety while controlled
- BaseCreature.AggressiveAction: the aggressor link to the master only applies to controlled pets
- PlayerMobile.AutoStablePets: uncontrolled summons (blade spirits, energy vortexes) are skipped
- CrystalCaveBarrier: only a controlled pet passes on its owner's quest progress
- Discordance: the own-pet exception only applies to a controlled pet
- Dismount (ML): the remount block only goes to the owner of a controlled pet

Also removes the "Summons from monsters can attack players" branch in Mobile_AllowHarmful.
It has been unreachable since #2000: a creature with no player master returns from the
NPC check above it.

Tests: UOContent.Tests 1158 passed.
This commit is contained in:
Kamron Batman 2026-09-23 11:45:48 -07:00 • committed by GitHub
parent 00688a04cf
commit 08dab47413
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 6 additions and 11 deletions

View file

@ -20,7 +20,7 @@ public partial class CrystalCaveBarrier : Item
if (m is BaseCreature creature)
{
mob = creature.ControlMaster;
mob = creature.Controlled ? creature.ControlMaster : null;
}
if (mob is not PlayerMobile pm)

View file

@ -102,7 +102,7 @@ public class Dismount : WeaponAbility
{
playerMobile.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, true);
}
else if (Core.ML && attacker is BaseCreature { ControlMaster: PlayerMobile pm })
else if (Core.ML && attacker is BaseCreature { Controlled: true, ControlMaster: PlayerMobile pm })
{
pm.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, false);
}

View file

@ -233,11 +233,6 @@ namespace Server.Misc
return false; // Cannot harm other controlled mobiles from players
}
if (pmFrom == null && bcFrom != null && bcFrom.Summoned && target.Player)
{
return true; // Summons from monsters can attack players
}
if (target.Player)
{
return false; // Cannot harm other players
@ -422,7 +417,7 @@ namespace Server.Misc
return Notoriety.CanBeAttacked;
}
master = bcTarg.ControlMaster;
master = bcTarg.Controlled ? bcTarg.ControlMaster : null;
if (Core.ML && master != null)
{

View file

@ -2734,7 +2734,7 @@ namespace Server.Mobiles
{
base.AggressiveAction(aggressor, criminal);
if (ControlMaster != null && NotorietyHandlers.CheckAggressor(ControlMaster.Aggressors, aggressor))
if (Controlled && ControlMaster != null && NotorietyHandlers.CheckAggressor(ControlMaster.Aggressors, aggressor))
{
aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true));
}

View file

@ -3517,7 +3517,7 @@ namespace Server.Mobiles
foreach (var follower in allFollowers)
{
if (follower is not BaseCreature pet || pet.ControlMaster == null)
if (follower is not BaseCreature { Controlled: true } pet || pet.ControlMaster == null)
{
continue;
}

View file

@ -141,7 +141,7 @@ namespace Server.SkillHandlers
{
if (targ == from || targ is BaseCreature bc &&
(bc.BardImmune || !from.CanBeHarmful(bc, false)) &&
bc.ControlMaster != from)
!(bc.Controlled && bc.ControlMaster == from))
{
from.SendLocalizedMessage(1049535); // A song of discord would have no effect on that.
}