diff --git a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs index 51a4de573..3b95f452e 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs @@ -182,22 +182,50 @@ public class BaseCreatureSerializationTests : IDisposable Assert.Equal(master, copy.LastOwner); } - [Fact] - public void UncontrolledSummon_KeepsItsSummonMaster() + // One serialized master; the Controlled and Summoned flags decide which views expose it. + [Theory] + [InlineData(true, false)] // controlled pet + [InlineData(true, true)] // controlled summon + [InlineData(false, true)] // energy vortex: summoned, never controlled + [InlineData(false, false)] // EnragedCreature: a summon master without Summoned + public void MasterShapes_RoundTrip(bool controlled, bool summoned) { var bc = NewCreature(); var master = NewMaster(); - // Energy vortex-style: summoned with a master, never controlled. - bc.Summoned = true; - bc.SummonMaster = master; + bc.Summoned = summoned; + + if (controlled) + { + bc.SetControlMaster(master); + } + else + { + 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); + Assert.Equal(controlled, copy.Controlled); + Assert.Equal(summoned, copy.Summoned); + Assert.Equal(controlled ? master : null, copy.ControlMaster); + Assert.Equal(controlled && !summoned ? null : master, copy.SummonMaster); + } + + [Fact] + public void SummonMaster_OnPlainPet_KeepsTheOwner() + { + var bc = NewCreature(); + var owner = NewMaster(); + var other = NewMaster(); + + bc.SetControlMaster(owner); + bc.SummonMaster = other; + + Assert.Equal(owner, bc.ControlMaster); + Assert.Null(bc.SummonMaster); + Assert.Equal(bc.ControlSlots, owner.Followers); + Assert.Equal(0, other.Followers); } [Fact] @@ -418,8 +446,8 @@ public class BaseCreatureSerializationTests : IDisposable Assert.Equal(0.6, copy.CurrentMoveSpeed); // passive mode, inheriting } - // ControlMaster and SummonMaster are independent references: a summon master can - // exist without Summoned (EnragedCreature), and a controlled summon carries both. + // A summon master can exist without Summoned (EnragedCreature), and a controlled summon + // reports the same mobile as both. [Theory] [InlineData(true, true, false, false)] // controlled pet [InlineData(true, true, true, true)] // controlled summon, SummonEnd on the wire @@ -452,4 +480,79 @@ public class BaseCreatureSerializationTests : IDisposable Assert.InRange(copy.SummonEndValue, summonEnd - TimeSpan.FromSeconds(1), summonEnd + TimeSpan.FromSeconds(1)); } } + + // Before summon ownership followed transfers, a traded summon kept its caster as summon master. + [Fact] + public void LegacyV22Stream_DifferingMasters_OwnerWins() + { + var owner = NewMaster(); + var caster = NewMaster(); + + var copy = LoadLegacyV22(true, owner, true, caster, Core.Now + TimeSpan.FromMinutes(5)); + + Assert.Equal(owner, copy.ControlMaster); + Assert.Equal(owner, copy.SummonMaster); + } + + // v23 bit positions: every save-flagged field in schema order (DefaultAI is not flagged). + private const int V23Controlled = 13; + private const int V23ControlMaster = 14; + private const int V23Summoned = 20; + private const int V23SummonEnd = 21; + private const int V23SummonMaster = 22; + private const int V23RemoveStep = 50; + private const int V23CorpseNameOverride = 52; + + // v23 wrote the owner and the summoner to separate slots; v24 keeps one master. + [Fact] + public void V23Stream_MigratesToOneMaster_AndDefaultsAbsentFields() + { + var owner = NewMaster(); + var caster = NewMaster(); + var summonEnd = Core.Now + TimeSpan.FromMinutes(5); + + var donor = new MobileStub(); + donor.DefaultMobileInit(); + _created.Add(donor); + + var writer = new BufferWriter(true); + donor.Serialize(writer); + + writer.Write(23); // version + writer.Write( + 1UL << V23Controlled | 1UL << V23ControlMaster | 1UL << V23Summoned | 1UL << V23SummonEnd | + 1UL << V23SummonMaster | 1UL << V23RemoveStep | 1UL << V23CorpseNameOverride + ); + writer.WriteEncodedInt((int)AIType.AI_Melee); // DefaultAI + writer.Write(owner); // ControlMaster + writer.WriteAnchoredTime(summonEnd); // SummonEnd + writer.Write(caster); // SummonMaster + writer.WriteEncodedInt(3); // RemoveStep + writer.Write("a migrated corpse"); // CorpseNameOverride + + var buffer = new byte[writer.Position]; + writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer); + + var copy = Load(buffer); + + Assert.True(copy.Controlled); + Assert.True(copy.Summoned); + Assert.Equal(owner, copy.ControlMaster); + Assert.Equal(owner, copy.SummonMaster); + Assert.InRange(copy.SummonEndValue, summonEnd - TimeSpan.FromSeconds(1), summonEnd + TimeSpan.FromSeconds(1)); + Assert.Equal(3, copy.RemoveStep); + Assert.Equal("a migrated corpse", copy.CorpseNameOverride); + + // Absent fields take the class defaults, not default(T). + Assert.Equal(AIType.AI_Melee, copy.AI); + Assert.Equal(-1, copy.HitsMaxSeed); + Assert.Equal(1, copy.ControlSlots); + Assert.Equal(BaseCreature.MaxLoyalty, copy.Loyalty); + Assert.Equal(100, copy.PhysicalDamage); + Assert.Equal(10, copy.RangeHome); + Assert.Equal(FightMode.Closest, copy.FightMode); + Assert.Equal(0.3, copy.ActiveSpeed); + Assert.Equal(0.6, copy.PassiveSpeed); + Assert.NotNull(copy.Owners); + } } diff --git a/Projects/UOContent.Tests/UOContent.Tests.csproj b/Projects/UOContent.Tests/UOContent.Tests.csproj index b8c675127..8b83f7992 100644 --- a/Projects/UOContent.Tests/UOContent.Tests.csproj +++ b/Projects/UOContent.Tests/UOContent.Tests.csproj @@ -11,7 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs b/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs index bf0953041..2a881d97f 100644 --- a/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs +++ b/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs @@ -228,11 +228,6 @@ public partial class BallOfSummoning : Item, TranslocationItem { pet.SetControlMaster(from); - if (pet.SummonMaster != null) - { - pet.SummonMaster = from; - } - pet.ControlTarget = from; pet.ControlOrder = OrderType.Follow; diff --git a/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v24.json b/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v24.json new file mode 100644 index 000000000..b1f4fe2fd --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v24.json @@ -0,0 +1,465 @@ +{ + "version": 24, + "type": "Server.Mobiles.BaseCreature", + "properties": [ + { + "name": "DefaultAI", + "type": "Server.Mobiles.AIType", + "rule": "EnumMigrationRule" + }, + { + "name": "CurrentAI", + "type": "Server.Mobiles.AIType", + "usesSaveFlag": true, + "rule": "EnumMigrationRule" + }, + { + "name": "RangePerception", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "RangeFight", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "RangeHome", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "Team", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "FightMode", + "type": "Server.Mobiles.FightMode", + "usesSaveFlag": true, + "rule": "EnumMigrationRule" + }, + { + "name": "ActiveSpeed", + "type": "double", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "PassiveSpeed", + "type": "double", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "CurrentSpeed", + "type": "double", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "ActiveMoveSpeed", + "type": "double", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "PassiveMoveSpeed", + "type": "double", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Home", + "type": "Server.Point3D", + "usesSaveFlag": true, + "rule": "PrimitiveUOTypeMigrationRule", + "ruleArguments": [ + "Point3D" + ] + }, + { + "name": "HomeMap", + "type": "Server.Map", + "usesSaveFlag": true, + "rule": "PrimitiveUOTypeMigrationRule", + "ruleArguments": [ + "Map" + ] + }, + { + "name": "Controlled", + "type": "bool", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Master", + "type": "Server.Mobile", + "usesSaveFlag": true, + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "ControlTarget", + "type": "Server.Mobile", + "usesSaveFlag": true, + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "ControlDest", + "type": "Server.Point3D", + "usesSaveFlag": true, + "rule": "PrimitiveUOTypeMigrationRule", + "ruleArguments": [ + "Point3D" + ] + }, + { + "name": "ControlOrder", + "type": "Server.Mobiles.OrderType", + "usesSaveFlag": true, + "rule": "EnumMigrationRule" + }, + { + "name": "MinTameSkill", + "type": "double", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Tamable", + "type": "bool", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Summoned", + "type": "bool", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "SummonEnd", + "type": "System.DateTime", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "AnchoredTime" + ] + }, + { + "name": "ControlSlots", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "Loyalty", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "CurrentWayPoint", + "type": "Server.Items.WayPoint", + "usesSaveFlag": true, + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "HitsMaxSeed", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "StamMaxSeed", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "ManaMaxSeed", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "DamageMin", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "DamageMax", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "PhysicalResistanceSeed", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "FireResistSeed", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "ColdResistSeed", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "PoisonResistSeed", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "EnergyResistSeed", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "PhysicalDamage", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "FireDamage", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "ColdDamage", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "PoisonDamage", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "EnergyDamage", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "Owners", + "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", + "usesSaveFlag": true, + "rule": "ListMigrationRule", + "ruleArguments": [ + "@Tidy", + "Server.Mobile", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "IsDeadPet", + "type": "bool", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "IsBonded", + "type": "bool", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "BondingBegin", + "type": "System.DateTime", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "OwnerAbandonTime", + "type": "System.DateTime", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "HasGeneratedLoot", + "type": "bool", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "IsParagon", + "type": "bool", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Friends", + "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", + "usesSaveFlag": true, + "rule": "ListMigrationRule", + "ruleArguments": [ + "@Tidy", + "Server.Mobile", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "RemoveIfUntamed", + "type": "bool", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "RemoveStep", + "type": "int", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "PendingDeleteTimer", + "type": "Server.Timer", + "usesSaveFlag": true, + "rule": "TimerMigrationRule", + "ruleArguments": [ + "@AnchoredTimer" + ] + }, + { + "name": "CorpseNameOverride", + "type": "string", + "usesSaveFlag": true, + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs b/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs index 7ce0994f6..deda86702 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs @@ -451,11 +451,6 @@ public abstract partial class BaseAI Mobile.SetControlMaster(e.Mobile); - if (Mobile.SummonMaster != null) - { - Mobile.SummonMaster = e.Mobile; - } - return true; } } diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs b/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs index 7eac17ae5..08dcda911 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs @@ -156,11 +156,6 @@ internal sealed partial class TransferItem : Item private void TransferPetOwnership(Mobile from, Mobile to) { - if (_creature.SummonMaster != null) - { - _creature.SummonMaster = to; - } - _creature.ClearPetFriends(); _creature.IssueOrder(OrderType.Follow, null, to); _creature.BondingBegin = DateTime.MinValue; diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 18743579d..d37465796 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -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; @@ -135,7 +134,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)); @@ -446,10 +445,12 @@ namespace Server.Mobiles InvalidateProperties(); } - // Follower bookkeeping brackets the assignment, so the property is hand-written. - private Mobile _controlMaster; + // ControlMaster and SummonMaster are views of this one reference, gated by Controlled and Summoned. + [SerializableField(15, getter: "private", setter: "private")] + [SaveFlag(nameof(ShouldSerializeMaster))] + private Mobile _master; - private bool ShouldSerializeControlMaster() => _controlMaster != null; + private bool ShouldSerializeMaster() => _master != null; [SerializableField(16)] [SaveFlag(nameof(ShouldSerializeControlTarget))] @@ -503,13 +504,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; @@ -519,7 +515,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; @@ -534,7 +530,7 @@ namespace Server.Mobiles return true; } - [SerializableField(26)] + [SerializableField(25)] [SaveFlag(nameof(ShouldSerializeCurrentWayPoint))] [SerializedCommandProperty(AccessLevel.GameMaster)] private WayPoint _currentWayPoint; @@ -542,7 +538,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; @@ -552,7 +548,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; @@ -562,7 +558,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; @@ -572,7 +568,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; @@ -582,7 +578,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; @@ -592,7 +588,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; @@ -602,7 +598,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; @@ -610,7 +606,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; @@ -618,7 +614,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; @@ -626,7 +622,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; @@ -634,7 +630,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; @@ -644,7 +640,7 @@ namespace Server.Mobiles private int PhysicalDamageDefaultValue() => 100; [EncodedInt] - [SerializableField(38)] + [SerializableField(37)] [SaveFlag(nameof(ShouldSerializeFireDamage))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _fireDamage; @@ -652,7 +648,7 @@ namespace Server.Mobiles private bool ShouldSerializeFireDamage() => _fireDamage != 0; [EncodedInt] - [SerializableField(39)] + [SerializableField(38)] [SaveFlag(nameof(ShouldSerializeColdDamage))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _coldDamage; @@ -660,7 +656,7 @@ namespace Server.Mobiles private bool ShouldSerializeColdDamage() => _coldDamage != 0; [EncodedInt] - [SerializableField(40)] + [SerializableField(39)] [SaveFlag(nameof(ShouldSerializePoisonDamage))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _poisonDamage; @@ -668,7 +664,7 @@ namespace Server.Mobiles private bool ShouldSerializePoisonDamage() => _poisonDamage != 0; [EncodedInt] - [SerializableField(41)] + [SerializableField(40)] [SaveFlag(nameof(ShouldSerializeEnergyDamage))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _energyDamage; @@ -676,7 +672,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 _owners; @@ -688,13 +684,13 @@ namespace Server.Mobiles private List 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; @@ -703,21 +699,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; @@ -729,7 +725,7 @@ namespace Server.Mobiles private bool ShouldSerializeIsParagon() => _isParagon; [Tidy] - [SerializableField(49, setter: "private")] + [SerializableField(48, setter: "private")] [SaveFlag(nameof(ShouldSerializeFriends))] private List _friends; @@ -739,7 +735,7 @@ namespace Server.Mobiles return _friends?.Count > 0; } - [SerializableField(50)] + [SerializableField(49)] [SaveFlag(nameof(ShouldSerializeRemoveIfUntamed))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _removeIfUntamed; @@ -747,21 +743,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) { @@ -769,7 +765,7 @@ namespace Server.Mobiles _pendingDeleteTimer.Start(); } - [SerializableField(53)] + [SerializableField(52)] [SaveFlag(nameof(ShouldSerializeCorpseNameOverride))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _corpseNameOverride; @@ -847,7 +843,6 @@ namespace Server.Mobiles Debug = false; _controlled = false; - _controlMaster = null; ControlTarget = null; _controlOrder = OrderType.None; @@ -931,7 +926,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 @@ -1028,8 +1023,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; @@ -1157,54 +1152,53 @@ namespace Server.Mobiles } } - [SerializableProperty(15, useField: nameof(_controlMaster))] - [SaveFlag(nameof(ShouldSerializeControlMaster))] + // Assigned before Controlled is raised (SetControlMaster), so the setter cannot be gated. [CommandProperty(AccessLevel.GameMaster)] public Mobile ControlMaster { - get => _controlMaster; + get => _controlled ? _master : null; set { - if (_controlMaster == value || this == value) + if (ControlMaster == value || this == value) { return; } - RemoveFollowers(); - _controlMaster = value; - AddFollowers(); - if (_controlMaster != null) + SetMaster(value); + + if (value != null) { StopDeleteTimer(); } - - Delta(MobileDelta.Noto); - this.MarkDirty(); } } - [SerializableProperty(23, useField: nameof(_summonMaster))] - [SaveFlag(nameof(ShouldSerializeSummonMaster))] + // 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. [CommandProperty(AccessLevel.GameMaster)] public Mobile SummonMaster { - get => _summonMaster; + get => _controlled && !_summoned ? null : _master; set { - if (_summonMaster == value || this == value) + if (SummonMaster == value || this == value || _controlled && !_summoned) { return; } - RemoveFollowers(); - _summonMaster = value; - AddFollowers(); - - Delta(MobileDelta.Noto); - this.MarkDirty(); + SetMaster(value); } } + private void SetMaster(Mobile value) + { + RemoveFollowers(); + Master = value; + AddFollowers(); + + Delta(MobileDelta.Noto); + } + // Fires on every assignment, not only changes: a reissued order is a command (retarget, re-anchor). // A raw assignment is system-issued; player commands go through IssueOrder. [SerializableProperty(18, useField: nameof(_controlOrder))] @@ -1271,7 +1265,7 @@ namespace Server.Mobiles } InvalidateProperties(); - _controlMaster?.InvalidateProperties(); + ControlMaster?.InvalidateProperties(); this.MarkDirty(); } @@ -2195,9 +2189,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(); @@ -2245,7 +2305,7 @@ namespace Server.Mobiles _fightMode = (FightMode)reader.ReadInt(); _controlled = reader.ReadBool(); - _controlMaster = reader.ReadEntity(); + controlMaster = reader.ReadEntity(); _controlTarget = reader.ReadEntity(); _controlDest = reader.ReadPoint3D(); _controlOrder = (OrderType)reader.ReadInt(); @@ -2273,7 +2333,6 @@ namespace Server.Mobiles _fightMode = FightMode.Closest; _controlled = false; - _controlMaster = null; _controlTarget = null; _controlOrder = OrderType.None; } @@ -2294,9 +2353,12 @@ namespace Server.Mobiles if (version >= 5) { - _summonMaster = reader.ReadEntity(); + _master = reader.ReadEntity(); } + // A transferred summon kept its caster as summon master; the owner wins. + _master = controlMaster ?? _master; + if (version >= 6) { _hitsMaxSeed = reader.ReadInt(); @@ -2457,7 +2519,7 @@ namespace Server.Mobiles if (IsAnimatedDead) { - AnimateDeadSpell.Register(_summonMaster, this); + AnimateDeadSpell.Register(SummonMaster, this); } } @@ -2566,7 +2628,7 @@ namespace Server.Mobiles public void RemoveFollowers() { - var master = _controlMaster ?? _summonMaster; + var master = _master; if (master != null) { master.Followers -= Math.Min(ControlSlots, master.Followers); @@ -2580,7 +2642,7 @@ namespace Server.Mobiles public void AddFollowers() { - var master = _controlMaster ?? _summonMaster; + var master = _master; if (master != null) { master.Followers += ControlSlots; @@ -2661,7 +2723,7 @@ namespace Server.Mobiles if (IsAnimatedDead) { - AnimateDeadSpell.Unregister(_summonMaster, this); + AnimateDeadSpell.Unregister(SummonMaster, this); } if (Summoned && SummonMaster != null) @@ -2842,7 +2904,7 @@ namespace Server.Mobiles } public override bool IsHarmfulCriminal(Mobile target) => - (!Controlled || target != _controlMaster) && (!Summoned || target != _summonMaster) && + target != ControlMaster && (!Summoned || target != SummonMaster) && (target is not BaseCreature { InitialInnocent: true } creature || creature.Controlled) && (target is not PlayerMobile mobile || mobile.PermaFlags.Count <= 0) && base.IsHarmfulCriminal(target); @@ -2850,16 +2912,9 @@ namespace Server.Mobiles { base.CriminalAction(message); - if (Controlled || Summoned) + if ((Controlled || Summoned) && _master?.Player == true) { - if (_controlMaster?.Player == true) - { - _controlMaster.CriminalAction(false); - } - else if (_summonMaster?.Player == true) - { - _summonMaster.CriminalAction(false); - } + _master.CriminalAction(false); } } @@ -2867,7 +2922,7 @@ namespace Server.Mobiles { base.DoHarmful(target, indirect); - if (target == this || target == _controlMaster || target == _summonMaster || !Controlled && !Summoned) + if (target == this || target == _master || !Controlled && !Summoned) { return; } @@ -3644,7 +3699,7 @@ namespace Server.Mobiles { CreatureEvents.CreatureDeletedEvent(this); - var m = _controlMaster; + var m = ControlMaster; SetControlMaster(null); SummonMaster = null; @@ -3951,14 +4006,9 @@ namespace Server.Mobiles return BardMaster; } - if (_controlled && _controlMaster != null) + if ((_controlled || _summoned) && _master != null) { - return _controlMaster; - } - - if (_summoned && _summonMaster != null) - { - return _summonMaster; + return _master; } return base.GetDamageMaster(damagee); @@ -4549,7 +4599,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 { diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index cf7fc000b..f1a81a53a 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -3553,7 +3553,6 @@ namespace Server.Mobiles pet.Internalize(); pet.SetControlMaster(null); - pet.SummonMaster = null; pet.IsStabled = true; pet.StabledBy = this; @@ -3602,11 +3601,6 @@ namespace Server.Mobiles { pet.SetControlMaster(this); - if (pet.SummonMaster != null) - { - pet.SummonMaster = this; - } - pet.ControlTarget = this; pet.ControlOrder = OrderType.Follow; diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs b/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs index b3fb4d721..81a000710 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs @@ -255,7 +255,6 @@ namespace Server.Mobiles pet.Internalize(); pet.SetControlMaster(null); - pet.SummonMaster = null; pet.IsStabled = true; pet.StabledBy = from; @@ -357,11 +356,6 @@ namespace Server.Mobiles { pet.SetControlMaster(from); - if (pet.SummonMaster != null) - { - pet.SummonMaster = from; - } - pet.ControlTarget = from; pet.ControlOrder = OrderType.Follow; diff --git a/Projects/UOContent/Systems/JailSystem/JailSystem.cs b/Projects/UOContent/Systems/JailSystem/JailSystem.cs index 8da7319c9..8802d0487 100644 --- a/Projects/UOContent/Systems/JailSystem/JailSystem.cs +++ b/Projects/UOContent/Systems/JailSystem/JailSystem.cs @@ -144,7 +144,6 @@ public class JailSystem : GenericPersistence bc.Internalize(); bc.SetControlMaster(null); - bc.SummonMaster = null; bc.IsStabled = true; bc.StabledBy = from;