#W# Confict: Fixed BaseCreature git merge conflict.

This commit is contained in:
WarrentyExpired 2026-10-10 23:17:41 -04:00
commit ad38314ca7
112 changed files with 5397 additions and 616 deletions

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using ModernUO.CodeGeneratedEvents;
using ModernUO.Serialization;
using Server.Collections;
using Server.ContextMenus;
@ -136,7 +135,7 @@ namespace Server.Mobiles
public int CompareTo(DamageStore ds) => (ds?.m_Damage ?? 0).CompareTo(m_Damage);
}
[SerializationGenerator(23, false)]
[SerializationGenerator(24, false)]
public abstract partial class BaseCreature : Mobile, IHonorTarget, IQuestGiver
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseCreature));
@ -451,10 +450,10 @@ namespace Server.Mobiles
InvalidateProperties();
}
// Follower bookkeeping brackets the assignment, so the property is hand-written.
private Mobile _controlMaster;
// Follower bookkeeping brackets the assignment, so Master is hand-written.
private Mobile _master;
private bool ShouldSerializeControlMaster() => _controlMaster != null;
private bool ShouldSerializeMaster() => _master != null;
[SerializableField(16)]
[SaveFlag(nameof(ShouldSerializeControlTarget))]
@ -508,13 +507,8 @@ namespace Server.Mobiles
private bool ShouldSerializeSummonEnd() => _summoned;
// Follower bookkeeping brackets the assignment, so the property is hand-written.
private Mobile _summonMaster;
private bool ShouldSerializeSummonMaster() => _summonMaster != null;
[EncodedInt]
[SerializableField(24)]
[SerializableField(23)]
[SaveFlag(nameof(ShouldSerializeControlSlots), nameof(ControlSlotsDefaultValue))]
[SerializedCommandProperty(AccessLevel.Administrator)]
private int _controlSlots = 1;
@ -524,7 +518,7 @@ namespace Server.Mobiles
private int ControlSlotsDefaultValue() => 1;
[EncodedInt]
[SerializableField(25, allowFieldChange: nameof(ClampLoyalty))]
[SerializableField(24, allowFieldChange: nameof(ClampLoyalty))]
[SaveFlag(nameof(ShouldSerializeLoyalty), nameof(LoyaltyDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _loyalty;
@ -539,7 +533,7 @@ namespace Server.Mobiles
return true;
}
[SerializableField(26)]
[SerializableField(25)]
[SaveFlag(nameof(ShouldSerializeCurrentWayPoint))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private WayPoint _currentWayPoint;
@ -547,7 +541,7 @@ namespace Server.Mobiles
private bool ShouldSerializeCurrentWayPoint() => _currentWayPoint != null;
[EncodedInt]
[SerializableField(27)]
[SerializableField(26)]
[SaveFlag(nameof(ShouldSerializeHitsMaxSeed), nameof(HitsMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _hitsMaxSeed = -1;
@ -557,7 +551,7 @@ namespace Server.Mobiles
private int HitsMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(28)]
[SerializableField(27)]
[SaveFlag(nameof(ShouldSerializeStamMaxSeed), nameof(StamMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _stamMaxSeed = -1;
@ -567,7 +561,7 @@ namespace Server.Mobiles
private int StamMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(29)]
[SerializableField(28)]
[SaveFlag(nameof(ShouldSerializeManaMaxSeed), nameof(ManaMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _manaMaxSeed = -1;
@ -577,7 +571,7 @@ namespace Server.Mobiles
private int ManaMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(30)]
[SerializableField(29)]
[SaveFlag(nameof(ShouldSerializeDamageMin), nameof(DamageMinDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _damageMin = -1;
@ -587,7 +581,7 @@ namespace Server.Mobiles
private int DamageMinDefaultValue() => -1;
[EncodedInt]
[SerializableField(31)]
[SerializableField(30)]
[SaveFlag(nameof(ShouldSerializeDamageMax), nameof(DamageMaxDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _damageMax = -1;
@ -597,7 +591,7 @@ namespace Server.Mobiles
private int DamageMaxDefaultValue() => -1;
[EncodedInt]
[SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(31, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializePhysicalResistanceSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _physicalResistanceSeed;
@ -607,7 +601,7 @@ namespace Server.Mobiles
private void OnResistanceSeedChange(int oldValue, int newValue) => UpdateResistances();
[EncodedInt]
[SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeFireResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _fireResistSeed;
@ -615,7 +609,7 @@ namespace Server.Mobiles
private bool ShouldSerializeFireResistSeed() => _fireResistSeed != 0;
[EncodedInt]
[SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeColdResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _coldResistSeed;
@ -623,7 +617,7 @@ namespace Server.Mobiles
private bool ShouldSerializeColdResistSeed() => _coldResistSeed != 0;
[EncodedInt]
[SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializePoisonResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _poisonResistSeed;
@ -631,7 +625,7 @@ namespace Server.Mobiles
private bool ShouldSerializePoisonResistSeed() => _poisonResistSeed != 0;
[EncodedInt]
[SerializableField(36, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeEnergyResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _energyResistSeed;
@ -639,7 +633,7 @@ namespace Server.Mobiles
private bool ShouldSerializeEnergyResistSeed() => _energyResistSeed != 0;
[EncodedInt]
[SerializableField(37)]
[SerializableField(36)]
[SaveFlag(nameof(ShouldSerializePhysicalDamage), nameof(PhysicalDamageDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _physicalDamage = 100;
@ -649,7 +643,7 @@ namespace Server.Mobiles
private int PhysicalDamageDefaultValue() => 100;
[EncodedInt]
[SerializableField(38)]
[SerializableField(37)]
[SaveFlag(nameof(ShouldSerializeFireDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _fireDamage;
@ -657,7 +651,7 @@ namespace Server.Mobiles
private bool ShouldSerializeFireDamage() => _fireDamage != 0;
[EncodedInt]
[SerializableField(39)]
[SerializableField(38)]
[SaveFlag(nameof(ShouldSerializeColdDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _coldDamage;
@ -665,7 +659,7 @@ namespace Server.Mobiles
private bool ShouldSerializeColdDamage() => _coldDamage != 0;
[EncodedInt]
[SerializableField(40)]
[SerializableField(39)]
[SaveFlag(nameof(ShouldSerializePoisonDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _poisonDamage;
@ -673,7 +667,7 @@ namespace Server.Mobiles
private bool ShouldSerializePoisonDamage() => _poisonDamage != 0;
[EncodedInt]
[SerializableField(41)]
[SerializableField(40)]
[SaveFlag(nameof(ShouldSerializeEnergyDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _energyDamage;
@ -681,7 +675,7 @@ namespace Server.Mobiles
private bool ShouldSerializeEnergyDamage() => _energyDamage != 0;
[Tidy]
[SerializableField(42, setter: "private")]
[SerializableField(41, setter: "private")]
[SaveFlag(nameof(ShouldSerializeOwners), nameof(OwnersDefaultValue))]
private List<Mobile> _owners;
@ -693,13 +687,13 @@ namespace Server.Mobiles
private List<Mobile> OwnersDefaultValue() => new();
[SerializableField(43)]
[SerializableField(42)]
[SaveFlag(nameof(ShouldSerializeIsDeadPet))]
private bool _isDeadPet;
private bool ShouldSerializeIsDeadPet() => _isDeadPet;
[SerializableField(44, fieldChanged: nameof(OnBondedChange))]
[SerializableField(43, fieldChanged: nameof(OnBondedChange))]
[SaveFlag(nameof(ShouldSerializeIsBonded))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _isBonded;
@ -708,21 +702,21 @@ namespace Server.Mobiles
private void OnBondedChange(bool oldValue, bool newValue) => InvalidateProperties();
[SerializableField(45)]
[SerializableField(44)]
[SaveFlag(nameof(ShouldSerializeBondingBegin))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private DateTime _bondingBegin;
private bool ShouldSerializeBondingBegin() => _bondingBegin != DateTime.MinValue;
[SerializableField(46)]
[SerializableField(45)]
[SaveFlag(nameof(ShouldSerializeOwnerAbandonTime))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private DateTime _ownerAbandonTime;
private bool ShouldSerializeOwnerAbandonTime() => _ownerAbandonTime != DateTime.MinValue;
[SerializableField(47)]
[SerializableField(46)]
[SaveFlag(nameof(ShouldSerializeHasGeneratedLoot))]
private bool _hasGeneratedLoot;
@ -734,7 +728,7 @@ namespace Server.Mobiles
private bool ShouldSerializeIsParagon() => _isParagon;
[Tidy]
[SerializableField(49, setter: "private")]
[SerializableField(48, setter: "private")]
[SaveFlag(nameof(ShouldSerializeFriends))]
private List<Mobile> _friends;
@ -744,7 +738,7 @@ namespace Server.Mobiles
return _friends?.Count > 0;
}
[SerializableField(50)]
[SerializableField(49)]
[SaveFlag(nameof(ShouldSerializeRemoveIfUntamed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _removeIfUntamed;
@ -752,21 +746,21 @@ namespace Server.Mobiles
private bool ShouldSerializeRemoveIfUntamed() => _removeIfUntamed;
[EncodedInt]
[SerializableField(51)]
[SerializableField(50)]
[SaveFlag(nameof(ShouldSerializeRemoveStep))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _removeStep;
private bool ShouldSerializeRemoveStep() => _removeStep != 0;
[SerializableField(52, setter: "private")]
[SerializableField(51, setter: "private")]
[SaveFlag(nameof(ShouldSerializePendingDeleteTimer))]
[DeserializeTimer(nameof(DeserializePendingDeleteTimer))]
private Timer _pendingDeleteTimer;
// Stabled and controlled pets never resume a delete countdown.
private bool ShouldSerializePendingDeleteTimer() =>
_pendingDeleteTimer?.Running == true && !IsStabled && !(_controlled && _controlMaster != null);
_pendingDeleteTimer?.Running == true && !IsStabled && ControlMaster == null;
private void DeserializePendingDeleteTimer(TimeSpan delay)
{
@ -774,7 +768,7 @@ namespace Server.Mobiles
_pendingDeleteTimer.Start();
}
[SerializableField(53)]
[SerializableField(52)]
[SaveFlag(nameof(ShouldSerializeCorpseNameOverride))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private string _corpseNameOverride;
@ -852,7 +846,6 @@ namespace Server.Mobiles
Debug = false;
_controlled = false;
_controlMaster = null;
ControlTarget = null;
_controlOrder = OrderType.None;
@ -925,6 +918,10 @@ namespace Server.Mobiles
[CommandProperty(AccessLevel.GameMaster)]
public bool IsPrisoner { get; set; }
/// <summary>
/// When false, a summon may target its own caster and anyone the caster's harmful spells would spare.
/// It still prefers any other target over the caster.
/// </summary>
public virtual bool FollowsAcquireRules => true;
public virtual Faction FactionAllegiance => null;
@ -932,7 +929,7 @@ namespace Server.Mobiles
public virtual double WeaponAbilityChance => 0.4;
[SerializableProperty(48, useField: nameof(_isParagon))]
[SerializableProperty(47, useField: nameof(_isParagon))]
[SaveFlag(nameof(ShouldSerializeIsParagon))]
[CommandProperty(AccessLevel.GameMaster)]
public bool IsParagon
@ -1029,8 +1026,8 @@ namespace Server.Mobiles
}
public virtual bool IsNecroFamiliar =>
Summoned && _controlMaster != null &&
SummonFamiliarSpell.Table.TryGetValue(_controlMaster, out var bc) && bc == this;
Summoned && ControlMaster != null &&
SummonFamiliarSpell.Table.TryGetValue(ControlMaster, out var bc) && bc == this;
public virtual bool DeleteCorpseOnDeath => !Core.AOS && _summoned;
@ -1158,23 +1155,26 @@ namespace Server.Mobiles
}
}
[SerializableProperty(15, useField: nameof(_controlMaster))]
[SaveFlag(nameof(ShouldSerializeControlMaster))]
// 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 => _controlMaster;
get => _master;
set
{
if (_controlMaster == value || this == value)
if (_master == value || this == value)
{
return;
}
RemoveFollowers();
_controlMaster = value;
_master = value;
AddFollowers();
if (_controlMaster != null)
if (value != null)
{
StopDeleteTimer();
}
@ -1184,26 +1184,20 @@ namespace Server.Mobiles
}
}
[SerializableProperty(23, useField: nameof(_summonMaster))]
[SaveFlag(nameof(ShouldSerializeSummonMaster))]
/// <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 => _summonMaster;
set
{
if (_summonMaster == value || this == value)
{
return;
}
RemoveFollowers();
_summonMaster = value;
AddFollowers();
Delta(MobileDelta.Noto);
this.MarkDirty();
}
get => _summoned ? _master : null;
set => Master = value;
}
// Fires on every assignment, not only changes: a reissued order is a command (retarget, re-anchor).
@ -1272,7 +1266,7 @@ namespace Server.Mobiles
}
InvalidateProperties();
_controlMaster?.InvalidateProperties();
ControlMaster?.InvalidateProperties();
this.MarkDirty();
}
@ -2197,9 +2191,75 @@ namespace Server.Mobiles
}
}
// Absent fields resolve through the same default-value methods the live reader uses.
private void MigrateFrom(V23Content content)
{
_defaultAI = content.DefaultAI;
_currentAI = content.CurrentAI ?? CurrentAIDefaultValue();
_rangePerception = content.RangePerception ?? RangePerceptionDefaultValue();
_rangeFight = content.RangeFight ?? RangeFightDefaultValue();
_rangeHome = content.RangeHome ?? RangeHomeDefaultValue();
_team = content.Team ?? 0;
_fightMode = content.FightMode ?? FightModeDefaultValue();
_activeSpeed = content.ActiveSpeed ?? ActiveSpeedDefaultValue();
_passiveSpeed = content.PassiveSpeed ?? PassiveSpeedDefaultValue();
_currentSpeed = content.CurrentSpeed ?? CurrentSpeedDefaultValue();
_activeMoveSpeed = content.ActiveMoveSpeed ?? ActiveMoveSpeedDefaultValue();
_passiveMoveSpeed = content.PassiveMoveSpeed ?? PassiveMoveSpeedDefaultValue();
_home = content.Home ?? Point3D.Zero;
_homeMap = content.HomeMap;
_controlled = content.Controlled;
// A traded summon could still carry its caster as summon master; the owner wins.
_master = content.ControlMaster ?? content.SummonMaster;
_controlTarget = content.ControlTarget;
_controlDest = content.ControlDest ?? Point3D.Zero;
_controlOrder = content.ControlOrder ?? OrderType.None;
_minTameSkill = content.MinTameSkill ?? 0;
_tamable = content.Tamable;
_summoned = content.Summoned;
_summonEnd = content.SummonEnd ?? DateTime.MinValue;
_controlSlots = content.ControlSlots ?? ControlSlotsDefaultValue();
_loyalty = content.Loyalty ?? LoyaltyDefaultValue();
_currentWayPoint = content.CurrentWayPoint;
_hitsMaxSeed = content.HitsMaxSeed ?? HitsMaxSeedDefaultValue();
_stamMaxSeed = content.StamMaxSeed ?? StamMaxSeedDefaultValue();
_manaMaxSeed = content.ManaMaxSeed ?? ManaMaxSeedDefaultValue();
_damageMin = content.DamageMin ?? DamageMinDefaultValue();
_damageMax = content.DamageMax ?? DamageMaxDefaultValue();
_physicalResistanceSeed = content.PhysicalResistanceSeed ?? 0;
_fireResistSeed = content.FireResistSeed ?? 0;
_coldResistSeed = content.ColdResistSeed ?? 0;
_poisonResistSeed = content.PoisonResistSeed ?? 0;
_energyResistSeed = content.EnergyResistSeed ?? 0;
_physicalDamage = content.PhysicalDamage ?? PhysicalDamageDefaultValue();
_fireDamage = content.FireDamage ?? 0;
_coldDamage = content.ColdDamage ?? 0;
_poisonDamage = content.PoisonDamage ?? 0;
_energyDamage = content.EnergyDamage ?? 0;
_owners = content.Owners ?? OwnersDefaultValue();
_isDeadPet = content.IsDeadPet;
_isBonded = content.IsBonded;
_bondingBegin = content.BondingBegin ?? DateTime.MinValue;
_ownerAbandonTime = content.OwnerAbandonTime ?? DateTime.MinValue;
_hasGeneratedLoot = content.HasGeneratedLoot;
_isParagon = content.IsParagon;
_friends = content.Friends;
_removeIfUntamed = content.RemoveIfUntamed;
_removeStep = content.RemoveStep ?? 0;
if (content.PendingDeleteTimerDelay != TimeSpan.MinValue)
{
DeserializePendingDeleteTimer(content.PendingDeleteTimerDelay);
}
_corpseNameOverride = content.CorpseNameOverride;
}
// Pre-codegen loads only (versions 0-22); post-codegen bumps use MigrateFrom.
private void Deserialize(IGenericReader reader, int version)
{
Mobile controlMaster = null;
_currentAI = (AIType)reader.ReadInt();
_defaultAI = (AIType)reader.ReadInt();
@ -2247,7 +2307,7 @@ namespace Server.Mobiles
_fightMode = (FightMode)reader.ReadInt();
_controlled = reader.ReadBool();
_controlMaster = reader.ReadEntity<Mobile>();
controlMaster = reader.ReadEntity<Mobile>();
_controlTarget = reader.ReadEntity<Mobile>();
_controlDest = reader.ReadPoint3D();
_controlOrder = (OrderType)reader.ReadInt();
@ -2275,7 +2335,6 @@ namespace Server.Mobiles
_fightMode = FightMode.Closest;
_controlled = false;
_controlMaster = null;
_controlTarget = null;
_controlOrder = OrderType.None;
}
@ -2296,9 +2355,12 @@ namespace Server.Mobiles
if (version >= 5)
{
_summonMaster = reader.ReadEntity<Mobile>();
_master = reader.ReadEntity<Mobile>();
}
// A transferred summon kept its caster as summon master; the owner wins.
_master = controlMaster ?? _master;
if (version >= 6)
{
_hitsMaxSeed = reader.ReadInt();
@ -2459,7 +2521,7 @@ namespace Server.Mobiles
if (IsAnimatedDead)
{
AnimateDeadSpell.Register(_summonMaster, this);
AnimateDeadSpell.Register(SummonMaster, this);
}
}
@ -2565,7 +2627,7 @@ namespace Server.Mobiles
public void RemoveFollowers()
{
var master = _controlMaster ?? _summonMaster;
var master = _master;
if (master != null)
{
master.Followers -= Math.Min(ControlSlots, master.Followers);
@ -2579,7 +2641,7 @@ namespace Server.Mobiles
public void AddFollowers()
{
var master = _controlMaster ?? _summonMaster;
var master = _master;
if (master != null)
{
master.Followers += ControlSlots;
@ -2658,16 +2720,6 @@ namespace Server.Mobiles
FocusMob = null;
if (IsAnimatedDead)
{
AnimateDeadSpell.Unregister(_summonMaster, this);
}
if (Summoned && SummonMaster != null)
{
SummonFamiliarSpell.Unregister(SummonMaster, this);
}
if (MLQuestSystem.Enabled)
{
MLQuestSystem.HandleDeletion(this);
@ -2738,20 +2790,10 @@ namespace Server.Mobiles
aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true));
}
var ct = _controlOrder;
if (AIObject != null)
// The AI owns the retaliation policy; a creature that stands down keeps its flee and acquire state.
if (AIObject?.OnAggressiveAction(aggressor) != true)
{
if (!StandsDownOnCommand || !BaseAI.IsStandDownOrder(ct))
{
AIObject.OnAggressiveAction(aggressor);
}
else
{
AIObject.DebugSay("I'm being attacked but my master told me not to fight.");
Warmode = false;
return;
}
return;
}
StopFlee();
@ -2767,17 +2809,6 @@ namespace Server.Mobiles
pl.FinishShield();
}
}
// Only reachable when the pet does not stand down: the orders above returned early.
if (aggressor.ChangingCombatant && (_controlled || _summoned) && BaseAI.IsStandDownOrder(ct))
{
IssueOrder(OrderType.Attack, null, aggressor);
}
else if (Combatant == null && !BardPacified)
{
Warmode = true;
Combatant = aggressor;
}
}
public override bool OnMoveOver(Mobile m)
@ -2862,7 +2893,7 @@ namespace Server.Mobiles
}
public override bool IsHarmfulCriminal(Mobile target) =>
(!Controlled || 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);
@ -2870,16 +2901,9 @@ namespace Server.Mobiles
{
base.CriminalAction(message);
if (Controlled || Summoned)
if (GetMaster() is { Player: true } master)
{
if (_controlMaster?.Player == true)
{
_controlMaster.CriminalAction(false);
}
else if (_summonMaster?.Player == true)
{
_summonMaster.CriminalAction(false);
}
master.CriminalAction(false);
}
}
@ -2887,7 +2911,7 @@ namespace Server.Mobiles
{
base.DoHarmful(target, indirect);
if (target == this || target == _controlMaster || target == _summonMaster || !Controlled && !Summoned)
if (target == this || GetMaster() is not { } owner || target == owner)
{
return;
}
@ -3301,20 +3325,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;
@ -3728,16 +3740,47 @@ namespace Server.Mobiles
{
CreatureEvents.CreatureDeletedEvent(this);
var m = _controlMaster;
// Both registries are keyed by the master, which is cleared below.
if (IsAnimatedDead)
{
AnimateDeadSpell.Unregister(SummonMaster, this);
}
if (SummonMaster != null)
{
SummonFamiliarSpell.Unregister(SummonMaster, this);
}
var m = ControlMaster;
SetControlMaster(null);
SummonMaster = null;
Master = null;
ReceivedHonorContext?.Cancel();
base.OnDelete();
m?.InvalidateProperties();
}
// A guard never fights its master or the master's other pets. An explicit Attack order and bard
// provocation still may.
public bool RefusesGuardTarget(Mobile target) =>
_controlled && _controlOrder == OrderType.Guard && !BardProvoked && AIObject?.IsGuardAlly(target) == true;
// Mobile.AggressiveAction assigns the first aggressor as Combatant before any retaliation policy runs.
public override Mobile Combatant
{
get => base.Combatant;
set
{
if (value != null && RefusesGuardTarget(value))
{
return;
}
base.Combatant = value;
}
}
public override bool CanBeHarmful(Mobile target, bool message, bool ignoreOurBlessedness)
{
if (target is BaseFactionGuard)
@ -3766,14 +3809,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;
@ -3796,7 +3844,7 @@ namespace Server.Mobiles
Home = Point3D.Zero;
ControlMaster = m;
Master = m;
Controlled = true;
ControlTarget = null;
ControlOrder = OrderType.Come;
@ -3858,7 +3906,7 @@ namespace Server.Mobiles
creature.RangeHome = 10;
creature.Summoned = true;
creature.SummonMaster = caster;
creature.Master = caster;
var pack = creature.Backpack;
@ -4060,14 +4108,11 @@ namespace Server.Mobiles
return BardMaster;
}
if (_controlled && _controlMaster != null)
{
return _controlMaster;
}
var master = GetMaster();
if (_summoned && _summonMaster != null)
if (master != null)
{
return _summonMaster;
return master;
}
return base.GetDamageMaster(damagee);
@ -4413,16 +4458,27 @@ namespace Server.Mobiles
public virtual void AddPetFriend(Mobile m)
{
if (IsPetFriend(m))
{
return;
}
Friends ??= [];
Friends.Add(m);
this.MarkDirty();
AddToFriends(m);
}
public virtual void RemovePetFriend(Mobile m)
{
if (Friends?.Remove(m) == true)
if (Friends == null)
{
this.MarkDirty();
return;
}
RemoveFromFriends(m);
if (Friends.Count == 0)
{
Friends = null;
}
}
@ -4647,7 +4703,7 @@ namespace Server.Mobiles
if (IsBondable && !IsBonded)
{
var master = _controlMaster;
var master = ControlMaster;
if (master != null && master == from) // So friends can't start the bonding process
{