From f4327e6a3a7112fffc4e1b59ed623ffa1efc4ccd Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 23 Aug 2026 10:50:38 -0700 Subject: [PATCH] refactor: BaseCreature to the SerializationGenerator with SaveFlag elision Converts BaseCreature's hand-written v22 serialization to codegen v23. Nearly every field sits behind a [SaveFlag], so a creature matching its defaults writes only [version int][ulong flags][default AI] - 13 bytes - instead of ~236, and the writer's work is mostly branch-not-taken. With 500k-1M creatures in a world, this is the dominant slice of mobile save freeze time and disk. - Speeds serialize only when they differ from the creature's npc-speeds values (GetSpeeds/GetMoveSpeeds on both the flag check and the load default), so table edits now reach existing unmodified spawns on restart, and former paragons (snapped back to table values) elide fully. CurrentSpeed writes only when it differs from PassiveSpeed. - The delete countdown is a [DeserializeTimer] field (anchored); stabled/controlled pets never persist one, and the abandoned-pet 3-day fallback lives in AfterDeserialization for both load paths. - SummonEnd stays anchored, written only while summoned. - Old saves (v0-22) load through the retained legacy Deserialize(reader, version), now assigning raw fields; the shared post-load fixups (stat timers, AI creation, followers, animate-dead registration, unsummon timer) moved to [AfterDeserialization]. - Side-effect setters became generated-field hooks (Team, Controlled, Summoned, Loyalty clamp, resistance seeds, CurrentSpeed); properties whose semantics the hooks cannot express stay hand-written as [SerializableProperty] (ControlMaster/SummonMaster bracket the assignment with follower bookkeeping, ControlOrder must run on equal re-assignment, Tamable/IsParagon/move speeds have custom getters). - CreatureDeathEvent/CreatureDeletedEvent moved to a CreatureEvents host class: the events generator and the serialization generator each emit a [GeneratedCode] partial for the declaring type, and the attribute forbids duplicates (CS0579). - m_ fields renamed to _camelCase. Tests: new-format round trips (default and fully populated) with exact byte consumption, back-to-back saves byte-identical, and a byte-authentic fossilized v22 stream loading through the legacy path. Co-Authored-By: Claude Fable 5 --- .../Mobiles/BaseCreatureSerializationTests.cs | 275 ++++ .../Spells/Necromancy/BloodOathSpellTests.cs | 2 +- .../Server.Mobiles.BaseCreature.v23.json | 471 ++++++ .../Mobiles/Abilities/MonsterAbility.cs | 4 +- Projects/UOContent/Mobiles/BaseCreature.cs | 1290 +++++++++-------- Projects/UOContent/Mobiles/CreatureEvents.cs | 15 + .../Mobiles/Monsters/LBR/Meers/MeerMage.cs | 2 +- .../Spells/Necromancy/BloodOathSpell.cs | 4 +- .../Spells/Spellweaving/GiftOfLife.cs | 2 +- 9 files changed, 1465 insertions(+), 600 deletions(-) create mode 100644 Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs create mode 100644 Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json create mode 100644 Projects/UOContent/Mobiles/CreatureEvents.cs diff --git a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs new file mode 100644 index 000000000..7425cdab7 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using Server; +using Server.Mobiles; +using Xunit; + +namespace UOContent.Tests.Mobiles; + +// BaseCreature's move to the SerializationGenerator (v23) is guarded three ways: the new +// SaveFlag format round-trips both a default and a fully-populated creature with exact +// byte consumption, back-to-back saves are byte-identical (freeze-time stability), and a +// byte-authentic pre-codegen v22 stream (written by a fossilized replica of the old +// Serialize) loads through the legacy path with the table-speed migration applied. +[Collection("Sequential UOContent Tests")] +public class BaseCreatureSerializationTests : IDisposable +{ + private readonly List _created = new(); + + public void Dispose() + { + for (var i = 0; i < _created.Count; i++) + { + _created[i].Delete(); + } + } + + private class CreatureStub : BaseCreature + { + public CreatureStub() : base(AIType.AI_Melee) => Body = 0xC9; + + public CreatureStub(Serial serial) : base(serial) => Body = 0xC9; + + // Stands in for the npc-speeds table (unconfigured in the test fixture). + public override void GetSpeeds(out double activeSpeed, out double passiveSpeed) + { + activeSpeed = 0.3; + passiveSpeed = 0.6; + } + + public override void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed) + { + activeMoveSpeed = 0.6; + passiveMoveSpeed = 1.2; + } + } + + private CreatureStub NewCreature() + { + var bc = new CreatureStub(); + _created.Add(bc); + return bc; + } + + private static byte[] Snapshot(Mobile m) + { + var writer = new BufferWriter(true); + m.Serialize(writer); + + var buffer = new byte[writer.Position]; + writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer); + return buffer; + } + + private CreatureStub Load(byte[] buffer) + { + var copy = new CreatureStub(World.NewMobile); + _created.Add(copy); + var reader = new BufferReader(buffer); + copy.Deserialize(reader); + + Assert.Equal(buffer.Length, reader.Position); // exact consumption + return copy; + } + + [Fact] + public void DefaultCreature_RoundTrips_AndElidesEverything() + { + var bc = NewCreature(); + + var buffer = Snapshot(bc); + var copy = Load(buffer); + + Assert.Equal(AIType.AI_Melee, copy.AI); + Assert.Equal(BaseCreature.DefaultRangePerception, copy.RangePerception); + Assert.Equal(0.3, copy.ActiveSpeed); + Assert.Equal(0.6, copy.PassiveSpeed); + Assert.Equal(0.6, copy.CurrentSpeed); + Assert.Equal(0.6, copy.ActiveMoveSpeed); // pulled from the table, not the wire + Assert.Equal(1.2, copy.PassiveMoveSpeed); + Assert.Equal(100, copy.PhysicalDamage); + Assert.Equal(BaseCreature.MaxLoyalty, copy.Loyalty); + Assert.Equal(1, copy.ControlSlots); + Assert.NotNull(copy.Owners); + Assert.Empty(copy.Owners); + } + + [Fact] + public void BackToBackSaves_AreByteIdentical() + { + var bc = NewCreature(); + bc.SetDamage(5, 10); + bc.PhysicalResistanceSeed = 25; + + Assert.Equal(Snapshot(bc), Snapshot(bc)); + } + + [Fact] + public void PopulatedCreature_RoundTrips() + { + var bc = NewCreature(); + var master = new PlayerMobile(World.NewMobile); + master.DefaultMobileInit(); + World.AddEntity(master); // ReadEntity resolves the reference through the world table + _created.Add(master); + + bc.Tamable = true; + bc.MinTameSkill = 47.1; + bc.SetControlMaster(master); + bc.Owners.Add(master); + bc.ControlOrder = OrderType.Guard; + bc.SetDamage(11, 17); + bc.SetSpeed(0.2, 0.4); // hand-tuned: no longer matches the stub table + bc.SetMoveSpeed(0.25, 0.5); + bc.PhysicalResistanceSeed = 40; + bc.EnergyResistSeed = 15; + bc.FireDamage = 25; + bc.PhysicalDamage = 75; + bc.HitsMaxSeed = 250; + bc.Loyalty = 55; + bc.Home = new Point3D(1000, 1100, 5); + bc.RangeHome = 4; + bc.Team = 3; + bc.IsBonded = true; + bc.BondingBegin = Core.Now; + bc.RemoveIfUntamed = true; + bc.RemoveStep = 2; + bc.CorpseNameOverride = "a test corpse"; + + var copy = Load(Snapshot(bc)); + + Assert.True(copy.Controlled); + Assert.Equal(master, copy.ControlMaster); + Assert.Equal(OrderType.Guard, copy.ControlOrder); + Assert.True(copy.Tamable); + Assert.Equal(47.1, copy.MinTameSkill); + Assert.Equal(11, copy.DamageMin); + Assert.Equal(17, copy.DamageMax); + Assert.Equal(0.2, copy.ActiveSpeed); + Assert.Equal(0.4, copy.PassiveSpeed); + Assert.Equal(0.25, copy.ActiveMoveSpeed); + Assert.Equal(0.5, copy.PassiveMoveSpeed); + Assert.Equal(40, copy.PhysicalResistanceSeed); + Assert.Equal(15, copy.EnergyResistSeed); + Assert.Equal(25, copy.FireDamage); + Assert.Equal(75, copy.PhysicalDamage); + Assert.Equal(250, copy.HitsMaxSeed); + Assert.Equal(55, copy.Loyalty); + Assert.Equal(new Point3D(1000, 1100, 5), copy.Home); + Assert.Equal(4, copy.RangeHome); + Assert.Equal(3, copy.Team); + Assert.True(copy.IsBonded); + Assert.Equal(bc.BondingBegin, copy.BondingBegin); + Assert.True(copy.RemoveIfUntamed); + Assert.Equal(2, copy.RemoveStep); + Assert.Equal("a test corpse", copy.CorpseNameOverride); + Assert.Equal(master, copy.LastOwner); + } + + private sealed class MobileStub : Mobile + { + public MobileStub() => Body = 0xC9; + } + + // Byte-authentic replica of the pre-codegen v22 tail — fossilized so the legacy + // upgrade path stays covered without an old save binary. The full stream is a plain + // Mobile section (identical layout for every Mobile subclass) followed by this tail. + private static void WriteLegacyV22Tail(IGenericWriter writer) + { + writer.Write(22); // version + writer.Write((int)AIType.AI_Melee); // current AI + writer.Write((int)AIType.AI_Melee); // default AI + writer.Write(10); // RangePerception + writer.Write(1); // RangeFight + writer.Write(0); // Team + writer.Write(0.3); // active (matches the stub table) + writer.Write(0.6); // passive + writer.Write(0.6); // current + writer.Write(2000); // Home X + writer.Write(2100); // Home Y + writer.Write(7); // Home Z + writer.Write(6); // RangeHome + writer.Write((int)FightMode.Closest); + writer.Write(false); // controlled + writer.Write((Mobile)null); // control master + writer.Write((Mobile)null); // control target + writer.Write(Point3D.Zero); // control dest + writer.Write((int)OrderType.None); + writer.Write(0.0); // min tame skill + writer.Write(true); // tamable + writer.Write(false); // summoned + writer.Write(2); // control slots + writer.Write(73); // loyalty + writer.Write((Item)null); // waypoint + writer.Write((Mobile)null); // summon master + writer.Write(180); // hits seed + writer.Write(-1); // stam seed + writer.Write(-1); // mana seed + writer.Write(7); // damage min + writer.Write(14); // damage max + writer.Write(30); // phys resist + writer.Write(100); // phys damage + writer.Write(10); // fire resist + writer.Write(0); // fire damage + writer.Write(0); // cold resist + writer.Write(0); // cold damage + writer.Write(0); // poison resist + writer.Write(0); // poison damage + writer.Write(0); // energy resist + writer.Write(0); // energy damage + writer.Write(new List()); // owners + writer.Write(false); // dead pet + writer.Write(false); // bonded + writer.Write(DateTime.MinValue); // bonding begin + writer.Write(DateTime.MinValue); // abandon time + writer.Write(true); // has generated loot + writer.Write(false); // paragon + writer.Write(false); // has friends + writer.Write(false); // remove if untamed + writer.Write(0); // remove step + writer.Write(TimeSpan.Zero); // delete time left + writer.Write((string)null); // corpse name override + writer.Write((Map)null); // home map + writer.Write(0.0); // active move speed (v22) + writer.Write(0.0); // passive move speed (v22) + } + + [Fact] + public void LegacyV22Stream_LoadsThroughLegacyPath() + { + // Every serialized BaseCreature starts with the Mobile base section; a plain + // Mobile donor produces a byte-authentic one. + var donor = new MobileStub(); + donor.DefaultMobileInit(); + _created.Add(donor); + + var writer = new BufferWriter(true); + donor.Serialize(writer); + WriteLegacyV22Tail(writer); + + var buffer = new byte[writer.Position]; + writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer); + + var copy = new CreatureStub(World.NewMobile); + _created.Add(copy); + var reader = new BufferReader(buffer); + copy.Deserialize(reader); + + Assert.Equal(buffer.Length, reader.Position); + Assert.Equal(10, copy.RangePerception); + Assert.Equal(new Point3D(2000, 2100, 7), copy.Home); + Assert.Equal(6, copy.RangeHome); + Assert.True(copy.Tamable); + Assert.Equal(2, copy.ControlSlots); + Assert.Equal(73, copy.Loyalty); + Assert.Equal(180, copy.HitsMaxSeed); + Assert.Equal(7, copy.DamageMin); + Assert.Equal(14, copy.DamageMax); + Assert.Equal(30, copy.PhysicalResistanceSeed); + Assert.Equal(10, copy.FireResistSeed); + Assert.Equal(0.3, copy.ActiveSpeed); + // v22 wrote explicit zeros for the move overrides ("inherit"), so the resolved + // pace falls back to the think clock through the resolving getters. + Assert.Equal(0.3, copy.ActiveMoveSpeed); + } +} diff --git a/Projects/UOContent.Tests/Tests/Spells/Necromancy/BloodOathSpellTests.cs b/Projects/UOContent.Tests/Tests/Spells/Necromancy/BloodOathSpellTests.cs index 0264a282e..08658b1a1 100644 --- a/Projects/UOContent.Tests/Tests/Spells/Necromancy/BloodOathSpellTests.cs +++ b/Projects/UOContent.Tests/Tests/Spells/Necromancy/BloodOathSpellTests.cs @@ -121,7 +121,7 @@ public class BloodOathSpellTests BloodOathSpell.RegisterOath(caster, target, TimeSpan.FromMinutes(5)); - BaseCreature.CreatureDeletedEvent(target); // central handler breaks the oath from the target side + CreatureEvents.CreatureDeletedEvent(target); // central handler breaks the oath from the target side Assert.Null(BloodOathSpell.GetBloodOath(target)); Assert.False(BloodOathSpell.RemoveCurse(caster)); diff --git a/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json b/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json new file mode 100644 index 000000000..b700154ab --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json @@ -0,0 +1,471 @@ +{ + "version": 23, + "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": "ControlMaster", + "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": "SummonMaster", + "type": "Server.Mobile", + "usesSaveFlag": true, + "rule": "SerializableInterfaceMigrationRule" + }, + { + "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/Abilities/MonsterAbility.cs b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs index fc227507e..75c6fc3dc 100644 --- a/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs +++ b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs @@ -99,8 +99,8 @@ public abstract class MonsterAbility { } - [OnEvent(nameof(BaseCreature.CreatureDeathEvent))] - [OnEvent(nameof(BaseCreature.CreatureDeletedEvent))] + [OnEvent(nameof(CreatureEvents.CreatureDeathEvent))] + [OnEvent(nameof(CreatureEvents.CreatureDeletedEvent))] public static void InvalidateNextAbilityTriggers(BaseCreature source) { var abilities = source.GetMonsterAbilities(); diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index f64a289f4..3953916fc 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using ModernUO.CodeGeneratedEvents; +using ModernUO.Serialization; using Server.Collections; using Server.ContextMenus; using Server.Engines.ConPVP; @@ -133,6 +134,7 @@ namespace Server.Mobiles public int CompareTo(DamageStore ds) => (ds?.m_Damage ?? 0).CompareTo(m_Damage); } + [SerializationGenerator(23, false)] public abstract partial class BaseCreature : Mobile, IHonorTarget, IQuestGiver { public enum Allegiance @@ -250,54 +252,518 @@ namespace Server.Mobiles typeof(AncientSmithyHammer), typeof(Scorp) }; - private bool _summoned; + // --- Serialized state --------------------------------------------------------- + // Nearly every field is behind a [SaveFlag] so a creature that matches its + // defaults (including npc-speeds table values) writes only the version and flags. - private bool m_bTamable; - private int m_ColdResistance; + [SerializableField(0, setter: "private")] + private AIType _defaultAI; - private bool _controlled; // Is controlled - private Mobile m_ControlMaster; // My master - private OrderType m_ControlOrder; // My order + [SerializableField(1, setter: "private")] + [SaveFlag(nameof(ShouldSerializeCurrentAI), nameof(CurrentAIDefaultValue))] + private AIType _currentAI; - private AIType m_CurrentAI; // The current AI + private bool ShouldSerializeCurrentAI() => _currentAI != _defaultAI; + private AIType CurrentAIDefaultValue() => _defaultAI; + + [EncodedInt] + [SerializableField(2)] + [SaveFlag(nameof(ShouldSerializeRangePerception), nameof(RangePerceptionDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _rangePerception; + + private bool ShouldSerializeRangePerception() => _rangePerception != DefaultRangePerception; + + private int RangePerceptionDefaultValue() => DefaultRangePerception; + + [EncodedInt] + [SerializableField(3)] + [SaveFlag(nameof(ShouldSerializeRangeFight), nameof(RangeFightDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _rangeFight; + + private bool ShouldSerializeRangeFight() => _rangeFight != 1; + + private int RangeFightDefaultValue() => 1; + + [EncodedInt] + [SerializableField(4)] + [SaveFlag(nameof(ShouldSerializeRangeHome), nameof(RangeHomeDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _rangeHome = 10; + + private bool ShouldSerializeRangeHome() => _rangeHome != 10; + + private int RangeHomeDefaultValue() => 10; + + [EncodedInt] + [SerializableField(5, fieldChanged: nameof(OnTeamChange))] + [SaveFlag(nameof(ShouldSerializeTeam))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _team; + + private bool ShouldSerializeTeam() => _team != 0; + + private void OnTeamChange(int oldValue, int newValue) => OnTeamChange(); + + [SerializableField(6)] + [SaveFlag(nameof(ShouldSerializeFightMode), nameof(FightModeDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private FightMode _fightMode; + + private bool ShouldSerializeFightMode() => _fightMode != FightMode.Closest; + + private FightMode FightModeDefaultValue() => FightMode.Closest; + + /// Seconds per AI decision while engaged; see for movement pace. + [SerializableField(7, isVirtual: true)] + [SaveFlag(nameof(ShouldSerializeActiveSpeed), nameof(ActiveSpeedDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] private double _activeSpeed; + + private bool ShouldSerializeActiveSpeed() + { + GetSpeeds(out var activeSpeed, out _); + return _activeSpeed != activeSpeed; + } + + private double ActiveSpeedDefaultValue() + { + GetSpeeds(out var activeSpeed, out _); + return activeSpeed; + } + + /// Seconds per AI decision while idle; see for movement pace. + [SerializableField(8, isVirtual: true)] + [SaveFlag(nameof(ShouldSerializePassiveSpeed), nameof(PassiveSpeedDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] private double _passiveSpeed; + + private bool ShouldSerializePassiveSpeed() + { + GetSpeeds(out _, out var passiveSpeed); + return _passiveSpeed != passiveSpeed; + } + + private double PassiveSpeedDefaultValue() + { + GetSpeeds(out _, out var passiveSpeed); + return passiveSpeed; + } + + [SerializableField(9, fieldChanged: nameof(OnCurrentSpeedChange))] + [SaveFlag(nameof(ShouldSerializeCurrentSpeed), nameof(CurrentSpeedDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] private double _currentSpeed; + private bool ShouldSerializeCurrentSpeed() => _currentSpeed != _passiveSpeed; + + private double CurrentSpeedDefaultValue() => _passiveSpeed; + + private void OnCurrentSpeedChange(double oldValue, double newValue) => AIObject?.OnCurrentSpeedChanged(); + // Movement clock (seconds per step); 0 = inherit the matching think value. + // Serialized through the hand-written resolving properties (fields 10 and 11). private double _activeMoveSpeed; private double _passiveMoveSpeed; + private bool ShouldSerializeActiveMoveSpeed() + { + GetMoveSpeeds(out var activeMoveSpeed, out _); + return _activeMoveSpeed != activeMoveSpeed; + } + + private double ActiveMoveSpeedDefaultValue() + { + GetMoveSpeeds(out var activeMoveSpeed, out _); + return activeMoveSpeed; + } + + private bool ShouldSerializePassiveMoveSpeed() + { + GetMoveSpeeds(out _, out var passiveMoveSpeed); + return _passiveMoveSpeed != passiveMoveSpeed; + } + + private double PassiveMoveSpeedDefaultValue() + { + GetMoveSpeeds(out _, out var passiveMoveSpeed); + return passiveMoveSpeed; + } + + [SerializableField(12)] + [SaveFlag(nameof(ShouldSerializeHome))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private Point3D _home; + + private bool ShouldSerializeHome() => _home != Point3D.Zero; + + [SerializableField(13)] + [SaveFlag(nameof(ShouldSerializeHomeMap))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private Map _homeMap; + + private bool ShouldSerializeHomeMap() => _homeMap != null; + + [SerializableField(14, fieldChanged: nameof(OnControlledChange))] + [SaveFlag(nameof(ShouldSerializeControlled))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private bool _controlled; + + private bool ShouldSerializeControlled() => _controlled; + + private void OnControlledChange(bool oldValue, bool newValue) + { + Delta(MobileDelta.Noto); + InvalidateProperties(); + } + + // Field 15: ControlMaster (hand-written property; follower bookkeeping brackets the assignment) + private Mobile _controlMaster; + + private bool ShouldSerializeControlMaster() => _controlMaster != null; + + [SerializableField(16)] + [SaveFlag(nameof(ShouldSerializeControlTarget))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private Mobile _controlTarget; + + private bool ShouldSerializeControlTarget() => _controlTarget != null; + + [SerializableField(17)] + [SaveFlag(nameof(ShouldSerializeControlDest))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private Point3D _controlDest; + + private bool ShouldSerializeControlDest() => _controlDest != Point3D.Zero; + + // Field 18: ControlOrder (hand-written property; order logic must run on equal re-assignment) + private OrderType _controlOrder; + + private bool ShouldSerializeControlOrder() => _controlOrder != OrderType.None; + + [SerializableField(19)] + [SaveFlag(nameof(ShouldSerializeMinTameSkill))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private double _minTameSkill; + + private bool ShouldSerializeMinTameSkill() => _minTameSkill != 0; + + // Field 20: Tamable (hand-written property; custom getter masks paragons) + private bool _tamable; + + private bool ShouldSerializeTamable() => _tamable; + + [SerializableField(21, fieldChanged: nameof(OnSummonedChange))] + [SaveFlag(nameof(ShouldSerializeSummoned))] + [SerializedCommandProperty(AccessLevel.Administrator)] + private bool _summoned; + + private bool ShouldSerializeSummoned() => _summoned; + + private void OnSummonedChange(bool oldValue, bool newValue) + { + NextReacquireTime = Core.TickCount; + Delta(MobileDelta.Noto); + InvalidateProperties(); + } + + [AnchoredDateTime] + [SerializableField(22, getter: "protected", setter: "protected")] + [SaveFlag(nameof(ShouldSerializeSummonEnd))] + private DateTime _summonEnd; + + private bool ShouldSerializeSummonEnd() => _summoned; + + // Field 23: SummonMaster (hand-written property; follower bookkeeping brackets the assignment) + private Mobile _summonMaster; + + private bool ShouldSerializeSummonMaster() => _summonMaster != null; + + [EncodedInt] + [SerializableField(24)] + [SaveFlag(nameof(ShouldSerializeControlSlots), nameof(ControlSlotsDefaultValue))] + [SerializedCommandProperty(AccessLevel.Administrator)] + private int _controlSlots = 1; + + private bool ShouldSerializeControlSlots() => _controlSlots != 1; + + private int ControlSlotsDefaultValue() => 1; + + [EncodedInt] + [SerializableField(25, allowFieldChange: nameof(ClampLoyalty))] + [SaveFlag(nameof(ShouldSerializeLoyalty), nameof(LoyaltyDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _loyalty; + + private bool ShouldSerializeLoyalty() => _loyalty != MaxLoyalty; + + private int LoyaltyDefaultValue() => MaxLoyalty; + + private bool ClampLoyalty(ref int value) + { + value = Math.Clamp(value, 0, MaxLoyalty); + return true; + } + + [SerializableField(26)] + [SaveFlag(nameof(ShouldSerializeCurrentWayPoint))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private WayPoint _currentWayPoint; + + private bool ShouldSerializeCurrentWayPoint() => _currentWayPoint != null; + + [EncodedInt] + [SerializableField(27)] + [SaveFlag(nameof(ShouldSerializeHitsMaxSeed), nameof(HitsMaxSeedDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _hitsMaxSeed = -1; + + private bool ShouldSerializeHitsMaxSeed() => _hitsMaxSeed != -1; + + private int HitsMaxSeedDefaultValue() => -1; + + [EncodedInt] + [SerializableField(28)] + [SaveFlag(nameof(ShouldSerializeStamMaxSeed), nameof(StamMaxSeedDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _stamMaxSeed = -1; + + private bool ShouldSerializeStamMaxSeed() => _stamMaxSeed != -1; + + private int StamMaxSeedDefaultValue() => -1; + + [EncodedInt] + [SerializableField(29)] + [SaveFlag(nameof(ShouldSerializeManaMaxSeed), nameof(ManaMaxSeedDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _manaMaxSeed = -1; + + private bool ShouldSerializeManaMaxSeed() => _manaMaxSeed != -1; + + private int ManaMaxSeedDefaultValue() => -1; + + [EncodedInt] + [SerializableField(30, isVirtual: true)] + [SaveFlag(nameof(ShouldSerializeDamageMin), nameof(DamageMinDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _damageMin = -1; + + private bool ShouldSerializeDamageMin() => _damageMin != -1; + + private int DamageMinDefaultValue() => -1; + + [EncodedInt] + [SerializableField(31, isVirtual: true)] + [SaveFlag(nameof(ShouldSerializeDamageMax), nameof(DamageMaxDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _damageMax = -1; + + private bool ShouldSerializeDamageMax() => _damageMax != -1; + + private int DamageMaxDefaultValue() => -1; + + [EncodedInt] + [SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))] + [SaveFlag(nameof(ShouldSerializePhysicalResistanceSeed))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _physicalResistanceSeed; + + private bool ShouldSerializePhysicalResistanceSeed() => _physicalResistanceSeed != 0; + + private void OnResistanceSeedChange(int oldValue, int newValue) => UpdateResistances(); + + [EncodedInt] + [SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))] + [SaveFlag(nameof(ShouldSerializeFireResistSeed))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _fireResistSeed; + + private bool ShouldSerializeFireResistSeed() => _fireResistSeed != 0; + + [EncodedInt] + [SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))] + [SaveFlag(nameof(ShouldSerializeColdResistSeed))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _coldResistSeed; + + private bool ShouldSerializeColdResistSeed() => _coldResistSeed != 0; + + [EncodedInt] + [SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))] + [SaveFlag(nameof(ShouldSerializePoisonResistSeed))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _poisonResistSeed; + + private bool ShouldSerializePoisonResistSeed() => _poisonResistSeed != 0; + + [EncodedInt] + [SerializableField(36, fieldChanged: nameof(OnResistanceSeedChange))] + [SaveFlag(nameof(ShouldSerializeEnergyResistSeed))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _energyResistSeed; + + private bool ShouldSerializeEnergyResistSeed() => _energyResistSeed != 0; + + [EncodedInt] + [SerializableField(37)] + [SaveFlag(nameof(ShouldSerializePhysicalDamage), nameof(PhysicalDamageDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _physicalDamage = 100; + + private bool ShouldSerializePhysicalDamage() => _physicalDamage != 100; + + private int PhysicalDamageDefaultValue() => 100; + + [EncodedInt] + [SerializableField(38)] + [SaveFlag(nameof(ShouldSerializeFireDamage))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _fireDamage; + + private bool ShouldSerializeFireDamage() => _fireDamage != 0; + + [EncodedInt] + [SerializableField(39)] + [SaveFlag(nameof(ShouldSerializeColdDamage))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _coldDamage; + + private bool ShouldSerializeColdDamage() => _coldDamage != 0; + + [EncodedInt] + [SerializableField(40)] + [SaveFlag(nameof(ShouldSerializePoisonDamage))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _poisonDamage; + + private bool ShouldSerializePoisonDamage() => _poisonDamage != 0; + + [EncodedInt] + [SerializableField(41)] + [SaveFlag(nameof(ShouldSerializeEnergyDamage))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _energyDamage; + + private bool ShouldSerializeEnergyDamage() => _energyDamage != 0; + + [Tidy] + [SerializableField(42, setter: "private")] + [SaveFlag(nameof(ShouldSerializeOwners), nameof(OwnersDefaultValue))] + private List _owners; + + private bool ShouldSerializeOwners() + { + _owners?.Tidy(); + return _owners?.Count > 0; + } + + private List OwnersDefaultValue() => new(); + + [SerializableField(43)] + [SaveFlag(nameof(ShouldSerializeIsDeadPet))] + private bool _isDeadPet; + + private bool ShouldSerializeIsDeadPet() => _isDeadPet; + + [SerializableField(44, fieldChanged: nameof(OnBondedChange))] + [SaveFlag(nameof(ShouldSerializeIsBonded))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private bool _isBonded; + + private bool ShouldSerializeIsBonded() => _isBonded; + + private void OnBondedChange(bool oldValue, bool newValue) => InvalidateProperties(); + + [SerializableField(45)] + [SaveFlag(nameof(ShouldSerializeBondingBegin))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private DateTime _bondingBegin; + + private bool ShouldSerializeBondingBegin() => _bondingBegin != DateTime.MinValue; + + [SerializableField(46)] + [SaveFlag(nameof(ShouldSerializeOwnerAbandonTime))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private DateTime _ownerAbandonTime; + + private bool ShouldSerializeOwnerAbandonTime() => _ownerAbandonTime != DateTime.MinValue; + + [SerializableField(47)] + [SaveFlag(nameof(ShouldSerializeHasGeneratedLoot))] + private bool _hasGeneratedLoot; + + private bool ShouldSerializeHasGeneratedLoot() => _hasGeneratedLoot; + + // Field 48: IsParagon (hand-written property; the setter converts, which must not run at load) + private bool _isParagon; + + private bool ShouldSerializeIsParagon() => _isParagon; + + [Tidy] + [SerializableField(49, setter: "private")] + [SaveFlag(nameof(ShouldSerializeFriends))] + private List _friends; + + private bool ShouldSerializeFriends() + { + _friends?.Tidy(); + return _friends?.Count > 0; + } + + [SerializableField(50)] + [SaveFlag(nameof(ShouldSerializeRemoveIfUntamed))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private bool _removeIfUntamed; + + private bool ShouldSerializeRemoveIfUntamed() => _removeIfUntamed; + + [EncodedInt] + [SerializableField(51)] + [SaveFlag(nameof(ShouldSerializeRemoveStep))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _removeStep; + + private bool ShouldSerializeRemoveStep() => _removeStep != 0; + + [SerializableField(52, setter: "private")] + [SaveFlag(nameof(ShouldSerializePendingDeleteTimer))] + [DeserializeTimer(nameof(DeserializePendingDeleteTimer))] + private Timer _pendingDeleteTimer; + + // Stabled and controlled pets never resume a delete countdown (legacy parity). + private bool ShouldSerializePendingDeleteTimer() => + _pendingDeleteTimer?.Running == true && !IsStabled && !(_controlled && _controlMaster != null); + + private void DeserializePendingDeleteTimer(TimeSpan delay) + { + _pendingDeleteTimer = new DeleteTimer(this, delay); + _pendingDeleteTimer.Start(); + } + + [SerializableField(53)] + [SaveFlag(nameof(ShouldSerializeCorpseNameOverride))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private string _corpseNameOverride; + + private bool ShouldSerializeCorpseNameOverride() => _corpseNameOverride != null; + + // --- Non-serialized state ------------------------------------------------------- + // Herding - forces the mob to walk to a specific location, paced by the movement // clock at HerdingMoveSpeed. Thinking is unaffected. private IPoint2D _targetLocation; - private int m_DamageMax = -1; - - private int m_DamageMin = -1; - private AIType m_DefaultAI; // The default AI - - private DeleteTimer m_DeleteTimer; - private int m_EnergyResistance; - private int m_FailedReturnHome; /* return to home failure counter */ - private int m_FireResistance; - private bool m_HasGeneratedLoot; // have we generated our loot yet? private TimerExecutionToken _healTimerToken; - private Point3D m_Home; // The home position of the creature, used by some AI - private DateTime m_IdleReleaseTime; - private bool m_IsBonded; - private bool m_IsStabled; protected int m_KillersLuck; - private int m_Loyalty; - private DateTime m_MLNextShout; private List m_MLQuests; @@ -310,11 +776,6 @@ namespace Server.Mobiles private long m_NextRummageTime; - private bool m_Paragon; - - private int m_PhysicalResistance; - private int m_PoisonResistance; - /* until we are sure about who should be getting deleted, move them instead */ /* On OSI, they despawn */ @@ -322,12 +783,8 @@ namespace Server.Mobiles protected bool m_Spawning; - private Mobile m_SummonMaster; - private SkillName m_Teaching = (SkillName)(-1); - private int m_Team; // Monster Team - public BaseCreature( AIType ai, FightMode mode = FightMode.Closest, @@ -335,10 +792,10 @@ namespace Server.Mobiles int iRangeFight = 1 ) { - m_Loyalty = MaxLoyalty; // Wonderfully Happy + _loyalty = MaxLoyalty; // Wonderfully Happy - m_CurrentAI = ai; - m_DefaultAI = ai; + _currentAI = ai; + _defaultAI = ai; RangePerception = iRangePerception; RangeFight = iRangeFight; @@ -352,16 +809,16 @@ namespace Server.Mobiles PassiveSpeed = passiveSpeed; CurrentSpeed = passiveSpeed; - m_Team = 0; + _team = 0; Debug = false; _controlled = false; - m_ControlMaster = null; + _controlMaster = null; ControlTarget = null; - m_ControlOrder = OrderType.None; + _controlOrder = OrderType.None; - m_bTamable = false; + _tamable = false; Owners = new List(); @@ -411,9 +868,6 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public bool SeeksHome { get; set; } - [CommandProperty(AccessLevel.GameMaster)] - public string CorpseNameOverride { get; set; } - [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] public bool IsStabled { @@ -436,20 +890,20 @@ namespace Server.Mobiles public virtual bool FollowsAcquireRules => true; - protected DateTime SummonEnd { get; set; } - public virtual Faction FactionAllegiance => null; public virtual int FactionSilverWorth => 30; public virtual double WeaponAbilityChance => 0.4; + [SerializableProperty(48, useField: nameof(_isParagon))] + [SaveFlag(nameof(ShouldSerializeIsParagon))] [CommandProperty(AccessLevel.GameMaster)] public bool IsParagon { - get => m_Paragon; + get => _isParagon; set { - if (m_Paragon == value) + if (_isParagon == value) { return; } @@ -463,9 +917,10 @@ namespace Server.Mobiles Paragon.UnConvert(this); } - m_Paragon = value; + _isParagon = value; InvalidateProperties(); + this.MarkDirty(); } } @@ -474,8 +929,6 @@ namespace Server.Mobiles public virtual FoodType FavoriteFood => FoodType.Meat; public virtual PackInstinct PackInstinct => PackInstinct.None; - public List Owners { get; private set; } - public virtual bool AllowMaleTamer => true; public virtual bool AllowFemaleTamer => true; public virtual bool SubdueBeforeTame => false; @@ -539,21 +992,11 @@ namespace Server.Mobiles } public virtual bool IsNecroFamiliar => - Summoned && m_ControlMaster != null && - SummonFamiliarSpell.Table.TryGetValue(m_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; - [CommandProperty(AccessLevel.GameMaster)] - public int Loyalty - { - get => m_Loyalty; - set => m_Loyalty = Math.Clamp(value, 0, MaxLoyalty); - } - - [CommandProperty(AccessLevel.GameMaster)] - public WayPoint CurrentWayPoint { get; set; } - public virtual Mobile ConstantFocus => null; public virtual bool DisallowAllMoves => false; @@ -566,41 +1009,18 @@ namespace Server.Mobiles public virtual bool AlwaysAttackable => false; - [CommandProperty(AccessLevel.GameMaster)] - public virtual int DamageMin - { - get => m_DamageMin; - set => m_DamageMin = value; - } - - [CommandProperty(AccessLevel.GameMaster)] - public virtual int DamageMax - { - get => m_DamageMax; - set => m_DamageMax = value; - } - [CommandProperty(AccessLevel.GameMaster)] public override int HitsMax => HitsMaxSeed <= 0 ? Str : Math.Clamp(HitsMaxSeed + GetStatOffset(StatType.Str), 1, 65000); - [CommandProperty(AccessLevel.GameMaster)] - public int HitsMaxSeed { get; set; } = -1; - [CommandProperty(AccessLevel.GameMaster)] public override int StamMax => StamMaxSeed <= 0 ? Dex : Math.Clamp(StamMaxSeed + GetStatOffset(StatType.Dex), 1, 65000); - [CommandProperty(AccessLevel.GameMaster)] - public int StamMaxSeed { get; set; } = -1; - [CommandProperty(AccessLevel.GameMaster)] public override int ManaMax => ManaMaxSeed <= 0 ? Int : Math.Clamp(ManaMaxSeed + GetStatOffset(StatType.Int), 1, 65000); - [CommandProperty(AccessLevel.GameMaster)] - public int ManaMaxSeed { get; set; } = -1; - public virtual bool CanOpenDoors => !Body.IsAnimal && !Body.IsSea; public virtual bool CanMoveOverObstacles => Core.AOS || Body.IsMonster; @@ -628,43 +1048,26 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public AIType AI { - get => m_CurrentAI; + get => _currentAI; set { - m_CurrentAI = value; + _currentAI = value; - if (m_CurrentAI == AIType.AI_Use_Default) + if (_currentAI == AIType.AI_Use_Default) { - m_CurrentAI = m_DefaultAI; + _currentAI = _defaultAI; } - ChangeAIType(m_CurrentAI); + ChangeAIType(_currentAI); } } [CommandProperty(AccessLevel.Administrator)] public bool Debug { get; set; } - [CommandProperty(AccessLevel.GameMaster)] - public int Team - { - get => m_Team; - set - { - m_Team = value; - OnTeamChange(); - } - } - [CommandProperty(AccessLevel.GameMaster)] public Mobile FocusMob { get; set; } - [CommandProperty(AccessLevel.GameMaster)] - public FightMode FightMode { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int RangePerception { get; set; } - /// /// How far a chase may stretch before the creature gives up its combatant. Between /// RangePerception and this leash it keeps chasing but may switch to closer targets. @@ -672,55 +1075,32 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public virtual int ChaseLeashRange => RangePerception * 2; - [CommandProperty(AccessLevel.GameMaster)] - public int RangeFight { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int RangeHome { get; set; } = 10; - - /// Seconds per AI decision while engaged; see for movement pace. - [CommandProperty(AccessLevel.GameMaster)] - public virtual double ActiveSpeed - { - get => _activeSpeed; - set - { - if (Math.Abs(_activeSpeed - value) > .0001) - { - _activeSpeed = value; - } - } - } - - /// Seconds per AI decision while idle; see for movement pace. - [CommandProperty(AccessLevel.GameMaster)] - public virtual double PassiveSpeed - { - get => _passiveSpeed; - set - { - _passiveSpeed = value; - if (Math.Abs(_passiveSpeed - value) > .0001) - { - _passiveSpeed = value; - } - } - } - /// Seconds per step while engaged. Inherits ; set 0 to re-inherit. + [SerializableProperty(10, useField: nameof(_activeMoveSpeed))] + [SaveFlag(nameof(ShouldSerializeActiveMoveSpeed), nameof(ActiveMoveSpeedDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public virtual double ActiveMoveSpeed { get => _activeMoveSpeed > 0 ? _activeMoveSpeed : _activeSpeed; - set => _activeMoveSpeed = value > 0 ? value : 0; + set + { + _activeMoveSpeed = value > 0 ? value : 0; + this.MarkDirty(); + } } /// Seconds per step while idle. Inherits ; set 0 to re-inherit. + [SerializableProperty(11, useField: nameof(_passiveMoveSpeed))] + [SaveFlag(nameof(ShouldSerializePassiveMoveSpeed), nameof(PassiveMoveSpeedDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public virtual double PassiveMoveSpeed { get => _passiveMoveSpeed > 0 ? _passiveMoveSpeed : _passiveSpeed; - set => _passiveMoveSpeed = value > 0 ? value : 0; + set + { + _passiveMoveSpeed = value > 0 ? value : 0; + this.MarkDirty(); + } } // Herded creatures walk at a fixed standard pace regardless of their own speed @@ -734,20 +1114,6 @@ namespace Server.Mobiles set => _targetLocation = value; } - [CommandProperty(AccessLevel.GameMaster)] - public double CurrentSpeed - { - get => _currentSpeed; - set - { - if (Math.Abs(_currentSpeed - value) > 0.0001) - { - _currentSpeed = value; - AIObject?.OnCurrentSpeedChanged(); - } - } - } - /// /// Resolved seconds per step: a verbatim active/passive /// maps to the matching movement value; a bespoke pace stays fused to both clocks. @@ -769,96 +1135,73 @@ namespace Server.Mobiles } } - [CommandProperty(AccessLevel.GameMaster)] - public Point3D Home - { - get => m_Home; - set => m_Home = value; - } - - [CommandProperty(AccessLevel.GameMaster)] - public Map HomeMap { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public bool Controlled - { - get => _controlled; - set - { - if (_controlled == value) - { - return; - } - - _controlled = value; - Delta(MobileDelta.Noto); - - InvalidateProperties(); - } - } - + [SerializableProperty(15, useField: nameof(_controlMaster))] + [SaveFlag(nameof(ShouldSerializeControlMaster))] [CommandProperty(AccessLevel.GameMaster)] public Mobile ControlMaster { - get => m_ControlMaster; + get => _controlMaster; set { - if (m_ControlMaster == value || this == value) + if (_controlMaster == value || this == value) { return; } RemoveFollowers(); - m_ControlMaster = value; + _controlMaster = value; AddFollowers(); - if (m_ControlMaster != null) + if (_controlMaster != null) { StopDeleteTimer(); } Delta(MobileDelta.Noto); + this.MarkDirty(); } } + [SerializableProperty(23, useField: nameof(_summonMaster))] + [SaveFlag(nameof(ShouldSerializeSummonMaster))] [CommandProperty(AccessLevel.GameMaster)] public Mobile SummonMaster { - get => m_SummonMaster; + get => _summonMaster; set { - if (m_SummonMaster == value || this == value) + if (_summonMaster == value || this == value) { return; } RemoveFollowers(); - m_SummonMaster = value; + _summonMaster = value; AddFollowers(); Delta(MobileDelta.Noto); + this.MarkDirty(); } } - [CommandProperty(AccessLevel.GameMaster)] - public Mobile ControlTarget { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public Point3D ControlDest { get; set; } - + // Re-issuing the current order must still run the order logic (pet commands), so + // this keeps a hand-written setter with no equality skip. + [SerializableProperty(18, useField: nameof(_controlOrder))] + [SaveFlag(nameof(ShouldSerializeControlOrder))] [CommandProperty(AccessLevel.GameMaster)] public OrderType ControlOrder { - get => m_ControlOrder; + get => _controlOrder; set { - var previous = m_ControlOrder; - m_ControlOrder = value; + var previous = _controlOrder; + _controlOrder = value; AIObject?.OnCurrentOrderChanged(previous); InvalidateProperties(); - m_ControlMaster?.InvalidateProperties(); + _controlMaster?.InvalidateProperties(); + this.MarkDirty(); } } @@ -877,39 +1220,19 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public DateTime BardEndTime { get; set; } - [CommandProperty(AccessLevel.GameMaster)] - public double MinTameSkill { get; set; } - + [SerializableProperty(20, useField: nameof(_tamable))] + [SaveFlag(nameof(ShouldSerializeTamable))] [CommandProperty(AccessLevel.GameMaster)] public bool Tamable { - get => m_bTamable && !m_Paragon; - set => m_bTamable = value; - } - - [CommandProperty(AccessLevel.Administrator)] - public bool Summoned - { - get => _summoned; + get => _tamable && !_isParagon; set { - if (_summoned == value) - { - return; - } - - NextReacquireTime = Core.TickCount; - - _summoned = value; - Delta(MobileDelta.Noto); - - InvalidateProperties(); + _tamable = value; + this.MarkDirty(); } } - [CommandProperty(AccessLevel.Administrator)] - public int ControlSlots { get; set; } = 1; - public virtual bool NoHouseRestrictions => false; public virtual bool IsHouseSummonable => false; @@ -945,7 +1268,7 @@ namespace Server.Mobiles public virtual TimeSpan ReacquireDelay => TimeSpan.FromSeconds(10.0); public virtual bool ReacquireOnMovement => false; - public virtual bool AcquireOnApproach => m_Paragon; + public virtual bool AcquireOnApproach => _isParagon; public virtual int AcquireOnApproachRange => 10; public static bool Summoning { get; set; } @@ -958,13 +1281,6 @@ namespace Server.Mobiles public virtual bool ReturnsToHome => SeeksHome && Home != Point3D.Zero && !m_ReturnQueued && !Controlled && !Summoned; - // used for deleting untamed creatures [in houses] - [CommandProperty(AccessLevel.GameMaster)] - public bool RemoveIfUntamed { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int RemoveStep { get; set; } - public virtual bool CanGiveMLQuest => MLQuests.Count != 0; public virtual bool StaticMLQuester => true; @@ -999,114 +1315,25 @@ namespace Server.Mobiles } } - [CommandProperty(AccessLevel.GameMaster)] - public bool IsBonded - { - get => m_IsBonded; - set - { - m_IsBonded = value; - InvalidateProperties(); - } - } - - public bool IsDeadPet { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public DateTime BondingBegin { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public DateTime OwnerAbandonTime { get; set; } - [CommandProperty(AccessLevel.GameMaster)] public TimeSpan DeleteTimeLeft { get { - if (m_DeleteTimer?.Running == true) + if (_pendingDeleteTimer?.Running == true) { - return m_DeleteTimer.Next - Core.Now; + return _pendingDeleteTimer.Next - Core.Now; } return TimeSpan.Zero; } } - public override int BasePhysicalResistance => m_PhysicalResistance; - public override int BaseFireResistance => m_FireResistance; - public override int BaseColdResistance => m_ColdResistance; - public override int BasePoisonResistance => m_PoisonResistance; - public override int BaseEnergyResistance => m_EnergyResistance; - - [CommandProperty(AccessLevel.GameMaster)] - public int PhysicalResistanceSeed - { - get => m_PhysicalResistance; - set - { - m_PhysicalResistance = value; - UpdateResistances(); - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public int FireResistSeed - { - get => m_FireResistance; - set - { - m_FireResistance = value; - UpdateResistances(); - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public int ColdResistSeed - { - get => m_ColdResistance; - set - { - m_ColdResistance = value; - UpdateResistances(); - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public int PoisonResistSeed - { - get => m_PoisonResistance; - set - { - m_PoisonResistance = value; - UpdateResistances(); - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public int EnergyResistSeed - { - get => m_EnergyResistance; - set - { - m_EnergyResistance = value; - UpdateResistances(); - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public int PhysicalDamage { get; set; } = 100; - - [CommandProperty(AccessLevel.GameMaster)] - public int FireDamage { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int ColdDamage { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int PoisonDamage { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int EnergyDamage { get; set; } + public override int BasePhysicalResistance => _physicalResistanceSeed; + public override int BaseFireResistance => _fireResistSeed; + public override int BaseColdResistance => _coldResistSeed; + public override int BasePoisonResistance => _poisonResistSeed; + public override int BaseEnergyResistance => _energyResistSeed; [CommandProperty(AccessLevel.GameMaster)] public int ChaosDamage { get; set; } @@ -1117,12 +1344,10 @@ namespace Server.Mobiles // Is immune to breath damages public virtual bool BreathImmune => false; - public virtual bool CanFlee => !m_Paragon; + public virtual bool CanFlee => !_isParagon; public DateTime EndFleeTime { get; set; } - public List Friends { get; private set; } - public virtual bool AllowNewPetFriend => Friends == null || Friends.Count < 5; public virtual Ethic EthicAllegiance => null; @@ -1400,7 +1625,7 @@ namespace Server.Mobiles return false; } - if (m_Team != c.Team || FightMode == FightMode.Evil && m.Karma < 0 || c.FightMode == FightMode.Evil && Karma < 0) + if (_team != c.Team || FightMode == FightMode.Evil && m.Karma < 0 || c.FightMode == FightMode.Evil && Karma < 0) { return true; } @@ -1518,7 +1743,7 @@ namespace Server.Mobiles var chance = Math.Clamp(700 + bonus, 220, 990); - chance -= (MaxLoyalty - m_Loyalty) * 10; + chance -= (MaxLoyalty - _loyalty) * 10; return chance / 1000.0; } @@ -1610,7 +1835,7 @@ namespace Server.Mobiles public override bool CheckPoisonImmunity(Mobile from, Poison poison) => base.CheckPoisonImmunity(from, poison) || - (m_Paragon ? PoisonImpl.IncreaseLevel(PoisonImmune) : PoisonImmune)?.Level >= poison.Level; + (_isParagon ? PoisonImpl.IncreaseLevel(PoisonImmune) : PoisonImmune)?.Level >= poison.Level; public void Unpacify() { @@ -1887,161 +2112,29 @@ namespace Server.Mobiles } } - public override void Serialize(IGenericWriter writer) + // Pre-codegen loads only (versions 0-22); post-codegen bumps use MigrateFrom. + private void Deserialize(IGenericReader reader, int version) { - base.Serialize(writer); - writer.Write(22); // version + _currentAI = (AIType)reader.ReadInt(); + _defaultAI = (AIType)reader.ReadInt(); - writer.Write((int)m_CurrentAI); - writer.Write((int)m_DefaultAI); + _rangePerception = reader.ReadInt(); + _rangeFight = reader.ReadInt(); - writer.Write(RangePerception); - writer.Write(RangeFight); - - writer.Write(m_Team); - - writer.Write(_activeSpeed); - writer.Write(_passiveSpeed); - writer.Write(_currentSpeed); - - writer.Write(m_Home.X); - writer.Write(m_Home.Y); - writer.Write(m_Home.Z); - - // Version 1 - writer.Write(RangeHome); - - // Version 2 - writer.Write((int)FightMode); - - writer.Write(_controlled); - writer.Write(m_ControlMaster); - writer.Write(ControlTarget); - writer.Write(ControlDest); - writer.Write((int)m_ControlOrder); - writer.Write(MinTameSkill); - // Removed in version 9 - // writer.Write( (double) m_dMaxTameSkill ); - writer.Write(m_bTamable); - writer.Write(_summoned); - - if (_summoned) - { - writer.WriteAnchoredTime(SummonEnd); - } - - writer.Write(ControlSlots); - - // Version 3 - writer.Write(m_Loyalty); - - // Version 4 - writer.Write(CurrentWayPoint); - - // Verison 5 - writer.Write(m_SummonMaster); - - // Version 6 - writer.Write(HitsMaxSeed); - writer.Write(StamMaxSeed); - writer.Write(ManaMaxSeed); - writer.Write(m_DamageMin); - writer.Write(m_DamageMax); - - // Version 7 - writer.Write(m_PhysicalResistance); - writer.Write(PhysicalDamage); - - writer.Write(m_FireResistance); - writer.Write(FireDamage); - - writer.Write(m_ColdResistance); - writer.Write(ColdDamage); - - writer.Write(m_PoisonResistance); - writer.Write(PoisonDamage); - - writer.Write(m_EnergyResistance); - writer.Write(EnergyDamage); - - // Version 8 - Owners.Tidy(); - writer.Write(Owners); - - // Version 10 - writer.Write(IsDeadPet); - writer.Write(m_IsBonded); - writer.Write(BondingBegin); - writer.Write(OwnerAbandonTime); - - // Version 11 - writer.Write(m_HasGeneratedLoot); - - // Version 12 - writer.Write(m_Paragon); - - var hasFriends = Friends?.Count > 0; - - // Version 13 - writer.Write(hasFriends); - - if (hasFriends) - { - Friends.Tidy(); - writer.Write(Friends); - } - - // Version 14 - writer.Write(RemoveIfUntamed); - writer.Write(RemoveStep); - - // Version 17 - if (IsStabled || Controlled && ControlMaster != null) - { - writer.Write(TimeSpan.Zero); - } - else - { - writer.Write(DeleteTimeLeft); - } - - // Version 18 - writer.Write(CorpseNameOverride); - - // Version 19 - writer.Write(HomeMap); - - // Version 22 (0 = inherit the matching think value) - writer.Write(_activeMoveSpeed); - writer.Write(_passiveMoveSpeed); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - m_CurrentAI = (AIType)reader.ReadInt(); - m_DefaultAI = (AIType)reader.ReadInt(); - - RangePerception = reader.ReadInt(); - RangeFight = reader.ReadInt(); - - m_Team = reader.ReadInt(); + _team = reader.ReadInt(); _activeSpeed = reader.ReadDouble(); _passiveSpeed = reader.ReadDouble(); _currentSpeed = reader.ReadDouble(); - m_Home.X = reader.ReadInt(); - m_Home.Y = reader.ReadInt(); - m_Home.Z = reader.ReadInt(); + _home.X = reader.ReadInt(); + _home.Y = reader.ReadInt(); + _home.Z = reader.ReadInt(); if (version >= 1) { - RangeHome = reader.ReadInt(); + _rangeHome = reader.ReadInt(); if (version < 20) { @@ -2062,121 +2155,121 @@ namespace Server.Mobiles } else { - RangeHome = 0; + _rangeHome = 0; } if (version >= 2) { - FightMode = (FightMode)reader.ReadInt(); + _fightMode = (FightMode)reader.ReadInt(); _controlled = reader.ReadBool(); - m_ControlMaster = reader.ReadEntity(); - ControlTarget = reader.ReadEntity(); - ControlDest = reader.ReadPoint3D(); - m_ControlOrder = (OrderType)reader.ReadInt(); + _controlMaster = reader.ReadEntity(); + _controlTarget = reader.ReadEntity(); + _controlDest = reader.ReadPoint3D(); + _controlOrder = (OrderType)reader.ReadInt(); - MinTameSkill = reader.ReadDouble(); + _minTameSkill = reader.ReadDouble(); if (version < 9) { reader.ReadDouble(); } - m_bTamable = reader.ReadBool(); + _tamable = reader.ReadBool(); _summoned = reader.ReadBool(); if (_summoned) { - SummonEnd = version >= 21 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); - new UnsummonTimer(this, SummonEnd - Core.Now).Start(); + // The UnsummonTimer is restarted in AfterDeserialization. + _summonEnd = version >= 21 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); } - ControlSlots = reader.ReadInt(); + _controlSlots = reader.ReadInt(); } else { - FightMode = FightMode.Closest; + _fightMode = FightMode.Closest; _controlled = false; - m_ControlMaster = null; - ControlTarget = null; - m_ControlOrder = OrderType.None; + _controlMaster = null; + _controlTarget = null; + _controlOrder = OrderType.None; } if (version >= 3) { - m_Loyalty = reader.ReadInt(); + _loyalty = reader.ReadInt(); } else { - m_Loyalty = MaxLoyalty; // Wonderfully Happy + _loyalty = MaxLoyalty; // Wonderfully Happy } if (version >= 4) { - CurrentWayPoint = reader.ReadEntity(); + _currentWayPoint = reader.ReadEntity(); } if (version >= 5) { - m_SummonMaster = reader.ReadEntity(); + _summonMaster = reader.ReadEntity(); } if (version >= 6) { - HitsMaxSeed = reader.ReadInt(); - StamMaxSeed = reader.ReadInt(); - ManaMaxSeed = reader.ReadInt(); - m_DamageMin = reader.ReadInt(); - m_DamageMax = reader.ReadInt(); + _hitsMaxSeed = reader.ReadInt(); + _stamMaxSeed = reader.ReadInt(); + _manaMaxSeed = reader.ReadInt(); + _damageMin = reader.ReadInt(); + _damageMax = reader.ReadInt(); } if (version >= 7) { - m_PhysicalResistance = reader.ReadInt(); - PhysicalDamage = reader.ReadInt(); + _physicalResistanceSeed = reader.ReadInt(); + _physicalDamage = reader.ReadInt(); - m_FireResistance = reader.ReadInt(); - FireDamage = reader.ReadInt(); + _fireResistSeed = reader.ReadInt(); + _fireDamage = reader.ReadInt(); - m_ColdResistance = reader.ReadInt(); - ColdDamage = reader.ReadInt(); + _coldResistSeed = reader.ReadInt(); + _coldDamage = reader.ReadInt(); - m_PoisonResistance = reader.ReadInt(); - PoisonDamage = reader.ReadInt(); + _poisonResistSeed = reader.ReadInt(); + _poisonDamage = reader.ReadInt(); - m_EnergyResistance = reader.ReadInt(); - EnergyDamage = reader.ReadInt(); + _energyResistSeed = reader.ReadInt(); + _energyDamage = reader.ReadInt(); } if (version >= 8) { - Owners = reader.ReadEntityList(); + _owners = reader.ReadEntityList(); } else { - Owners = new List(); + _owners = new List(); } if (version >= 10) { - IsDeadPet = reader.ReadBool(); - m_IsBonded = reader.ReadBool(); - BondingBegin = reader.ReadDateTime(); - OwnerAbandonTime = reader.ReadDateTime(); + _isDeadPet = reader.ReadBool(); + _isBonded = reader.ReadBool(); + _bondingBegin = reader.ReadDateTime(); + _ownerAbandonTime = reader.ReadDateTime(); } - m_HasGeneratedLoot = version < 11 || reader.ReadBool(); + _hasGeneratedLoot = version < 11 || reader.ReadBool(); - m_Paragon = version >= 12 && reader.ReadBool(); + _isParagon = version >= 12 && reader.ReadBool(); if (version >= 13 && reader.ReadBool()) { - Friends = reader.ReadEntityList(); + _friends = reader.ReadEntityList(); } - else if (version < 13 && m_ControlOrder >= OrderType.Unfriend) + else if (version < 13 && _controlOrder >= OrderType.Unfriend) { - ++m_ControlOrder; + ++_controlOrder; } if (version < 16 && Loyalty != MaxLoyalty) @@ -2186,8 +2279,8 @@ namespace Server.Mobiles if (version >= 14) { - RemoveIfUntamed = reader.ReadBool(); - RemoveStep = reader.ReadInt(); + _removeIfUntamed = reader.ReadBool(); + _removeStep = reader.ReadInt(); } var deleteTime = TimeSpan.Zero; @@ -2204,18 +2297,18 @@ namespace Server.Mobiles deleteTime = TimeSpan.FromDays(3.0); } - m_DeleteTimer = new DeleteTimer(this, deleteTime); - m_DeleteTimer.Start(); + _pendingDeleteTimer = new DeleteTimer(this, deleteTime); + _pendingDeleteTimer.Start(); } if (version >= 18) { - CorpseNameOverride = reader.ReadString(); + _corpseNameOverride = reader.ReadString(); } if (version >= 19) { - HomeMap = reader.ReadMap(); + _homeMap = reader.ReadMap(); } if (version >= 22) @@ -2228,25 +2321,42 @@ namespace Server.Mobiles MigrateMoveSpeeds(); } - if (version <= 14 && m_Paragon && Hue == 0x31) + if (version <= 14 && _isParagon && Hue == 0x31) { Hue = Paragon.Hue; // Paragon hue fixed, should now be 0x501. } + } + [AfterDeserialization] + private void AfterDeserialization() + { if (Core.AOS && NameHue == 0x35) { NameHue = -1; } + if (_summoned) + { + new UnsummonTimer(this, _summonEnd - Core.Now).Start(); + } + + // Abandoned-pet fallback: a pet with a former owner but no persisted delete + // countdown still despawns (legacy loads restore their own timer above). + if (_pendingDeleteTimer == null && LastOwner != null && !_controlled && !IsStabled) + { + _pendingDeleteTimer = new DeleteTimer(this, TimeSpan.FromDays(3.0)); + _pendingDeleteTimer.Start(); + } + CheckStatTimers(); - ChangeAIType(m_CurrentAI); + ChangeAIType(_currentAI); AddFollowers(); if (IsAnimatedDead) { - AnimateDeadSpell.Register(m_SummonMaster, this); + AnimateDeadSpell.Register(_summonMaster, this); } } @@ -2353,7 +2463,7 @@ namespace Server.Mobiles public void RemoveFollowers() { - var master = m_ControlMaster ?? m_SummonMaster; + var master = _controlMaster ?? _summonMaster; if (master != null) { master.Followers -= Math.Min(ControlSlots, master.Followers); @@ -2367,7 +2477,7 @@ namespace Server.Mobiles public void AddFollowers() { - var master = m_ControlMaster ?? m_SummonMaster; + var master = _controlMaster ?? _summonMaster; if (master != null) { master.Followers += ControlSlots; @@ -2413,7 +2523,7 @@ namespace Server.Mobiles public virtual void OnGaveMeleeAttack(Mobile defender, int damage) { - var p = m_Paragon ? PoisonImpl.IncreaseLevel(HitPoison) : HitPoison; + var p = _isParagon ? PoisonImpl.IncreaseLevel(HitPoison) : HitPoison; if (p != null && HitPoisonChance >= Utility.RandomDouble()) { @@ -2442,17 +2552,17 @@ namespace Server.Mobiles AIObject = null; } - if (m_DeleteTimer != null) + if (_pendingDeleteTimer != null) { - m_DeleteTimer.Stop(); - m_DeleteTimer = null; + _pendingDeleteTimer.Stop(); + _pendingDeleteTimer = null; } FocusMob = null; if (IsAnimatedDead) { - AnimateDeadSpell.Unregister(m_SummonMaster, this); + AnimateDeadSpell.Unregister(_summonMaster, this); } if (Summoned && SummonMaster != null) @@ -2511,7 +2621,7 @@ namespace Server.Mobiles public bool IsHurt() => Hits != HitsMax; - public double GetHomeDistance() => this.GetDistanceToSqrt(m_Home); + public double GetHomeDistance() => this.GetDistanceToSqrt(_home); public virtual int GetTeamSize(int iRange) { @@ -2538,7 +2648,7 @@ namespace Server.Mobiles aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true)); } - var ct = m_ControlOrder; + var ct = _controlOrder; if (AIObject != null) { @@ -2612,7 +2722,7 @@ namespace Server.Mobiles AIObject?.GetContextMenuEntries(from, ref list); } - if (m_bTamable && !_controlled && from.Alive) + if (_tamable && !_controlled && from.Alive) { list.Add(new TameEntry(from.Female ? AllowFemaleTamer : AllowMaleTamer)); } @@ -2663,7 +2773,7 @@ namespace Server.Mobiles } public override bool IsHarmfulCriminal(Mobile target) => - (!Controlled || target != m_ControlMaster) && (!Summoned || target != m_SummonMaster) && + (!Controlled || 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); @@ -2673,13 +2783,13 @@ namespace Server.Mobiles if (Controlled || Summoned) { - if (m_ControlMaster?.Player == true) + if (_controlMaster?.Player == true) { - m_ControlMaster.CriminalAction(false); + _controlMaster.CriminalAction(false); } - else if (m_SummonMaster?.Player == true) + else if (_summonMaster?.Player == true) { - m_SummonMaster.CriminalAction(false); + _summonMaster.CriminalAction(false); } } } @@ -2688,7 +2798,7 @@ namespace Server.Mobiles { base.DoHarmful(target, indirect); - if (target == this || target == m_ControlMaster || target == m_SummonMaster || !Controlled && !Summoned) + if (target == this || target == _controlMaster || target == _summonMaster || !Controlled && !Summoned) { return; } @@ -2955,7 +3065,7 @@ namespace Server.Mobiles list.Add(TotalWeight == 1 ? 1072788 : 1072789, TotalWeight); // Weight: ~1_WEIGHT~ stones } - if (m_ControlOrder == OrderType.Guard) + if (_controlOrder == OrderType.Guard) { list.Add(1080078); // guarding } @@ -3029,7 +3139,7 @@ namespace Server.Mobiles { if (treasureLevel >= 0) { - if (m_Paragon && Paragon.ChestChance > Utility.RandomDouble()) + if (_isParagon && Paragon.ChestChance > Utility.RandomDouble()) { PackItem(new ParagonChest(Name, treasureLevel)); } @@ -3039,7 +3149,7 @@ namespace Server.Mobiles } } - if (m_Paragon && Paragon.ChocolateIngredientChance > Utility.RandomDouble()) + if (_isParagon && Paragon.ChocolateIngredientChance > Utility.RandomDouble()) { switch (Utility.Random(4)) { @@ -3067,9 +3177,9 @@ namespace Server.Mobiles } } - if (!Summoned && !NoKillAwards && !m_HasGeneratedLoot) + if (!Summoned && !NoKillAwards && !_hasGeneratedLoot) { - m_HasGeneratedLoot = true; + _hasGeneratedLoot = true; GenerateLoot(false); } @@ -3274,7 +3384,7 @@ namespace Server.Mobiles MondainsLegacy.GiveArtifactTo(mob); } } - else if (m_Paragon) + else if (_isParagon) { if (Paragon.CheckArtifactChance(mob, this)) { @@ -3283,9 +3393,6 @@ namespace Server.Mobiles } } - [GeneratedEvent(nameof(CreatureDeathEvent))] - public static partial void CreatureDeathEvent(BaseCreature bc); - public override void OnDeath(Container c) { if (IsBonded) @@ -3348,7 +3455,7 @@ namespace Server.Mobiles OwnerAbandonTime = DateTime.MinValue; } - CreatureDeathEvent(this); + CreatureEvents.CreatureDeathEvent(this); CheckStatTimers(); return; @@ -3478,17 +3585,14 @@ namespace Server.Mobiles c.Delete(); } - CreatureDeathEvent(this); + CreatureEvents.CreatureDeathEvent(this); } - [GeneratedEvent(nameof(CreatureDeletedEvent))] - public static partial void CreatureDeletedEvent(BaseCreature bc); - public override void OnDelete() { - CreatureDeletedEvent(this); + CreatureEvents.CreatureDeletedEvent(this); - var m = m_ControlMaster; + var m = _controlMaster; SetControlMaster(null); SummonMaster = null; @@ -3562,10 +3666,10 @@ namespace Server.Mobiles ControlOrder = OrderType.Come; - if (m_DeleteTimer != null) + if (_pendingDeleteTimer != null) { - m_DeleteTimer.Stop(); - m_DeleteTimer = null; + _pendingDeleteTimer.Stop(); + _pendingDeleteTimer = null; } } @@ -3780,14 +3884,14 @@ namespace Server.Mobiles return BardMaster; } - if (_controlled && m_ControlMaster != null) + if (_controlled && _controlMaster != null) { - return m_ControlMaster; + return _controlMaster; } - if (_summoned && m_SummonMaster != null) + if (_summoned && _summonMaster != null) { - return m_SummonMaster; + return _summonMaster; } return base.GetDamageMaster(damagee); @@ -4059,17 +4163,17 @@ namespace Server.Mobiles if (this is not BaseEscortable && !Summoned && !Deleted && !IsStabled) { StopDeleteTimer(); - m_DeleteTimer = new DeleteTimer(this, TimeSpan.FromDays(3.0)); - m_DeleteTimer.Start(); + _pendingDeleteTimer = new DeleteTimer(this, TimeSpan.FromDays(3.0)); + _pendingDeleteTimer.Start(); } } public void StopDeleteTimer() { - if (m_DeleteTimer != null) + if (_pendingDeleteTimer != null) { - m_DeleteTimer.Stop(); - m_DeleteTimer = null; + _pendingDeleteTimer.Stop(); + _pendingDeleteTimer = null; } } @@ -4150,7 +4254,7 @@ namespace Server.Mobiles public virtual void RemovePetFriend(Mobile m) => Friends?.Remove(m); public virtual bool IsFriend(Mobile m) => - OppositionGroup?.IsEnemy(this, m) != true && m is BaseCreature c && m_Team == c.m_Team + OppositionGroup?.IsEnemy(this, m) != true && m is BaseCreature c && _team == c._team && (_summoned || _controlled) == (c._summoned || c._controlled); public virtual Allegiance GetFactionAllegiance(Mobile mob) @@ -4342,16 +4446,16 @@ namespace Server.Mobiles if (Core.SE) { - m_Loyalty = MaxLoyalty; + _loyalty = MaxLoyalty; } - else if (m_Loyalty < MaxLoyalty) + else if (_loyalty < MaxLoyalty) { // Calculate the loyalty increase var loyaltyIncrease = Utility.CoinFlips(amount, MaxLoyaltyIncrease) * 10; if (loyaltyIncrease > 0) // Only update if there's an actual increase { - m_Loyalty = Math.Min(MaxLoyalty, m_Loyalty + loyaltyIncrease); + _loyalty = Math.Min(MaxLoyalty, _loyalty + loyaltyIncrease); SayTo(from, 502060); // Your pet looks happier. } } @@ -4367,7 +4471,7 @@ namespace Server.Mobiles if (IsBondable && !IsBonded) { - var master = m_ControlMaster; + var master = _controlMaster; if (master != null && master == from) // So friends can't start the bonding process { @@ -4721,14 +4825,14 @@ namespace Server.Mobiles public void SetDamage(int val) { - m_DamageMin = val; - m_DamageMax = val; + _damageMin = val; + _damageMax = val; } public void SetDamage(int min, int max) { - m_DamageMin = min; - m_DamageMax = max; + _damageMin = min; + _damageMax = max; } public void SetHits(int val) @@ -4862,27 +4966,27 @@ namespace Server.Mobiles { case ResistanceType.Physical: { - m_PhysicalResistance = val; + _physicalResistanceSeed = val; break; } case ResistanceType.Fire: { - m_FireResistance = val; + _fireResistSeed = val; break; } case ResistanceType.Cold: { - m_ColdResistance = val; + _coldResistSeed = val; break; } case ResistanceType.Poison: { - m_PoisonResistance = val; + _poisonResistSeed = val; break; } case ResistanceType.Energy: { - m_EnergyResistance = val; + _energyResistSeed = val; break; } } @@ -5083,7 +5187,7 @@ namespace Server.Mobiles GenerateLoot(); - if (m_Paragon) + if (_isParagon) { if (Fame < 1250) { diff --git a/Projects/UOContent/Mobiles/CreatureEvents.cs b/Projects/UOContent/Mobiles/CreatureEvents.cs new file mode 100644 index 000000000..d1fdc9112 --- /dev/null +++ b/Projects/UOContent/Mobiles/CreatureEvents.cs @@ -0,0 +1,15 @@ +using ModernUO.CodeGeneratedEvents; + +namespace Server.Mobiles; + +// Hosts BaseCreature's generated events. They cannot live on BaseCreature itself: the +// events generator and the serialization generator each emit a [GeneratedCode] partial for +// the declaring type, and the attribute does not allow duplicates (CS0579). +public static partial class CreatureEvents +{ + [GeneratedEvent(nameof(CreatureDeathEvent))] + public static partial void CreatureDeathEvent(BaseCreature bc); + + [GeneratedEvent(nameof(CreatureDeletedEvent))] + public static partial void CreatureDeletedEvent(BaseCreature bc); +} diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs index 178436fb9..fca41ec25 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs @@ -154,7 +154,7 @@ namespace Server.Mobiles public static bool UnderEffect(Mobile m) => m_Table.ContainsKey(m); [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] - [OnEvent(nameof(CreatureDeathEvent))] + [OnEvent(nameof(CreatureEvents.CreatureDeathEvent))] public static void StopEffect(Mobile m, bool message = false) { if (m_Table.Remove(m, out var timer)) diff --git a/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs b/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs index c0f3c3ed0..578bdc482 100644 --- a/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs +++ b/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs @@ -151,8 +151,8 @@ public class BloodOathSpell : NecromancerSpell, ITargetingSpell // shared timer from either the caster or the target key, so a single call per mobile is enough. [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] [OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))] - [OnEvent(nameof(BaseCreature.CreatureDeathEvent))] - [OnEvent(nameof(BaseCreature.CreatureDeletedEvent))] + [OnEvent(nameof(CreatureEvents.CreatureDeathEvent))] + [OnEvent(nameof(CreatureEvents.CreatureDeletedEvent))] public static void OnCurseEnds(Mobile m) => RemoveCurse(m); private class ExpireTimer : Timer diff --git a/Projects/UOContent/Spells/Spellweaving/GiftOfLife.cs b/Projects/UOContent/Spells/Spellweaving/GiftOfLife.cs index 585d37284..6a5d4850a 100644 --- a/Projects/UOContent/Spells/Spellweaving/GiftOfLife.cs +++ b/Projects/UOContent/Spells/Spellweaving/GiftOfLife.cs @@ -79,7 +79,7 @@ namespace Server.Spells.Spellweaving Caster.Target = new SpellTarget(this, TargetFlags.Beneficial); } - [OnEvent(nameof(BaseCreature.CreatureDeathEvent))] + [OnEvent(nameof(CreatureEvents.CreatureDeathEvent))] [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] public static void OnDeathEvent(Mobile m) {