refactor: serialize one master reference; fold the SummonMaster lockstep

ControlMaster and SummonMaster, when both set, are always the same
mobile: BaseCreature.Summon assigns both to the caster, and every pet
management flow (transfer, stable, claim, ball of summoning, GM obey,
login overflow) followed SetControlMaster with an identical SummonMaster
assignment. They differ only in presence - uncontrolled summons carry
only a summon master, pets only a control master.

So one _master reference serializes (refreshed at save, fanned back out
through the Controlled/Summoned flags in AfterDeserialization; legacy
loads feed the same path), and SetControlMaster now keeps SummonMaster
in lockstep itself, deleting the six hand-rolled copies of that
boilerplate.

Also: a creature constructed without speeds (missing npc-speeds.json)
now logs debug and defaults to Medium (0.25/0.5) instead of throwing -
this is the place a sane default belongs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-23 20:07:35 -07:00
parent 6d7eb24cc1
commit a082202e98
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
8 changed files with 102 additions and 91 deletions

View file

@ -164,6 +164,27 @@ public class BaseCreatureSerializationTests : IDisposable
Assert.Equal(master, copy.LastOwner); Assert.Equal(master, copy.LastOwner);
} }
[Fact]
public void UncontrolledSummon_KeepsItsSummonMaster()
{
var bc = NewCreature();
var master = new PlayerMobile(World.NewMobile);
master.DefaultMobileInit();
World.AddEntity(master);
_created.Add(master);
// Energy vortex-style: summoned with a master, never controlled.
bc.Summoned = true;
bc.SummonMaster = master;
var copy = Load(Snapshot(bc));
Assert.True(copy.Summoned);
Assert.False(copy.Controlled);
Assert.Equal(master, copy.SummonMaster);
Assert.Null(copy.ControlMaster);
}
private sealed class BucketStub : BaseCreature private sealed class BucketStub : BaseCreature
{ {
public BucketStub() : base(AIType.AI_Melee) => Body = 0xC9; public BucketStub() : base(AIType.AI_Melee) => Body = 0xC9;

View file

@ -227,12 +227,6 @@ public partial class BallOfSummoning : Item, TranslocationItem
if (pet.IsStabled) if (pet.IsStabled)
{ {
pet.SetControlMaster(from); pet.SetControlMaster(from);
if (pet.Summoned)
{
pet.SummonMaster = from;
}
pet.ControlTarget = from; pet.ControlTarget = from;
pet.ControlOrder = OrderType.Follow; pet.ControlOrder = OrderType.Follow;

View file

@ -133,12 +133,6 @@
"" ""
] ]
}, },
{
"name": "ControlMaster",
"type": "Server.Mobile",
"usesSaveFlag": true,
"rule": "SerializableInterfaceMigrationRule"
},
{ {
"name": "ControlTarget", "name": "ControlTarget",
"type": "Server.Mobile", "type": "Server.Mobile",
@ -197,7 +191,7 @@
] ]
}, },
{ {
"name": "SummonMaster", "name": "Master",
"type": "Server.Mobile", "type": "Server.Mobile",
"usesSaveFlag": true, "usesSaveFlag": true,
"rule": "SerializableInterfaceMigrationRule" "rule": "SerializableInterfaceMigrationRule"

View file

@ -447,11 +447,6 @@ public abstract partial class BaseAI
if (Mobile.FindMyName(e.Speech, true) && e.Speech.InsensitiveContains("obey")) if (Mobile.FindMyName(e.Speech, true) && e.Speech.InsensitiveContains("obey"))
{ {
Mobile.SetControlMaster(e.Mobile); Mobile.SetControlMaster(e.Mobile);
if (Mobile.Summoned)
{
Mobile.SummonMaster = e.Mobile;
}
} }
} }
} }

View file

