refactor: master views over BaseCreature's one master reference (#2674)

## Summary

After #2670, `BaseCreature` stores one master. Callers still reached it through three differently gated names plus hand-rolled combinations of the `Controlled`/`Summoned` flags. Each gate is now one named view:

| Member | Definition | Meaning |
|---|---|---|
| `Master` | the reference; its setter does the follower bookkeeping; serialized as before | whoever the creature answers to |
| `ControlMaster` | `Controlled ? Master : null` | the owner |
| `SummonMaster` | `Summoned ? Master : null` | the summoner |
| `GetMaster()` | `ControlMaster ?? SummonMaster` | who answers for the creature |

`ControlMaster` and `SummonMaster` stay settable so custom code keeps compiling: both setters assign `Master`. There is no guard, so a write through either one can read back as null until the matching flag is set.

An enraged creature is the one shape with a master and neither flag. It keeps its meer through `Master`, while `GetMaster()` stays null, so the meer never answers for it (notoriety, kill credit).

Readers move onto the view they mean:
- "Owner or summoner" pairs and `ControlMaster ?? SummonMaster` collapse into `GetMaster()`: pack instinct, familiars, golem, house access, mounts, pack horse, Solen friendship, spell target and guild checks, and the `[` pet command.
- Checks such as `Controlled && ControlMaster == from` drop the flag the view already applies. Checks that don't reduce keep it: `Controlled && ControlMaster != from`, a possibly-null `from`, and `MageAI.CanDispel`'s `Summoned &&`.