@ -156,11 +156,6 @@ internal sealed partial class TransferItem : Item
private void TransferPetOwnership(Mobile from, Mobile to) private void TransferPetOwnership(Mobile from, Mobile to)
{ {
if (_creature.Summoned)
{
_creature.SummonMaster = to;
}
_creature.ControlTarget = to; _creature.ControlTarget = to;
_creature.ControlOrder = OrderType.Follow; _creature.ControlOrder = OrderType.Follow;
_creature.BondingBegin = DateTime.MinValue; _creature.BondingBegin = DateTime.MinValue;

View file

@ -13,6 +13,7 @@ using Server.Engines.Virtues;
using Server.Ethics; using Server.Ethics;
using Server.Factions; using Server.Factions;
using Server.Items; using Server.Items;
using Server.Logging;
using Server.Misc; using Server.Misc;
using Server.Multis; using Server.Multis;
using Server.Network; using Server.Network;
@ -136,6 +137,8 @@ namespace Server.Mobiles
[SerializationGenerator(23, false)] [SerializationGenerator(23, false)]
public abstract partial class BaseCreature : Mobile, IHonorTarget, IQuestGiver public abstract partial class BaseCreature : Mobile, IHonorTarget, IQuestGiver
{ {
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseCreature));
public enum Allegiance public enum Allegiance
{ {
None, None,
@ -491,43 +494,41 @@ namespace Server.Mobiles
InvalidateProperties(); InvalidateProperties();
} }
// Field 16: ControlMaster (hand-written property; follower bookkeeping brackets the assignment) // ControlMaster and SummonMaster serialize as one master reference (Master below).
private Mobile _controlMaster; private Mobile _controlMaster;
private bool ShouldSerializeControlMaster() => _controlMaster != null; [SerializableField(16)]
[SerializableField(17)]
[SaveFlag(nameof(ShouldSerializeControlTarget))] [SaveFlag(nameof(ShouldSerializeControlTarget))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private Mobile _controlTarget; private Mobile _controlTarget;
private bool ShouldSerializeControlTarget() => _controlTarget != null; private bool ShouldSerializeControlTarget() => _controlTarget != null;
[SerializableField(18)] [SerializableField(17)]
[SaveFlag(nameof(ShouldSerializeControlDest))] [SaveFlag(nameof(ShouldSerializeControlDest))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private Point3D _controlDest; private Point3D _controlDest;
private bool ShouldSerializeControlDest() => _controlDest != Point3D.Zero; private bool ShouldSerializeControlDest() => _controlDest != Point3D.Zero;
// Field 19: ControlOrder (hand-written property; order logic must run on equal re-assignment) // Field 18: ControlOrder (hand-written property; order logic must run on equal re-assignment)
private OrderType _controlOrder; private OrderType _controlOrder;
private bool ShouldSerializeControlOrder() => _controlOrder != OrderType.None; private bool ShouldSerializeControlOrder() => _controlOrder != OrderType.None;
[SerializableField(20)] [SerializableField(19)]
[SaveFlag(nameof(ShouldSerializeMinTameSkill))] [SaveFlag(nameof(ShouldSerializeMinTameSkill))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private double _minTameSkill; private double _minTameSkill;
private bool ShouldSerializeMinTameSkill() => _minTameSkill != 0; private bool ShouldSerializeMinTameSkill() => _minTameSkill != 0;
// Field 21: Tamable (hand-written property; custom getter masks paragons) // Field 20: Tamable (hand-written property; custom getter masks paragons)
private bool _tamable; private bool _tamable;
private bool ShouldSerializeTamable() => _tamable; private bool ShouldSerializeTamable() => _tamable;
[SerializableField(22, fieldChanged: nameof(OnSummonedChange))] [SerializableField(21, fieldChanged: nameof(OnSummonedChange))]
[SaveFlag(nameof(ShouldSerializeSummoned))] [SaveFlag(nameof(ShouldSerializeSummoned))]
[SerializedCommandProperty(AccessLevel.Administrator)] [SerializedCommandProperty(AccessLevel.Administrator)]
private bool _summoned; private bool _summoned;
@ -542,19 +543,30 @@ namespace Server.Mobiles
} }
[AnchoredDateTime] [AnchoredDateTime]
[SerializableField(23, getter: "protected", setter: "protected")] [SerializableField(22, getter: "protected", setter: "protected")]
[SaveFlag(nameof(ShouldSerializeSummonEnd))] [SaveFlag(nameof(ShouldSerializeSummonEnd))]
private DateTime _summonEnd; private DateTime _summonEnd;
private bool ShouldSerializeSummonEnd() => _summoned; private bool ShouldSerializeSummonEnd() => _summoned;
// Field 24: SummonMaster (hand-written property; follower bookkeeping brackets the assignment)
private Mobile _summonMaster; private Mobile _summonMaster;
private bool ShouldSerializeSummonMaster() => _summonMaster != null; // When both roles are set they are always the same mobile (every management flow
// assigns them in lockstep via SetControlMaster), so one reference serializes -
// refreshed here at save - and fans back out through the Controlled/Summoned
// flags in AfterDeserialization.
[SerializableField(23, getter: "private", setter: "private")]
[SaveFlag(nameof(ShouldSerializeMaster))]
private Mobile _master;
private bool ShouldSerializeMaster()
{
_master = _controlMaster ?? _summonMaster;
return _master != null;
}
[EncodedInt] [EncodedInt]
[SerializableField(25)] [SerializableField(24)]
[SaveFlag(nameof(ShouldSerializeControlSlots), nameof(ControlSlotsDefaultValue))] [SaveFlag(nameof(ShouldSerializeControlSlots), nameof(ControlSlotsDefaultValue))]
[SerializedCommandProperty(AccessLevel.Administrator)] [SerializedCommandProperty(AccessLevel.Administrator)]
private int _controlSlots = 1; private int _controlSlots = 1;
@ -564,7 +576,7 @@ namespace Server.Mobiles
private int ControlSlotsDefaultValue() => 1; private int ControlSlotsDefaultValue() => 1;
[EncodedInt] [EncodedInt]
[SerializableField(26, allowFieldChange: nameof(ClampLoyalty))] [SerializableField(25, allowFieldChange: nameof(ClampLoyalty))]
[SaveFlag(nameof(ShouldSerializeLoyalty), nameof(LoyaltyDefaultValue))] [SaveFlag(nameof(ShouldSerializeLoyalty), nameof(LoyaltyDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _loyalty; private int _loyalty;
@ -579,7 +591,7 @@ namespace Server.Mobiles
return true; return true;
} }
[SerializableField(27)] [SerializableField(26)]
[SaveFlag(nameof(ShouldSerializeCurrentWayPoint))] [SaveFlag(nameof(ShouldSerializeCurrentWayPoint))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private WayPoint _currentWayPoint; private WayPoint _currentWayPoint;
@ -587,7 +599,7 @@ namespace Server.Mobiles
private bool ShouldSerializeCurrentWayPoint() => _currentWayPoint != null; private bool ShouldSerializeCurrentWayPoint() => _currentWayPoint != null;
[EncodedInt] [EncodedInt]
[SerializableField(28)] [SerializableField(27)]
[SaveFlag(nameof(ShouldSerializeHitsMaxSeed), nameof(HitsMaxSeedDefaultValue))] [SaveFlag(nameof(ShouldSerializeHitsMaxSeed), nameof(HitsMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _hitsMaxSeed = -1; private int _hitsMaxSeed = -1;
@ -597,7 +609,7 @@ namespace Server.Mobiles
private int HitsMaxSeedDefaultValue() => -1; private int HitsMaxSeedDefaultValue() => -1;
[EncodedInt] [EncodedInt]
[SerializableField(29)] [SerializableField(28)]
[SaveFlag(nameof(ShouldSerializeStamMaxSeed), nameof(StamMaxSeedDefaultValue))] [SaveFlag(nameof(ShouldSerializeStamMaxSeed), nameof(StamMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _stamMaxSeed = -1; private int _stamMaxSeed = -1;
@ -607,7 +619,7 @@ namespace Server.Mobiles
private int StamMaxSeedDefaultValue() => -1; private int StamMaxSeedDefaultValue() => -1;
[EncodedInt] [EncodedInt]
[SerializableField(30)] [SerializableField(29)]
[SaveFlag(nameof(ShouldSerializeManaMaxSeed), nameof(ManaMaxSeedDefaultValue))] [SaveFlag(nameof(ShouldSerializeManaMaxSeed), nameof(ManaMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _manaMaxSeed = -1; private int _manaMaxSeed = -1;
@ -617,7 +629,7 @@ namespace Server.Mobiles
private int ManaMaxSeedDefaultValue() => -1; private int ManaMaxSeedDefaultValue() => -1;
[EncodedInt] [EncodedInt]
[SerializableField(31)] [SerializableField(30)]
[SaveFlag(nameof(ShouldSerializeDamageMin), nameof(DamageMinDefaultValue))] [SaveFlag(nameof(ShouldSerializeDamageMin), nameof(DamageMinDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _damageMin = -1; private int _damageMin = -1;
@ -627,7 +639,7 @@ namespace Server.Mobiles
private int DamageMinDefaultValue() => -1; private int DamageMinDefaultValue() => -1;
[EncodedInt] [EncodedInt]
[SerializableField(32)] [SerializableField(31)]
[SaveFlag(nameof(ShouldSerializeDamageMax), nameof(DamageMaxDefaultValue))] [SaveFlag(nameof(ShouldSerializeDamageMax), nameof(DamageMaxDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _damageMax = -1; private int _damageMax = -1;
@ -637,7 +649,7 @@ namespace Server.Mobiles
private int DamageMaxDefaultValue() => -1; private int DamageMaxDefaultValue() => -1;
[EncodedInt] [EncodedInt]
[SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))] [SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializePhysicalResistanceSeed))] [SaveFlag(nameof(ShouldSerializePhysicalResistanceSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _physicalResistanceSeed; private int _physicalResistanceSeed;
@ -647,7 +659,7 @@ namespace Server.Mobiles
private void OnResistanceSeedChange(int oldValue, int newValue) => UpdateResistances(); private void OnResistanceSeedChange(int oldValue, int newValue) => UpdateResistances();
[EncodedInt] [EncodedInt]
[SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))] [SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeFireResistSeed))] [SaveFlag(nameof(ShouldSerializeFireResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _fireResistSeed; private int _fireResistSeed;
@ -655,7 +667,7 @@ namespace Server.Mobiles
private bool ShouldSerializeFireResistSeed() => _fireResistSeed != 0; private bool ShouldSerializeFireResistSeed() => _fireResistSeed != 0;
[EncodedInt] [EncodedInt]
[SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))] [SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeColdResistSeed))] [SaveFlag(nameof(ShouldSerializeColdResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _coldResistSeed; private int _coldResistSeed;
@ -663,7 +675,7 @@ namespace Server.Mobiles
private bool ShouldSerializeColdResistSeed() => _coldResistSeed != 0; private bool ShouldSerializeColdResistSeed() => _coldResistSeed != 0;
[EncodedInt] [EncodedInt]
[SerializableField(36, fieldChanged: nameof(OnResistanceSeedChange))] [SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializePoisonResistSeed))] [SaveFlag(nameof(ShouldSerializePoisonResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _poisonResistSeed; private int _poisonResistSeed;
@ -671,7 +683,7 @@ namespace Server.Mobiles
private bool ShouldSerializePoisonResistSeed() => _poisonResistSeed != 0; private bool ShouldSerializePoisonResistSeed() => _poisonResistSeed != 0;
[EncodedInt] [EncodedInt]
[SerializableField(37, fieldChanged: nameof(OnResistanceSeedChange))] [SerializableField(36, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeEnergyResistSeed))] [SaveFlag(nameof(ShouldSerializeEnergyResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _energyResistSeed; private int _energyResistSeed;
@ -679,7 +691,7 @@ namespace Server.Mobiles
private bool ShouldSerializeEnergyResistSeed() => _energyResistSeed != 0; private bool ShouldSerializeEnergyResistSeed() => _energyResistSeed != 0;
[EncodedInt] [EncodedInt]
[SerializableField(38)] [SerializableField(37)]
[SaveFlag(nameof(ShouldSerializePhysicalDamage), nameof(PhysicalDamageDefaultValue))] [SaveFlag(nameof(ShouldSerializePhysicalDamage), nameof(PhysicalDamageDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _physicalDamage = 100; private int _physicalDamage = 100;
@ -689,7 +701,7 @@ namespace Server.Mobiles
private int PhysicalDamageDefaultValue() => 100; private int PhysicalDamageDefaultValue() => 100;
[EncodedInt] [EncodedInt]
[SerializableField(39)] [SerializableField(38)]
[SaveFlag(nameof(ShouldSerializeFireDamage))] [SaveFlag(nameof(ShouldSerializeFireDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _fireDamage; private int _fireDamage;
@ -697,7 +709,7 @@ namespace Server.Mobiles
private bool ShouldSerializeFireDamage() => _fireDamage != 0; private bool ShouldSerializeFireDamage() => _fireDamage != 0;
[EncodedInt] [EncodedInt]
[SerializableField(40)] [SerializableField(39)]
[SaveFlag(nameof(ShouldSerializeColdDamage))] [SaveFlag(nameof(ShouldSerializeColdDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _coldDamage; private int _coldDamage;
@ -705,7 +717,7 @@ namespace Server.Mobiles
private bool ShouldSerializeColdDamage() => _coldDamage != 0; private bool ShouldSerializeColdDamage() => _coldDamage != 0;
[EncodedInt] [EncodedInt]
[SerializableField(41)] [SerializableField(40)]
[SaveFlag(nameof(ShouldSerializePoisonDamage))] [SaveFlag(nameof(ShouldSerializePoisonDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _poisonDamage; private int _poisonDamage;
@ -713,7 +725,7 @@ namespace Server.Mobiles
private bool ShouldSerializePoisonDamage() => _poisonDamage != 0; private bool ShouldSerializePoisonDamage() => _poisonDamage != 0;
[EncodedInt] [EncodedInt]
[SerializableField(42)] [SerializableField(41)]
[SaveFlag(nameof(ShouldSerializeEnergyDamage))] [SaveFlag(nameof(ShouldSerializeEnergyDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _energyDamage; private int _energyDamage;
@ -721,7 +733,7 @@ namespace Server.Mobiles
private bool ShouldSerializeEnergyDamage() => _energyDamage != 0; private bool ShouldSerializeEnergyDamage() => _energyDamage != 0;
[Tidy] [Tidy]
[SerializableField(43, setter: "private")] [SerializableField(42, setter: "private")]
[SaveFlag(nameof(ShouldSerializeOwners), nameof(OwnersDefaultValue))] [SaveFlag(nameof(ShouldSerializeOwners), nameof(OwnersDefaultValue))]
private List<Mobile> _owners; private List<Mobile> _owners;
@ -733,13 +745,13 @@ namespace Server.Mobiles
private List<Mobile> OwnersDefaultValue() => new(); private List<Mobile> OwnersDefaultValue() => new();
[SerializableField(44)] [SerializableField(43)]
[SaveFlag(nameof(ShouldSerializeIsDeadPet))] [SaveFlag(nameof(ShouldSerializeIsDeadPet))]
private bool _isDeadPet; private bool _isDeadPet;
private bool ShouldSerializeIsDeadPet() => _isDeadPet; private bool ShouldSerializeIsDeadPet() => _isDeadPet;
[SerializableField(45, fieldChanged: nameof(OnBondedChange))] [SerializableField(44, fieldChanged: nameof(OnBondedChange))]
[SaveFlag(nameof(ShouldSerializeIsBonded))] [SaveFlag(nameof(ShouldSerializeIsBonded))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _isBonded; private bool _isBonded;
@ -748,33 +760,33 @@ namespace Server.Mobiles
private void OnBondedChange(bool oldValue, bool newValue) => InvalidateProperties(); private void OnBondedChange(bool oldValue, bool newValue) => InvalidateProperties();
[SerializableField(46)] [SerializableField(45)]
[SaveFlag(nameof(ShouldSerializeBondingBegin))] [SaveFlag(nameof(ShouldSerializeBondingBegin))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private DateTime _bondingBegin; private DateTime _bondingBegin;
private bool ShouldSerializeBondingBegin() => _bondingBegin != DateTime.MinValue; private bool ShouldSerializeBondingBegin() => _bondingBegin != DateTime.MinValue;
[SerializableField(47)] [SerializableField(46)]
[SaveFlag(nameof(ShouldSerializeOwnerAbandonTime))] [SaveFlag(nameof(ShouldSerializeOwnerAbandonTime))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private DateTime _ownerAbandonTime; private DateTime _ownerAbandonTime;
private bool ShouldSerializeOwnerAbandonTime() => _ownerAbandonTime != DateTime.MinValue; private bool ShouldSerializeOwnerAbandonTime() => _ownerAbandonTime != DateTime.MinValue;
[SerializableField(48)] [SerializableField(47)]
[SaveFlag(nameof(ShouldSerializeHasGeneratedLoot))] [SaveFlag(nameof(ShouldSerializeHasGeneratedLoot))]
private bool _hasGeneratedLoot; private bool _hasGeneratedLoot;
private bool ShouldSerializeHasGeneratedLoot() => _hasGeneratedLoot; private bool ShouldSerializeHasGeneratedLoot() => _hasGeneratedLoot;
// Field 49: IsParagon (hand-written property; the setter converts, which must not run at load) // Field 48: IsParagon (hand-written property; the setter converts, which must not run at load)
private bool _isParagon; private bool _isParagon;
private bool ShouldSerializeIsParagon() => _isParagon; private bool ShouldSerializeIsParagon() => _isParagon;
[Tidy] [Tidy]
[SerializableField(50, setter: "private")] [SerializableField(49, setter: "private")]
[SaveFlag(nameof(ShouldSerializeFriends))] [SaveFlag(nameof(ShouldSerializeFriends))]
private List<Mobile> _friends; private List<Mobile> _friends;
@ -784,7 +796,7 @@ namespace Server.Mobiles
return _friends?.Count > 0; return _friends?.Count > 0;
} }
[SerializableField(51)] [SerializableField(50)]
[SaveFlag(nameof(ShouldSerializeRemoveIfUntamed))] [SaveFlag(nameof(ShouldSerializeRemoveIfUntamed))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _removeIfUntamed; private bool _removeIfUntamed;
@ -792,14 +804,14 @@ namespace Server.Mobiles
private bool ShouldSerializeRemoveIfUntamed() => _removeIfUntamed; private bool ShouldSerializeRemoveIfUntamed() => _removeIfUntamed;
[EncodedInt] [EncodedInt]
[SerializableField(52)] [SerializableField(51)]
[SaveFlag(nameof(ShouldSerializeRemoveStep))] [SaveFlag(nameof(ShouldSerializeRemoveStep))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private int _removeStep; private int _removeStep;
private bool ShouldSerializeRemoveStep() => _removeStep != 0; private bool ShouldSerializeRemoveStep() => _removeStep != 0;
[SerializableField(53, setter: "private")] [SerializableField(52, setter: "private")]
[SaveFlag(nameof(ShouldSerializePendingDeleteTimer))] [SaveFlag(nameof(ShouldSerializePendingDeleteTimer))]
[DeserializeTimer(nameof(DeserializePendingDeleteTimer))] [DeserializeTimer(nameof(DeserializePendingDeleteTimer))]
private Timer _pendingDeleteTimer; private Timer _pendingDeleteTimer;
@ -814,7 +826,7 @@ namespace Server.Mobiles
_pendingDeleteTimer.Start(); _pendingDeleteTimer.Start();
} }
[SerializableField(54)] [SerializableField(53)]
[SaveFlag(nameof(ShouldSerializeCorpseNameOverride))] [SaveFlag(nameof(ShouldSerializeCorpseNameOverride))]
[SerializedCommandProperty(AccessLevel.GameMaster)] [SerializedCommandProperty(AccessLevel.GameMaster)]
private string _corpseNameOverride; private string _corpseNameOverride;
@ -880,10 +892,11 @@ namespace Server.Mobiles
if (_activeSpeed <= 0 || _passiveSpeed <= 0) if (_activeSpeed <= 0 || _passiveSpeed <= 0)
{ {
// A 0-delay creature spins its AI timer at wheel resolution. // A 0-delay creature would spin its AI timer at wheel resolution.
throw new InvalidOperationException( logger.Debug("{Type} constructed without speeds - is Data/npc-speeds.json missing? Defaulting to Medium.", GetType());
$"{GetType()} constructed without speeds - is {"Data/npc-speeds.json"} missing?" _activeSpeed = 0.25;
); _passiveSpeed = 0.5;
_currentSpeed = _passiveSpeed;
} }
_team = 0; _team = 0;
@ -972,7 +985,7 @@ namespace Server.Mobiles
public virtual double WeaponAbilityChance => 0.4; public virtual double WeaponAbilityChance => 0.4;
[SerializableProperty(49, useField: nameof(_isParagon))] [SerializableProperty(48, useField: nameof(_isParagon))]
[SaveFlag(nameof(ShouldSerializeIsParagon))] [SaveFlag(nameof(ShouldSerializeIsParagon))]
[CommandProperty(AccessLevel.GameMaster)] [CommandProperty(AccessLevel.GameMaster)]
public bool IsParagon public bool IsParagon
@ -1183,8 +1196,6 @@ namespace Server.Mobiles
} }
} }
[SerializableProperty(16, useField: nameof(_controlMaster))]
[SaveFlag(nameof(ShouldSerializeControlMaster))]
[CommandProperty(AccessLevel.GameMaster)] [CommandProperty(AccessLevel.GameMaster)]
public Mobile ControlMaster public Mobile ControlMaster
{ {
@ -1209,8 +1220,6 @@ namespace Server.Mobiles
} }
} }
[SerializableProperty(24, useField: nameof(_summonMaster))]
[SaveFlag(nameof(ShouldSerializeSummonMaster))]
[CommandProperty(AccessLevel.GameMaster)] [CommandProperty(AccessLevel.GameMaster)]
public Mobile SummonMaster public Mobile SummonMaster
{ {
@ -1233,7 +1242,7 @@ namespace Server.Mobiles
// Re-issuing the current order must still run the order logic (pet commands), so // Re-issuing the current order must still run the order logic (pet commands), so
// this keeps a hand-written setter with no equality skip. // this keeps a hand-written setter with no equality skip.
[SerializableProperty(19, useField: nameof(_controlOrder))] [SerializableProperty(18, useField: nameof(_controlOrder))]
[SaveFlag(nameof(ShouldSerializeControlOrder))] [SaveFlag(nameof(ShouldSerializeControlOrder))]
[CommandProperty(AccessLevel.GameMaster)] [CommandProperty(AccessLevel.GameMaster)]
public OrderType ControlOrder public OrderType ControlOrder
@ -1268,7 +1277,7 @@ namespace Server.Mobiles
[CommandProperty(AccessLevel.GameMaster)] [CommandProperty(AccessLevel.GameMaster)]
public DateTime BardEndTime { get; set; } public DateTime BardEndTime { get; set; }
[SerializableProperty(21, useField: nameof(_tamable))] [SerializableProperty(20, useField: nameof(_tamable))]
[SaveFlag(nameof(ShouldSerializeTamable))] [SaveFlag(nameof(ShouldSerializeTamable))]
[CommandProperty(AccessLevel.GameMaster)] [CommandProperty(AccessLevel.GameMaster)]
public bool Tamable public bool Tamable
@ -2368,6 +2377,10 @@ namespace Server.Mobiles
_speedEntry = null; _speedEntry = null;
} }
// Feed the masters through the consolidated reference so the
// AfterDeserialization fan-out is uniform across both load paths.
_master = _controlMaster ?? _summonMaster;
if (version <= 14 && _isParagon && Hue == 0x31) if (version <= 14 && _isParagon && Hue == 0x31)
{ {
Hue = Paragon.Hue; // Paragon hue fixed, should now be 0x501. Hue = Paragon.Hue; // Paragon hue fixed, should now be 0x501.
@ -2377,6 +2390,9 @@ namespace Server.Mobiles
[AfterDeserialization] [AfterDeserialization]
private void AfterDeserialization() private void AfterDeserialization()
{ {
_controlMaster = _controlled ? _master : null;
_summonMaster = _summoned ? _master : null;
if (Core.AOS && NameHue == 0x35) if (Core.AOS && NameHue == 0x35)
{ {
NameHue = -1; NameHue = -1;
@ -3622,8 +3638,8 @@ namespace Server.Mobiles
var m = _controlMaster; var m = _controlMaster;
SetControlMaster(null); SetControlMaster(null);
SummonMaster = null; // uncontrolled summons have no control master to clear through
SummonMaster = null;
ReceivedHonorContext?.Cancel(); ReceivedHonorContext?.Cancel();
base.OnDelete(); base.OnDelete();
@ -3669,6 +3685,11 @@ namespace Server.Mobiles
Controlled = false; Controlled = false;
ControlTarget = null; ControlTarget = null;
ControlOrder = OrderType.None; ControlOrder = OrderType.None;
if (_summoned)
{
SummonMaster = null;
}
} }
else else
{ {
@ -3693,6 +3714,11 @@ namespace Server.Mobiles
ControlTarget = null; ControlTarget = null;
ControlOrder = OrderType.Come; ControlOrder = OrderType.Come;
if (_summoned)
{
SummonMaster = m;
}
if (_pendingDeleteTimer != null) if (_pendingDeleteTimer != null)
{ {

View file

@ -3553,7 +3553,6 @@ namespace Server.Mobiles
pet.Internalize(); pet.Internalize();
pet.SetControlMaster(null); pet.SetControlMaster(null);
pet.SummonMaster = null;
pet.IsStabled = true; pet.IsStabled = true;
pet.StabledBy = this; pet.StabledBy = this;
@ -3601,12 +3600,6 @@ namespace Server.Mobiles
if (Followers + pet.ControlSlots <= FollowersMax) if (Followers + pet.ControlSlots <= FollowersMax)
{ {
pet.SetControlMaster(this); pet.SetControlMaster(this);
if (pet.Summoned)
{
pet.SummonMaster = this;
}
pet.ControlTarget = this; pet.ControlTarget = this;
pet.ControlOrder = OrderType.Follow; pet.ControlOrder = OrderType.Follow;

View file

@ -255,7 +255,6 @@ namespace Server.Mobiles
pet.Internalize(); pet.Internalize();
pet.SetControlMaster(null); pet.SetControlMaster(null);
pet.SummonMaster = null;
pet.IsStabled = true; pet.IsStabled = true;
pet.StabledBy = from; pet.StabledBy = from;
@ -356,12 +355,6 @@ namespace Server.Mobiles
private void DoClaim(Mobile from, BaseCreature pet) private void DoClaim(Mobile from, BaseCreature pet)
{ {
pet.SetControlMaster(from); pet.SetControlMaster(from);
if (pet.Summoned)
{
pet.SummonMaster = from;
}
pet.ControlTarget = from; pet.ControlTarget = from;
pet.ControlOrder = OrderType.Follow; pet.ControlOrder = OrderType.Follow;