**No behavior change, save format unchanged** (the schema generator produces no diff). Edge cases for custom code and shard operators:
- `SummonMaster = x` on a plain pet now replaces its owner (#2670 made that a no-op), and `ControlMaster = null` on an uncontrolled summon now clears its caster (it did nothing before).
- `SpellHelper.GetGuildFor` no longer falls back to an enraged creature's meer. Meer mages have no guild.

## Docs

- `dev-docs/content-patterns.md` § Masters: which view answers which question, how to set a master, and which flag checks are redundant.
- `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` § Masters: RunUO's two-field reads mapped onto the views, plus the gotcha for creatures that set `SummonMaster` without `Summoned` (the write compiles, the read now returns null; use `Master`).
- The `modernuo-content-patterns` and `migrate-items-mobiles` skills point at both.

## Test plan

- [x] `MasterViewsTests`: all four views across wild, pet, controlled summon, uncontrolled summon and enraged shapes; reassigning `Master` moves follower slots; `SetControlMaster(null)` keeps an uncontrolled summon's caster; deleting a summon returns the caster's slots
- [x] Serialization tests updated to the new views (round-trip, legacy v22, v23 migration)
- [x] UOContent.Tests (1178 passed, 2 skipped) and Server.Tests (907) green
This commit is contained in:
Kamron Batman 2026-09-24 22:45:24 -07:00 • committed by GitHub
parent 37970a4811
commit 843a29e7dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 346 additions and 213 deletions

View file

@ -213,7 +213,7 @@ namespace Server.Commands
{
if (m is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster == master || bc.Summoned && bc.SummonMaster == master)
if (bc.GetMaster() == master)
{
pets.Add(bc);
}

View file

@ -637,7 +637,7 @@ namespace Server.Engines.ConPVP
foreach (var mob in facet.GetMobilesInBounds(m_Bounds))
{
if (mob is BaseCreature pet && pet.Controlled && pet.ControlMaster != null &&
if (mob is BaseCreature pet && pet.ControlMaster != null &&
Players.Contains(pet.ControlMaster))
{
pets.Add(pet);

View file

@ -31,7 +31,7 @@ namespace Server.Engines.ConPVP
}
var pm = m as PlayerMobile ??
(m is BaseCreature bc && bc.Summoned ? bc.SummonMaster as PlayerMobile : null);
(m as BaseCreature)?.SummonMaster as PlayerMobile;
if (pm?.DuelContext?.StartedBeginCountdown == true)
{

View file

@ -47,7 +47,7 @@ namespace Server.Engines.Doom
return;
}
}
else if (m is BaseCreature bc && (bc.Controlled && bc.ControlMaster == m_Controller.Successful ||
else if (m is BaseCreature bc && (bc.ControlMaster == m_Controller.Successful ||
bc.Summoned))
{
return;

View file

@ -36,7 +36,7 @@ public class ConfirmReleaseGump : StaticGump<ConfirmReleaseGump>
public override void OnResponse(NetState sender, in RelayInfo info)
{
if (info.ButtonID != 2 || _pet.Deleted ||
!(_pet.Controlled && _from == _pet.ControlMaster &&
!(_from == _pet.ControlMaster &&
_from.CheckAlive() && _pet.Map == _from.Map && _pet.InRange(_from, 14)))
{
return;

View file

@ -237,7 +237,7 @@ public partial class StrongBackpack : Backpack // Used on Pack animals
base.CheckHold(m, item, false, checkItems, plusItems, plusWeight);
public override bool CheckContentDisplay(Mobile from) =>
RootParent is BaseCreature creature && creature.Controlled && creature.ControlMaster == from ||
RootParent is BaseCreature creature && creature.ControlMaster == from ||
base.CheckContentDisplay(from);
}

View file

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

View file

@ -1698,12 +1698,12 @@ public abstract partial class BaseWeapon
return 0;
}
if (attacker is not BaseCreature bc || bc.PackInstinct == PackInstinct.None || !bc.Controlled && !bc.Summoned)
if (attacker is not BaseCreature bc || bc.PackInstinct == PackInstinct.None)
{
return 0;
}
var master = bc.ControlMaster ?? bc.SummonMaster;
var master = bc.GetMaster();
if (master == null)
{
@ -1713,8 +1713,7 @@ public abstract partial class BaseWeapon
var inPack = 1;
foreach (var m in defender.GetMobilesInRange<BaseCreature>(1))
{
if (m != attacker && (m.PackInstinct & bc.PackInstinct) != 0 && (m.Controlled || m.Summoned) &&
master == (m.ControlMaster ?? m.SummonMaster) && m.Combatant == defender)
if (m != attacker && (m.PackInstinct & bc.PackInstinct) != 0 && master == m.GetMaster() && m.Combatant == defender)
{
inPack++;
}
@ -1736,7 +1735,7 @@ public abstract partial class BaseWeapon
{
foreach (var m in defender.GetMobilesInRange<Clone>(4))
{
if (m?.Summoned == true && m.SummonMaster == defender)
if (m?.SummonMaster == defender)
{
// Your attack has been diverted to a nearby mirror image of your target!
attacker.SendLocalizedMessage(1063141);
@ -2100,7 +2099,7 @@ public abstract partial class BaseWeapon
if (attacker is VampireBatFamiliar bc)
{
var caster = bc.ControlMaster ?? bc.SummonMaster;
var caster = bc.GetMaster();
if (caster != null && caster.Map == bc.Map)
{

View file

@ -228,7 +228,7 @@ namespace Server.Misc
}
if (bcTarg?.Controlled == true
|| bcTarg?.Summoned == true && bcTarg.SummonMaster != from && bcTarg.SummonMaster?.Player == true)
|| bcTarg?.SummonMaster is { Player: true } summoner && summoner != from)
{
return false; // Cannot harm other controlled mobiles from players
}

View file

@ -144,7 +144,7 @@ public abstract partial class BaseAI
}
// Only a creature somebody can command has been told anything; wild creatures rest on None.
var toldToStandDown = Mobile.Controlled && Mobile.ControlMaster != null && Mobile.Commandable &&
var toldToStandDown = Mobile.ControlMaster != null && Mobile.Commandable &&
IsStandDownOrder(Mobile.ControlOrder);
if (Mobile.StandsDownOnCommand && toldToStandDown)
@ -1049,15 +1049,15 @@ public abstract partial class BaseAI
private bool IsInvalidSummonTarget(Mobile m, BaseCreature bc)
{
// A summon whose caster was deleted before a reload comes back with no master.
if (Core.AOS && bc?.Summoned == true &&
(bc.SummonMaster == Mobile || bc.SummonMaster is { Player: false } summonMaster && IsHostile(summonMaster)))
if (Core.AOS &&
(bc?.SummonMaster == Mobile || bc?.SummonMaster is { Player: false } summonMaster && IsHostile(summonMaster)))
{
return true;
}
var master = Mobile.SummonMaster;
if (!Mobile.Summoned || master == null)
if (master == null)
{
return false;
}

View file

@ -775,7 +775,7 @@ public abstract partial class BaseAI
var master = Mobile.ControlMaster;
return target == Mobile || master != null &&
(target == master || target is BaseCreature { Controlled: true } creature && creature.ControlMaster == master);
(target == master || target is BaseCreature creature && creature.ControlMaster == master);
}
/// <summary>

View file

@ -152,7 +152,7 @@ public class FamiliarAI : BaseAI
var combatant = target.Combatant;
return combatant == Mobile || combatant == master ||
combatant is BaseCreature { Controlled: true } pet && pet.ControlMaster == master;
combatant is BaseCreature pet && pet.ControlMaster == master;
}
// Closest mobile in a fight with the caster's side.

View file

@ -48,7 +48,7 @@ public abstract class SummonUndeadCounter : MonsterAbility
summon = CreateSummon(source);
summon.Team = source.Team;
summon.Summoned = true;
summon.SummonMaster = source;
summon.Master = source;
summon.FightMode = FightMode.Closest;
summon.Combatant = target;
summon.Home = source.Home;

View file

@ -137,7 +137,7 @@ namespace Server.Mobiles
public static bool CheckAccess(BaseCreature animal, Mobile from) =>
from == animal || from.AccessLevel >= AccessLevel.GameMaster || from.Alive && animal.Controlled &&
!animal.IsDeadPet &&
(from == animal.ControlMaster || from == animal.SummonMaster || animal.IsPetFriend(from));
(from == animal.GetMaster() || animal.IsPetFriend(from));
public static void CombineBackpacks(BaseCreature animal)
{

View file

@ -239,8 +239,7 @@ public abstract partial class BaseMount : BaseCreature, IMount
if (from.InRange(this, 1))
{
var canAccess = from.AccessLevel >= AccessLevel.GameMaster
|| Controlled && ControlMaster == from
|| Summoned && SummonMaster == from;
|| GetMaster() == from;
if (canAccess)
{

View file

@ -71,7 +71,7 @@ namespace Server.Mobiles
public override void OnHarmfulSpell(Mobile from)
{
if (!Controlled && ControlMaster == null)
if (!Controlled)
{
CurrentSpeed = BoostedSpeed;
}
@ -79,7 +79,7 @@ namespace Server.Mobiles
public override void OnCombatantChange()
{
if (Combatant == null && !Controlled && ControlMaster == null)
if (Combatant == null && !Controlled)
{
CurrentSpeed = PassiveSpeed;
}

View file

@ -445,9 +445,7 @@ namespace Server.Mobiles
InvalidateProperties();
}
// ControlMaster and SummonMaster are views of this one reference, gated by Controlled and Summoned.
[SerializableField(15, getter: "private", setter: "private")]
[SaveFlag(nameof(ShouldSerializeMaster))]
// Follower bookkeeping brackets the assignment, so Master is hand-written.
private Mobile _master;
private bool ShouldSerializeMaster() => _master != null;
@ -1152,51 +1150,49 @@ namespace Server.Mobiles
}
}
// Assigned before Controlled is raised (SetControlMaster), so the setter cannot be gated.
// Whoever this creature answers to. ControlMaster, SummonMaster and GetMaster() gate it by
// Controlled and Summoned; an enraged creature has a master with neither flag.
[SerializableProperty(15, useField: nameof(_master))]
[SaveFlag(nameof(ShouldSerializeMaster))]
[CommandProperty(AccessLevel.GameMaster)]
public Mobile ControlMaster
public Mobile Master
{
get => _controlled ? _master : null;
get => _master;
set
{
if (ControlMaster == value || this == value)
if (_master == value || this == value)
{
return;
}
SetMaster(value);
RemoveFollowers();
_master = value;
AddFollowers();
if (value != null)
{
StopDeleteTimer();
}
Delta(MobileDelta.Noto);
this.MarkDirty();
}
}
// The summoner, controlled or not. Enraged creatures have one without being Summoned.
// A plain pet has no summoner, so assigning one would overwrite its owner.
/// <summary>The owner. Setting it sets <see cref="Master"/>.</summary>
[CommandProperty(AccessLevel.GameMaster)]
public Mobile ControlMaster
{
get => _controlled ? _master : null;
set => Master = value;
}
/// <summary>The summoner, controlled or not. Setting it sets <see cref="Master"/>.</summary>
[CommandProperty(AccessLevel.GameMaster)]
public Mobile SummonMaster
{
get => _controlled && !_summoned ? null : _master;
set
{
if (SummonMaster == value || this == value || _controlled && !_summoned)
{
return;
}
SetMaster(value);
}
}
private void SetMaster(Mobile value)
{
RemoveFollowers();
Master = value;
AddFollowers();
Delta(MobileDelta.Noto);
get => _summoned ? _master : null;
set => Master = value;
}
// Fires on every assignment, not only changes: a reissued order is a command (retarget, re-anchor).
@ -2726,7 +2722,7 @@ namespace Server.Mobiles
AnimateDeadSpell.Unregister(SummonMaster, this);
}
if (Summoned && SummonMaster != null)
if (SummonMaster != null)
{
SummonFamiliarSpell.Unregister(SummonMaster, this);
}
@ -2796,7 +2792,7 @@ namespace Server.Mobiles
{
base.AggressiveAction(aggressor, criminal);
if (Controlled && ControlMaster != null && NotorietyHandlers.CheckAggressor(ControlMaster.Aggressors, aggressor))
if (ControlMaster != null && NotorietyHandlers.CheckAggressor(ControlMaster.Aggressors, aggressor))
{
aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true));
}
@ -2904,7 +2900,7 @@ namespace Server.Mobiles
}
public override bool IsHarmfulCriminal(Mobile target) =>
target != ControlMaster && (!Summoned || target != SummonMaster) &&
target != GetMaster() &&
(target is not BaseCreature { InitialInnocent: true } creature || creature.Controlled) &&
(target is not PlayerMobile mobile || mobile.PermaFlags.Count <= 0) && base.IsHarmfulCriminal(target);
@ -2912,9 +2908,9 @@ namespace Server.Mobiles
{
base.CriminalAction(message);
if ((Controlled || Summoned) && _master?.Player == true)
if (GetMaster() is { Player: true } master)
{
_master.CriminalAction(false);
master.CriminalAction(false);
}
}
@ -2922,7 +2918,7 @@ namespace Server.Mobiles
{
base.DoHarmful(target, indirect);
if (target == this || target == _master || !Controlled && !Summoned)
if (target == this || GetMaster() is not { } owner || target == owner)
{
return;
}
@ -3343,20 +3339,8 @@ namespace Server.Mobiles
return bonus;
}
public Mobile GetMaster()
{
if (Controlled && ControlMaster != null)
{
return ControlMaster;
}
if (Summoned && SummonMaster != null)
{
return SummonMaster;
}
return null;
}
// Who answers for this creature: its owner or its summoner. Never an enraged creature's meer.
public Mobile GetMaster() => ControlMaster ?? SummonMaster;
public virtual bool IsMonster => !Controlled || (GetMaster() as BaseCreature)?.IsMonster == true;
@ -3702,7 +3686,7 @@ namespace Server.Mobiles
var m = ControlMaster;
SetControlMaster(null);
SummonMaster = null;
Master = null;
ReceivedHonorContext?.Cancel();
base.OnDelete();
@ -3757,14 +3741,19 @@ namespace Server.Mobiles
}
public override bool CanBeRenamedBy(Mobile from) =>
Controlled && from == ControlMaster && !from.Region.IsPartOf<JailRegion>() ||
from == ControlMaster && !from.Region.IsPartOf<JailRegion>() ||
base.CanBeRenamedBy(from);
public bool SetControlMaster(Mobile m)
{
if (m == null)
{
ControlMaster = null;
// An uncontrolled summon keeps its caster.
if (_controlled)
{
Master = null;
}
Controlled = false;
ControlTarget = null;
ControlOrder = OrderType.None;
@ -3787,7 +3776,7 @@ namespace Server.Mobiles
Home = Point3D.Zero;
ControlMaster = m;
Master = m;
Controlled = true;
ControlTarget = null;
ControlOrder = OrderType.Come;
@ -3849,7 +3838,7 @@ namespace Server.Mobiles
creature.RangeHome = 10;
creature.Summoned = true;
creature.SummonMaster = caster;
creature.Master = caster;
var pack = creature.Backpack;
@ -4006,9 +3995,11 @@ namespace Server.Mobiles
return BardMaster;
}
if ((_controlled || _summoned) && _master != null)
var master = GetMaster();
if (master != null)
{
return _master;
return master;
}
return base.GetDamageMaster(damagee);

View file

@ -94,7 +94,7 @@ public abstract partial class BaseFamiliar : BaseCreature
{
base.GetContextMenuEntries(from, ref list);
if (from.Alive && Controlled && from == ControlMaster && from.InRange(this, 14))
if (from.Alive && from == ControlMaster && from.InRange(this, 14))
{
list.Add(new ReleaseEntry());
}

View file

@ -52,7 +52,7 @@ public partial class DarkWolfFamiliar : BaseFamiliar
m_NextRestore = Core.Now + TimeSpan.FromSeconds(2.0);
var caster = ControlMaster ?? SummonMaster;
var caster = GetMaster();
if (caster != null)
{

View file

@ -62,7 +62,7 @@ public partial class ShadowWispFamiliar : BaseFamiliar
private void Flare()
{
var caster = ControlMaster ?? SummonMaster;
var caster = GetMaster();
if (caster == null)
{

View file

@ -21,14 +21,9 @@ namespace Server.Mobiles
{
if (m is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster is PlayerMobile)
if (bc.GetMaster() is PlayerMobile master)
{
return CheckRedFriendship(bc.ControlMaster);
}
if (bc.Summoned && bc.SummonMaster is PlayerMobile)
{
return CheckRedFriendship(bc.SummonMaster);
return CheckRedFriendship(master);
}
}
@ -39,14 +34,9 @@ namespace Server.Mobiles
{
if (m is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster is PlayerMobile)
if (bc.GetMaster() is PlayerMobile master)
{
return CheckBlackFriendship(bc.ControlMaster);
}
if (bc.Summoned && bc.SummonMaster is PlayerMobile)
{
return CheckBlackFriendship(bc.SummonMaster);
return CheckBlackFriendship(master);
}
}
@ -57,13 +47,9 @@ namespace Server.Mobiles
{
if (from is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster is PlayerMobile)
if (bc.GetMaster() is PlayerMobile master)
{
OnRedDamage(bc.ControlMaster);
}
else if (bc.Summoned && bc.SummonMaster is PlayerMobile)
{
OnRedDamage(bc.SummonMaster);
OnRedDamage(master);
}
}
@ -83,13 +69,9 @@ namespace Server.Mobiles
{
if (from is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster is PlayerMobile)
if (bc.GetMaster() is PlayerMobile master)
{
OnBlackDamage(bc.ControlMaster);
}
else if (bc.Summoned && bc.SummonMaster is PlayerMobile)
{
OnBlackDamage(bc.SummonMaster);
OnBlackDamage(master);
}
}

View file

@ -92,15 +92,15 @@ namespace Server.Mobiles
Karma = -1000;
Tamable = false;
SummonMaster = summoner;
Master = summoner;
}
// The meer's FightMode.Evil against our negative karma would otherwise make us enemies.
public override bool IsEnemy(Mobile m) => m != SummonMaster && base.IsEnemy(m);
public override bool IsEnemy(Mobile m) => m != Master && base.IsEnemy(m);
public override void OnThink()
{
if (SummonMaster?.Deleted != false)
if (Master?.Deleted != false)
{
Delete();
}
@ -111,7 +111,7 @@ namespace Server.Mobiles
*/
else if (!Combat(this))
{
AIObject?.MoveTo(SummonMaster, 5);
AIObject?.MoveTo(Master, 5);
}
/*
On OSI, if the summon attacks a mobile, the summoner meer also
@ -119,11 +119,11 @@ namespace Server.Mobiles
is a player or controlled/summoned, and the summoner is not already
engaged in combat.
*/
else if (!Combat(SummonMaster))
else if (!Combat(Master))
{
if (Combatant.Player || Combatant is BaseCreature bc && bc.GetMaster() != null)
{
SummonMaster.Combatant = Combatant;
Master.Combatant = Combatant;
}
}
else

View file

@ -60,7 +60,7 @@ namespace Server.Mobiles
public override bool InitialInnocent => true;
// FightMode.Evil would otherwise target our own enraged creatures for their negative karma.
public override bool IsEnemy(Mobile m) => (m as BaseEnraged)?.SummonMaster != this && base.IsEnemy(m);
public override bool IsEnemy(Mobile m) => (m as BaseEnraged)?.Master != this && base.IsEnemy(m);
public override void GenerateLoot()
{
@ -88,7 +88,7 @@ namespace Server.Mobiles
if (combatant is BaseCreature bc)
{
if (bc.Controlled && bc.ControlMaster?.Deleted == false && bc.ControlMaster.Alive)
if (bc.ControlMaster is { Deleted: false, Alive: true })
{
if (bc.ControlMaster.Map == Map && bc.ControlMaster.InRange(this, 12) &&
!UnderEffect(bc.ControlMaster))

View file

@ -55,7 +55,7 @@ namespace Server.Mobiles
public override bool BleedImmune => true;
public override Poison PoisonImmune => Poison.Lethal;
public override bool FollowsAcquireRules => Core.AOS || !Summoned || SummonMaster?.Player != true ||
public override bool FollowsAcquireRules => Core.AOS || SummonMaster?.Player != true ||
(Map?.Rules & MapRules.HarmfulRestrictions) != 0;
public override double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) =>

View file

@ -63,7 +63,7 @@ namespace Server.Mobiles
public override bool BleedImmune => true;
public override Poison PoisonImmune => Poison.Lethal;
public override bool FollowsAcquireRules => Core.AOS || !Summoned || SummonMaster?.Player != true ||
public override bool FollowsAcquireRules => Core.AOS || SummonMaster?.Player != true ||
(Map?.Rules & MapRules.HarmfulRestrictions) != 0;
public override double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) =>

View file

@ -142,22 +142,17 @@ namespace Server.Mobiles
public override void OnDamage(int amount, Mobile from, bool willKill)
{
if (Controlled || Summoned)
if (GetMaster() is { Player: true } master && master.Map == Map && master.InRange(Location, 20))
{
var master = ControlMaster ?? SummonMaster;
if (master?.Player == true && master.Map == Map && master.InRange(Location, 20))
if (master.Mana >= amount)
{
if (master.Mana >= amount)
{
master.Mana -= amount;
}
else
{
amount -= master.Mana;
master.Mana = 0;
master.Damage(amount);
}
master.Mana -= amount;
}
else
{
amount -= master.Mana;
master.Mana = 0;
master.Damage(amount);
}
}

View file

@ -59,7 +59,7 @@ namespace Server.Mobiles
public override void OnHarmfulSpell(Mobile from)
{
if (!Controlled && ControlMaster == null)
if (!Controlled)
{
CurrentSpeed = BoostedSpeed;
}
@ -67,7 +67,7 @@ namespace Server.Mobiles
public override void OnCombatantChange()
{
if (Combatant == null && !Controlled && ControlMaster == null)
if (Combatant == null && !Controlled)
{
CurrentSpeed = PassiveSpeed;
}

View file

@ -3660,12 +3660,7 @@ namespace Server.Multis
return true;
}
if (!(bc.Controlled || bc.Summoned))
{
return false;
}
m = bc.ControlMaster ?? bc.SummonMaster;
m = bc.GetMaster();
return m != null && (m.AccessLevel > AccessLevel.Player || IsFriend(m) || Access?.Contains(m) == true);
}

View file

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

View file

@ -407,32 +407,8 @@ namespace Server.Spells
};
}
public static Guild GetGuildFor(Mobile m)
{
var g = m.Guild as Guild;
if (g == null && m is BaseCreature c)
{
m = c.ControlMaster;
if (m != null)
{
g = m.Guild as Guild;
}
if (g == null)
{
m = c.SummonMaster;
if (m != null)
{
g = m.Guild as Guild;
}
}
}
return g;
}
public static Guild GetGuildFor(Mobile m) =>
m.Guild as Guild ?? (m as BaseCreature)?.GetMaster()?.Guild as Guild;
public static bool ValidIndirectTarget(Mobile from, Mobile to)
{
@ -473,29 +449,17 @@ namespace Server.Spells
return false;
}
if (bcTarg != null && (bcTarg.Controlled || bcTarg.Summoned))
if (bcTarg?.GetMaster() is { } targMaster)
{
if (bcTarg.ControlMaster == from || bcTarg.SummonMaster == from)
{
return false;
}
if (p != null && (p.Contains(bcTarg.ControlMaster) || p.Contains(bcTarg.SummonMaster)))
if (targMaster == from || p?.Contains(targMaster) == true)
{
return false;
}
}
if (bcFrom != null && (bcFrom.Controlled || bcFrom.Summoned))
if (bcFrom?.GetMaster() is { } fromMaster)
{
if (bcFrom.ControlMaster == to || bcFrom.SummonMaster == to)
{
return false;
}
p = Party.Get(to);
if (p != null && (p.Contains(bcFrom.ControlMaster) || p.Contains(bcFrom.SummonMaster)))
if (fromMaster == to || Party.Get(to)?.Contains(fromMaster) == true)
{
return false;
}

View file

@ -158,7 +158,7 @@ namespace Server.Mobiles
Warmode = true;
Summoned = true;
SummonMaster = caster;
Master = caster;
ControlOrder = OrderType.Follow;
ControlTarget = caster;