diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs index 7ad149eba..7d9243a29 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs @@ -57,9 +57,8 @@ public class MoveSpeedTests : IDisposable { var bc = NewCreature(); - // 0 = no override; the resolved pace comes from CurrentMoveSpeed. - Assert.Equal(0, bc.ActiveMoveSpeed); - Assert.Equal(0, bc.PassiveMoveSpeed); + Assert.Equal(0.3, bc.ActiveMoveSpeed); + Assert.Equal(0.6, bc.PassiveMoveSpeed); Assert.Equal(bc.CurrentSpeed, bc.CurrentMoveSpeed); } @@ -97,8 +96,8 @@ public class MoveSpeedTests : IDisposable bc.SetSpeed(0.2, 0.4); - Assert.Equal(0, bc.ActiveMoveSpeed); - Assert.Equal(0, bc.PassiveMoveSpeed); + Assert.Equal(0.2, bc.ActiveMoveSpeed); + Assert.Equal(0.4, bc.PassiveMoveSpeed); } [Fact] @@ -109,10 +108,8 @@ public class MoveSpeedTests : IDisposable bc.ActiveMoveSpeed = 0; - Assert.Equal(0, bc.ActiveMoveSpeed); // inheriting again + Assert.Equal(0.3, bc.ActiveMoveSpeed); // inheriting again Assert.Equal(0.9, bc.PassiveMoveSpeed); // other override untouched - bc.SetCurrentSpeedToActive(); - Assert.Equal(0.3, bc.CurrentMoveSpeed); // resolves to the think clock } [Fact] @@ -124,7 +121,7 @@ public class MoveSpeedTests : IDisposable bc.ScaleMoveSpeed(1.0 / 1.2); Assert.Equal(0.5, bc.ActiveMoveSpeed); - Assert.Equal(0, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar + Assert.Equal(bc.PassiveSpeed, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar } [Fact] @@ -188,8 +185,8 @@ public class MoveSpeedTests : IDisposable bc.MigrateMoveSpeeds(); - Assert.Equal(0, bc.ActiveMoveSpeed); // still inheriting the (tuned) think clock - Assert.Equal(0, bc.PassiveMoveSpeed); + Assert.Equal(0.35, bc.ActiveMoveSpeed); + Assert.Equal(0.6, bc.PassiveMoveSpeed); } [Theory] @@ -216,7 +213,7 @@ public class MoveSpeedTests : IDisposable // The v22 tail is the last block; exact consumption catches any offset mistake. Assert.Equal(buffer.Length, reader.Position); - Assert.Equal(overridden ? 0.45 : 0, copy.ActiveMoveSpeed); - Assert.Equal(overridden ? 0.9 : 0, copy.PassiveMoveSpeed); + Assert.Equal(overridden ? 0.45 : 0.3, copy.ActiveMoveSpeed); + Assert.Equal(overridden ? 0.9 : 0.6, copy.PassiveMoveSpeed); } } diff --git a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs deleted file mode 100644 index 724b14070..000000000 --- a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using System.Collections.Generic; -using Server; -using Server.Mobiles; -using Xunit; - -namespace UOContent.Tests.Mobiles; - -// BaseCreature's SaveFlag format round-trips both a default and a fully-populated -// creature with exact byte consumption, and back-to-back saves are byte-identical -// (freeze-time stability). -[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); // class None (no table in tests): restored from 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); - } - - [Fact] - public void UncontrolledSummon_KeepsItsSummonMaster() - { - var bc = NewCreature(); - var master = new PlayerMobile(World.NewMobile); - master.DefaultMobileInit(); - World.AddEntity(master); - _created.Add(master); - - // Energy vortex-style: summoned with a master, never controlled. - bc.Summoned = true; - bc.SummonMaster = master; - - var copy = Load(Snapshot(bc)); - - Assert.True(copy.Summoned); - Assert.False(copy.Controlled); - Assert.Equal(master, copy.SummonMaster); - Assert.Null(copy.ControlMaster); - } - - private sealed class BucketStub : BaseCreature - { - public BucketStub() : base(AIType.AI_Melee) => Body = 0xC9; - - public BucketStub(Serial serial) : base(serial) => Body = 0xC9; - - public override SpeedLevel DefaultSpeedClass => SpeedLevel.Fast; - } - - [Fact] - public void SpeedClass_Assignment_AppliesBucket_AndRoundTrips() - { - NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry - { - Level = SpeedLevel.Fast, ActiveSpeed = 0.2, PassiveSpeed = 0.4, - ActiveMoveSpeed = 0.3, PassiveMoveSpeed = 0.9, Types = new HashSet() - }); - NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry - { - Level = SpeedLevel.VeryFast, ActiveSpeed = 0.125, PassiveSpeed = 0.3, - ActiveMoveSpeed = 0.125, PassiveMoveSpeed = 0.6, Types = new HashSet() - }); - - var bc = new BucketStub(); - _created.Add(bc); - - Assert.Equal(0.2, bc.ActiveSpeed); // seeded from the default bucket - Assert.Equal(0.3, bc.ActiveMoveSpeed); - - bc.SpeedClass = SpeedLevel.VeryFast; // boss state change - - Assert.Equal(SpeedLevel.VeryFast, bc.SpeedClass); // conforming assignment holds - Assert.Equal(0.125, bc.ActiveSpeed); - Assert.Equal(0.3, bc.PassiveSpeed); - Assert.Equal(0.125, bc.ActiveMoveSpeed); - Assert.Equal(0.6, bc.PassiveMoveSpeed); - Assert.Equal(0.3, bc.CurrentSpeed); // stayed in the passive mode - - // The changed bucket persists; the (bucket-matching) speeds elide but restore - // through the new bucket - the consistency the stateful SpeedClass guarantees. - var writer = new BufferWriter(true); - bc.Serialize(writer); - var buffer = new byte[writer.Position]; - writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer); - - var copy = new BucketStub(World.NewMobile); - _created.Add(copy); - var reader = new BufferReader(buffer); - copy.Deserialize(reader); - - Assert.Equal(buffer.Length, reader.Position); - Assert.Equal(SpeedLevel.VeryFast, copy.SpeedClass); - Assert.Equal(0.125, copy.ActiveSpeed); - Assert.Equal(0.3, copy.PassiveSpeed); - Assert.Equal(0.125, copy.ActiveMoveSpeed); - Assert.Equal(0.6, copy.PassiveMoveSpeed); - } - - [Fact] - public void PartialSpeedTuning_MakesTheCreatureFullyCustom() - { - NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry - { - Level = SpeedLevel.Fast, ActiveSpeed = 0.2, PassiveSpeed = 0.4, - ActiveMoveSpeed = 0.3, PassiveMoveSpeed = 0.9, Types = new HashSet() - }); - - var bc = new BucketStub(); - _created.Add(bc); - - bc.ActiveSpeed = 0.25; // one tuned value customizes the whole block - - Assert.Equal(SpeedLevel.None, bc.SpeedClass); // the bucket label never lies - - var writer = new BufferWriter(true); - bc.Serialize(writer); - var buffer = new byte[writer.Position]; - writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer); - - var copy = new BucketStub(World.NewMobile); - _created.Add(copy); - var reader = new BufferReader(buffer); - copy.Deserialize(reader); - - // All four persisted raw - no value is left silently tracking the table. - Assert.Equal(buffer.Length, reader.Position); - Assert.Equal(SpeedLevel.None, copy.SpeedClass); - Assert.Equal(0.25, copy.ActiveSpeed); - Assert.Equal(0.4, copy.PassiveSpeed); - Assert.Equal(0.3, copy.ActiveMoveSpeed); - Assert.Equal(0.9, copy.PassiveMoveSpeed); - } - -} diff --git a/Projects/UOContent.Tests/Tests/Spells/Necromancy/BloodOathSpellTests.cs b/Projects/UOContent.Tests/Tests/Spells/Necromancy/BloodOathSpellTests.cs index 08658b1a1..0264a282e 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)); - CreatureEvents.CreatureDeletedEvent(target); // central handler breaks the oath from the target side + BaseCreature.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/Items/Special/Solen Items/BallOfSummoning.cs b/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs index 3936d68db..070a802b1 100644 --- a/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs +++ b/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs @@ -227,6 +227,12 @@ public partial class BallOfSummoning : Item, TranslocationItem if (pet.IsStabled) { pet.SetControlMaster(from); + + if (pet.Summoned) + { + pet.SummonMaster = from; + } + pet.ControlTarget = from; pet.ControlOrder = OrderType.Follow; diff --git a/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json b/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json deleted file mode 100644 index 077a428fe..000000000 --- a/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json +++ /dev/null @@ -1,471 +0,0 @@ -{ - "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": "SpeedClass", - "type": "Server.Mobiles.SpeedLevel", - "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": "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": "Master", - "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/AI/BaseAI/OnSpeech.cs b/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs index f254b68ae..8ffd39b42 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs @@ -447,6 +447,11 @@ public abstract partial class BaseAI if (Mobile.FindMyName(e.Speech, true) && e.Speech.InsensitiveContains("obey")) { Mobile.SetControlMaster(e.Mobile); + + if (Mobile.Summoned) + { + Mobile.SummonMaster = e.Mobile; + } } } } diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs b/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs index 9f2fa937c..9efad5a5e 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs @@ -156,6 +156,11 @@ internal sealed partial class TransferItem : Item private void TransferPetOwnership(Mobile from, Mobile to) { + if (_creature.Summoned) + { + _creature.SummonMaster = to; + } + _creature.ControlTarget = to; _creature.ControlOrder = OrderType.Follow; _creature.BondingBegin = DateTime.MinValue; diff --git a/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs index 75c6fc3dc..fc227507e 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(CreatureEvents.CreatureDeathEvent))] - [OnEvent(nameof(CreatureEvents.CreatureDeletedEvent))] + [OnEvent(nameof(BaseCreature.CreatureDeathEvent))] + [OnEvent(nameof(BaseCreature.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 67da2b526..f64a289f4 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using ModernUO.Serialization; +using ModernUO.CodeGeneratedEvents; using Server.Collections; using Server.ContextMenus; using Server.Engines.ConPVP; @@ -13,7 +13,6 @@ using Server.Engines.Virtues; using Server.Ethics; using Server.Factions; using Server.Items; -using Server.Logging; using Server.Misc; using Server.Multis; using Server.Network; @@ -134,11 +133,8 @@ 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 { - private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseCreature)); - public enum Allegiance { None, @@ -164,7 +160,7 @@ namespace Server.Mobiles public const int DefaultRangePerception = 16; - private const double ChanceToRummage = 0.5; + private const double ChanceToRummage = 0.5; // 50% private const double MinutesToNextRummageMin = 1.0; private const double MinutesToNextRummageMax = 4.0; @@ -231,7 +227,7 @@ namespace Server.Mobiles private static readonly Type[] _gold = { - // White wyrms eat gold. + // white wyrms eat gold.. typeof(Gold) }; @@ -254,600 +250,54 @@ namespace Server.Mobiles typeof(AncientSmithyHammer), typeof(Scorp) }; - // --- 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. - - [SerializableField(0, setter: "private")] - private AIType _defaultAI; - - [SerializableField(1, setter: "private")] - [SaveFlag(nameof(ShouldSerializeCurrentAI), nameof(CurrentAIDefaultValue))] - private AIType _currentAI; - - 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; - - /// - /// The creature's npc-speeds bucket, resolved at construction. Assigning applies - /// the bucket's speeds; None means custom (its own speeds are authoritative). A - /// type's constant bucket belongs in . - /// - [SerializableField(7, fieldChanged: nameof(OnSpeedClassChange))] - [SaveFlag(nameof(ShouldSerializeSpeedClass), nameof(SpeedClassDefaultValue))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private SpeedLevel _speedClass; - - private bool ShouldSerializeSpeedClass() => _speedClass != ResolvedDefaultSpeedClass; - - private SpeedLevel SpeedClassDefaultValue() => ResolvedDefaultSpeedClass; - - private void OnSpeedClassChange(SpeedLevel oldValue, SpeedLevel newValue) - { - _speedEntry = null; - ApplySpeedClass(); - } - - private bool _applyingSpeedClass; - - // Applies the current bucket's speeds, preserving the active/passive mode. The - // guard keeps OnSpeedTuned from reading the half-assigned block as customization. - private void ApplySpeedClass() - { - if (SpeedEntry == null) - { - return; // None (custom) or an unloaded table has no bucket to apply - } - - _applyingSpeedClass = true; - - try - { - var wasActive = _currentSpeed == _activeSpeed && _currentSpeed != _passiveSpeed; - - GetSpeeds(out var activeSpeed, out var passiveSpeed); - GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed); - - ActiveSpeed = activeSpeed; - PassiveSpeed = passiveSpeed; - CurrentSpeed = wasActive ? activeSpeed : passiveSpeed; - } - finally - { - _applyingSpeedClass = false; - } - } - - /// Seconds per AI decision while engaged; see for movement pace. - [SerializableField(8, fieldChanged: nameof(OnSpeedTuned))] - [SaveFlag(nameof(ShouldSerializeSpeeds), nameof(ActiveSpeedDefaultValue))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private double _activeSpeed; - - // The four speeds are one block: either all conform to the bucket (elided as a - // set), or the creature is custom and all four serialize. Partial conformance - // cannot exist on the wire. - private bool ShouldSerializeSpeeds() - { - if (_speedClass == SpeedLevel.None) - { - return true; - } - - GetSpeeds(out var activeSpeed, out var passiveSpeed); - GetMoveSpeeds(out var activeMoveSpeed, out var passiveMoveSpeed); - - return _activeSpeed != activeSpeed || _passiveSpeed != passiveSpeed || - _activeMoveSpeed != activeMoveSpeed || _passiveMoveSpeed != passiveMoveSpeed; - } - - // Tuning any speed away from the bucket makes the creature fully custom - the - // bucket label must never lie. ApplySpeedClass assigns mid-transition and guards. - private void OnSpeedTuned(double oldValue, double newValue) - { - if (!_applyingSpeedClass && _speedClass != SpeedLevel.None && ShouldSerializeSpeeds()) - { - _speedClass = SpeedLevel.None; - _speedEntry = null; - } - } - - private double ActiveSpeedDefaultValue() - { - GetSpeeds(out var activeSpeed, out _); - return activeSpeed; - } - - /// Seconds per AI decision while idle; see for movement pace. - [SerializableField(9, fieldChanged: nameof(OnSpeedTuned))] - [SaveFlag(nameof(ShouldSerializeSpeeds), nameof(PassiveSpeedDefaultValue))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private double _passiveSpeed; - - private double PassiveSpeedDefaultValue() - { - GetSpeeds(out _, out var passiveSpeed); - return passiveSpeed; - } - - [SerializableField(10, 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) while engaged; 0 = inherit - /// . resolves the pace. - /// - [SerializableField(11, allowFieldChange: nameof(CoerceMoveSpeed), fieldChanged: nameof(OnSpeedTuned))] - [SaveFlag(nameof(ShouldSerializeSpeeds), nameof(ActiveMoveSpeedDefaultValue))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private double _activeMoveSpeed; - - /// - /// Movement clock (seconds per step) while idle; 0 = inherit - /// . resolves the pace. - /// - [SerializableField(12, allowFieldChange: nameof(CoerceMoveSpeed), fieldChanged: nameof(OnSpeedTuned))] - [SaveFlag(nameof(ShouldSerializeSpeeds), nameof(PassiveMoveSpeedDefaultValue))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private double _passiveMoveSpeed; - - private bool CoerceMoveSpeed(ref double value) - { - value = Math.Max(0, value); // anything non-positive means "inherit" - return true; - } - - private double ActiveMoveSpeedDefaultValue() - { - GetMoveSpeeds(out var activeMoveSpeed, out _); - return activeMoveSpeed; - } - - private double PassiveMoveSpeedDefaultValue() - { - GetMoveSpeeds(out _, out var passiveMoveSpeed); - return passiveMoveSpeed; - } - - [SerializableField(13)] - [SaveFlag(nameof(ShouldSerializeHome))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private Point3D _home; - - private bool ShouldSerializeHome() => _home != Point3D.Zero; - - [SerializableField(14)] - [SaveFlag(nameof(ShouldSerializeHomeMap))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private Map _homeMap; - - private bool ShouldSerializeHomeMap() => _homeMap != null; - - [SerializableField(15, 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(); - } - - // ControlMaster and SummonMaster serialize as one master reference (Master below). - private Mobile _controlMaster; - - [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 bool m_bTamable; + private int m_ColdResistance; - private void OnSummonedChange(bool oldValue, bool newValue) - { - NextReacquireTime = Core.TickCount; - Delta(MobileDelta.Noto); - InvalidateProperties(); - } + private bool _controlled; // Is controlled + private Mobile m_ControlMaster; // My master + private OrderType m_ControlOrder; // My order - [AnchoredDateTime] - [SerializableField(22, getter: "protected", setter: "protected")] - [SaveFlag(nameof(ShouldSerializeSummonEnd))] - private DateTime _summonEnd; + private AIType m_CurrentAI; // The current AI - private bool ShouldSerializeSummonEnd() => _summoned; + private double _activeSpeed; + private double _passiveSpeed; + private double _currentSpeed; - private Mobile _summonMaster; - - // When both roles are set they are always the same mobile (every management flow - // assigns them in lockstep via SetControlMaster), so one reference serializes - - // refreshed here at save - and fans back out through the Controlled/Summoned - // flags in AfterDeserialization. - [SerializableField(23, getter: "private", setter: "private")] - [SaveFlag(nameof(ShouldSerializeMaster))] - private Mobile _master; - - private bool ShouldSerializeMaster() - { - _master = _controlMaster ?? _summonMaster; - return _master != null; - } - - [EncodedInt] - [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)] - [SaveFlag(nameof(ShouldSerializeDamageMin), nameof(DamageMinDefaultValue))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _damageMin = -1; - - private bool ShouldSerializeDamageMin() => _damageMin != -1; - - private int DamageMinDefaultValue() => -1; - - [EncodedInt] - [SerializableField(31)] - [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 ------------------------------------------------------- + // Movement clock (seconds per step); 0 = inherit the matching think value. + private double _activeMoveSpeed; + private double _passiveMoveSpeed; // 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_FailedReturnHome; /* return to home failure counter */ + 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; @@ -860,13 +310,24 @@ namespace Server.Mobiles private long m_NextRummageTime; - // On OSI these despawn; we queue a return home instead of deleting. + 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 */ + private bool m_ReturnQueued; 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, @@ -874,41 +335,33 @@ namespace Server.Mobiles int iRangeFight = 1 ) { - _loyalty = MaxLoyalty; + m_Loyalty = MaxLoyalty; // Wonderfully Happy - _currentAI = ai; - _defaultAI = ai; - - _speedClass = ResolvedDefaultSpeedClass; + m_CurrentAI = ai; + m_DefaultAI = ai; RangePerception = iRangePerception; RangeFight = iRangeFight; FightMode = mode; - GetSpeeds(out _activeSpeed, out _passiveSpeed); + GetSpeeds(out var activeSpeed, out var passiveSpeed); GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed); - _currentSpeed = _passiveSpeed; - if (_activeSpeed <= 0 || _passiveSpeed <= 0) - { - // A 0-delay creature would spin its AI timer at wheel resolution. - logger.Debug("{Type} constructed without speeds - is Data/npc-speeds.json missing? Defaulting to Medium.", GetType()); - _activeSpeed = 0.25; - _passiveSpeed = 0.5; - _currentSpeed = _passiveSpeed; - } + ActiveSpeed = activeSpeed; + PassiveSpeed = passiveSpeed; + CurrentSpeed = passiveSpeed; - _team = 0; + m_Team = 0; Debug = false; _controlled = false; - _controlMaster = null; + m_ControlMaster = null; ControlTarget = null; - _controlOrder = OrderType.None; + m_ControlOrder = OrderType.None; - _tamable = false; + m_bTamable = false; Owners = new List(); @@ -930,7 +383,6 @@ namespace Server.Mobiles public BaseCreature(Serial serial) : base(serial) { - _speedClass = ResolvedDefaultSpeedClass; Debug = false; } @@ -954,10 +406,14 @@ namespace Server.Mobiles public virtual InhumanSpeech SpeechType => null; - // Deliberately not serialized until the feature is finalized. + /* Do not serialize this till the code is finalized */ + [CommandProperty(AccessLevel.GameMaster)] public bool SeeksHome { get; set; } + [CommandProperty(AccessLevel.GameMaster)] + public string CorpseNameOverride { get; set; } + [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] public bool IsStabled { @@ -980,20 +436,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 => _isParagon; + get => m_Paragon; set { - if (_isParagon == value) + if (m_Paragon == value) { return; } @@ -1007,10 +463,9 @@ namespace Server.Mobiles Paragon.UnConvert(this); } - _isParagon = value; + m_Paragon = value; InvalidateProperties(); - this.MarkDirty(); } } @@ -1019,6 +474,8 @@ 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; @@ -1042,11 +499,11 @@ namespace Server.Mobiles public virtual bool DeathAdderCharmable => false; - //TODO Apply the pub 31 DispelDifficulty tweaks - // Skill level at which dispel succeeds 50% of the time. + // TODO: Find the pub 31 tweaks to the DispelDifficulty and apply them of course. + // at this skill level we dispel 50% chance public virtual double DispelDifficulty => 0.0; - // 0% at difficulty - focus, 100% at difficulty + focus. + // at difficulty - focus we have 0%, at difficulty + focus we have 100% public virtual double DispelFocus => 20.0; public virtual bool DisplayWeight => Backpack is StrongBackpack; @@ -1082,11 +539,21 @@ namespace Server.Mobiles } public virtual bool IsNecroFamiliar => - Summoned && _controlMaster != null && - SummonFamiliarSpell.Table.TryGetValue(_controlMaster, out var bc) && bc == this; + Summoned && m_ControlMaster != null && + SummonFamiliarSpell.Table.TryGetValue(m_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; @@ -1099,25 +566,57 @@ 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; public virtual bool CanDestroyObstacles => false; - // OSI followers were distracted by attacks well into AoS; removed around ML. + /* + Seems this actually was removed on OSI somewhere between the original bug report and now. + We will call it ML, until we can get better information. I suspect it was on the OSI TC when + originally it taken out of RunUO, and not implemented on OSIs production shards until more + recently. Either way, this is, or was, accurate OSI behavior, and just entirely + removing it was incorrect. OSI followers were distracted by being attacked well into + AoS, at very least. + + */ + public virtual bool CanBeDistracted => !Core.ML; public override bool ShouldCheckStatTimers => false; @@ -1129,26 +628,43 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public AIType AI { - get => _currentAI; + get => m_CurrentAI; set { - _currentAI = value; + m_CurrentAI = value; - if (_currentAI == AIType.AI_Use_Default) + if (m_CurrentAI == AIType.AI_Use_Default) { - _currentAI = _defaultAI; + m_CurrentAI = m_DefaultAI; } - ChangeAIType(_currentAI); + ChangeAIType(m_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. @@ -1156,6 +672,57 @@ 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. + [CommandProperty(AccessLevel.GameMaster)] + public virtual double ActiveMoveSpeed + { + get => _activeMoveSpeed > 0 ? _activeMoveSpeed : _activeSpeed; + set => _activeMoveSpeed = value > 0 ? value : 0; + } + + /// Seconds per step while idle. Inherits ; set 0 to re-inherit. + [CommandProperty(AccessLevel.GameMaster)] + public virtual double PassiveMoveSpeed + { + get => _passiveMoveSpeed > 0 ? _passiveMoveSpeed : _passiveSpeed; + set => _passiveMoveSpeed = value > 0 ? value : 0; + } + // Herded creatures walk at a fixed standard pace regardless of their own speed // (RunUO's forced 0.3, without its TransformMoveDelay inflation to 0.6). private const double HerdingMoveSpeed = 0.3; @@ -1167,6 +734,20 @@ 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. @@ -1182,83 +763,102 @@ namespace Server.Mobiles return HerdingMoveSpeed; } - if (_currentSpeed == _activeSpeed) + return _currentSpeed == _activeSpeed ? ActiveMoveSpeed + : _currentSpeed == _passiveSpeed ? PassiveMoveSpeed + : _currentSpeed; + } + } + + [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 _activeMoveSpeed > 0 ? _activeMoveSpeed : _activeSpeed; + return; } - if (_currentSpeed == _passiveSpeed) - { - return _passiveMoveSpeed > 0 ? _passiveMoveSpeed : _passiveSpeed; - } + _controlled = value; + Delta(MobileDelta.Noto); - return _currentSpeed; + InvalidateProperties(); } } [CommandProperty(AccessLevel.GameMaster)] public Mobile ControlMaster { - get => _controlMaster; + get => m_ControlMaster; set { - if (_controlMaster == value || this == value) + if (m_ControlMaster == value || this == value) { return; } RemoveFollowers(); - _controlMaster = value; + m_ControlMaster = value; AddFollowers(); - if (_controlMaster != null) + if (m_ControlMaster != null) { StopDeleteTimer(); } Delta(MobileDelta.Noto); - this.MarkDirty(); } } [CommandProperty(AccessLevel.GameMaster)] public Mobile SummonMaster { - get => _summonMaster; + get => m_SummonMaster; set { - if (_summonMaster == value || this == value) + if (m_SummonMaster == value || this == value) { return; } RemoveFollowers(); - _summonMaster = value; + m_SummonMaster = value; AddFollowers(); Delta(MobileDelta.Noto); - this.MarkDirty(); } } - // 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 Mobile ControlTarget { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public Point3D ControlDest { get; set; } + [CommandProperty(AccessLevel.GameMaster)] public OrderType ControlOrder { - get => _controlOrder; + get => m_ControlOrder; set { - var previous = _controlOrder; - _controlOrder = value; + var previous = m_ControlOrder; + m_ControlOrder = value; AIObject?.OnCurrentOrderChanged(previous); InvalidateProperties(); - _controlMaster?.InvalidateProperties(); - this.MarkDirty(); + m_ControlMaster?.InvalidateProperties(); } } @@ -1277,19 +877,39 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public DateTime BardEndTime { get; set; } - [SerializableProperty(20, useField: nameof(_tamable))] - [SaveFlag(nameof(ShouldSerializeTamable))] + [CommandProperty(AccessLevel.GameMaster)] + public double MinTameSkill { get; set; } + [CommandProperty(AccessLevel.GameMaster)] public bool Tamable { - get => _tamable && !_isParagon; + get => m_bTamable && !m_Paragon; + set => m_bTamable = value; + } + + [CommandProperty(AccessLevel.Administrator)] + public bool Summoned + { + get => _summoned; set { - _tamable = value; - this.MarkDirty(); + if (_summoned == value) + { + return; + } + + NextReacquireTime = Core.TickCount; + + _summoned = value; + Delta(MobileDelta.Noto); + + InvalidateProperties(); } } + [CommandProperty(AccessLevel.Administrator)] + public int ControlSlots { get; set; } = 1; + public virtual bool NoHouseRestrictions => false; public virtual bool IsHouseSummonable => false; @@ -1313,13 +933,19 @@ namespace Server.Mobiles public virtual bool GivesMLMinorArtifact => false; - // Reacquire only every ReacquireDelay, when attacked, or (for some creatures) on - // seeing movement - OSI parity and a CPU saver. + /* To save on cpu usage, RunUO creatures only reacquire creatures under the following circumstances: + * - 10 seconds have elapsed since the last time it tried + * - The creature was attacked + * - Some creatures, like dragons, will reacquire when they see someone move + * + * This functionality appears to be implemented on OSI as well + */ + public long NextReacquireTime { get; set; } public virtual TimeSpan ReacquireDelay => TimeSpan.FromSeconds(10.0); public virtual bool ReacquireOnMovement => false; - public virtual bool AcquireOnApproach => _isParagon; + public virtual bool AcquireOnApproach => m_Paragon; public virtual int AcquireOnApproachRange => 10; public static bool Summoning { get; set; } @@ -1332,6 +958,13 @@ 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; @@ -1366,25 +999,114 @@ 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 (_pendingDeleteTimer?.Running == true) + if (m_DeleteTimer?.Running == true) { - return _pendingDeleteTimer.Next - Core.Now; + return m_DeleteTimer.Next - Core.Now; } return TimeSpan.Zero; } } - 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; + 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; } [CommandProperty(AccessLevel.GameMaster)] public int ChaosDamage { get; set; } @@ -1392,12 +1114,15 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public int DirectDamage { get; set; } + // Is immune to breath damages public virtual bool BreathImmune => false; - public virtual bool CanFlee => !_isParagon; + public virtual bool CanFlee => !m_Paragon; 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; @@ -1449,6 +1174,7 @@ namespace Server.Mobiles public HonorContext ReceivedHonorContext { get; set; } public List MLQuests => + // Assign the quests if we don't have one, and if it is still null, return an empty list (m_MLQuests ??= StaticMLQuester ? MLQuestSystem.FindQuestList(GetType()) : ConstructQuestList()) ?? MLQuestSystem.EmptyList; public virtual MonsterAbility[] GetMonsterAbilities() => null; @@ -1674,7 +1400,7 @@ namespace Server.Mobiles return false; } - if (_team != c.Team || FightMode == FightMode.Evil && m.Karma < 0 || c.FightMode == FightMode.Evil && Karma < 0) + if (m_Team != c.Team || FightMode == FightMode.Evil && m.Karma < 0 || c.FightMode == FightMode.Evil && Karma < 0) { return true; } @@ -1792,7 +1518,7 @@ namespace Server.Mobiles var chance = Math.Clamp(700 + bonus, 220, 990); - chance -= (MaxLoyalty - _loyalty) * 10; + chance -= (MaxLoyalty - m_Loyalty) * 10; return chance / 1000.0; } @@ -1884,7 +1610,7 @@ namespace Server.Mobiles public override bool CheckPoisonImmunity(Mobile from, Poison poison) => base.CheckPoisonImmunity(from, poison) || - (_isParagon ? PoisonImpl.IncreaseLevel(PoisonImmune) : PoisonImmune)?.Level >= poison.Level; + (m_Paragon ? PoisonImpl.IncreaseLevel(PoisonImmune) : PoisonImmune)?.Level >= poison.Level; public void Unpacify() { @@ -1911,6 +1637,7 @@ namespace Server.Mobiles } int disruptThreshold; + // NPCs can use bandages too! if (!Core.AOS) { disruptThreshold = 0; @@ -2160,29 +1887,161 @@ namespace Server.Mobiles } } - // Pre-codegen loads only (versions 0-22); post-codegen bumps use MigrateFrom. - private void Deserialize(IGenericReader reader, int version) + public override void Serialize(IGenericWriter writer) { + base.Serialize(writer); - _currentAI = (AIType)reader.ReadInt(); - _defaultAI = (AIType)reader.ReadInt(); + writer.Write(22); // version - _rangePerception = reader.ReadInt(); - _rangeFight = reader.ReadInt(); + writer.Write((int)m_CurrentAI); + writer.Write((int)m_DefaultAI); - _team = 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(); _activeSpeed = reader.ReadDouble(); _passiveSpeed = reader.ReadDouble(); _currentSpeed = reader.ReadDouble(); - _home.X = reader.ReadInt(); - _home.Y = reader.ReadInt(); - _home.Z = reader.ReadInt(); + m_Home.X = reader.ReadInt(); + m_Home.Y = reader.ReadInt(); + m_Home.Z = reader.ReadInt(); if (version >= 1) { - _rangeHome = reader.ReadInt(); + RangeHome = reader.ReadInt(); if (version < 20) { @@ -2203,121 +2062,121 @@ namespace Server.Mobiles } else { - _rangeHome = 0; + RangeHome = 0; } if (version >= 2) { - _fightMode = (FightMode)reader.ReadInt(); + FightMode = (FightMode)reader.ReadInt(); _controlled = reader.ReadBool(); - _controlMaster = reader.ReadEntity(); - _controlTarget = reader.ReadEntity(); - _controlDest = reader.ReadPoint3D(); - _controlOrder = (OrderType)reader.ReadInt(); + m_ControlMaster = reader.ReadEntity(); + ControlTarget = reader.ReadEntity(); + ControlDest = reader.ReadPoint3D(); + m_ControlOrder = (OrderType)reader.ReadInt(); - _minTameSkill = reader.ReadDouble(); + MinTameSkill = reader.ReadDouble(); if (version < 9) { reader.ReadDouble(); } - _tamable = reader.ReadBool(); + m_bTamable = reader.ReadBool(); _summoned = reader.ReadBool(); if (_summoned) { - // The UnsummonTimer is restarted in AfterDeserialization. - _summonEnd = version >= 21 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); + SummonEnd = version >= 21 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); + new UnsummonTimer(this, SummonEnd - Core.Now).Start(); } - _controlSlots = reader.ReadInt(); + ControlSlots = reader.ReadInt(); } else { - _fightMode = FightMode.Closest; + FightMode = FightMode.Closest; _controlled = false; - _controlMaster = null; - _controlTarget = null; - _controlOrder = OrderType.None; + m_ControlMaster = null; + ControlTarget = null; + m_ControlOrder = OrderType.None; } if (version >= 3) { - _loyalty = reader.ReadInt(); + m_Loyalty = reader.ReadInt(); } else { - _loyalty = MaxLoyalty; + m_Loyalty = MaxLoyalty; // Wonderfully Happy } if (version >= 4) { - _currentWayPoint = reader.ReadEntity(); + CurrentWayPoint = reader.ReadEntity(); } if (version >= 5) { - _summonMaster = reader.ReadEntity(); + m_SummonMaster = reader.ReadEntity(); } if (version >= 6) { - _hitsMaxSeed = reader.ReadInt(); - _stamMaxSeed = reader.ReadInt(); - _manaMaxSeed = reader.ReadInt(); - _damageMin = reader.ReadInt(); - _damageMax = reader.ReadInt(); + HitsMaxSeed = reader.ReadInt(); + StamMaxSeed = reader.ReadInt(); + ManaMaxSeed = reader.ReadInt(); + m_DamageMin = reader.ReadInt(); + m_DamageMax = reader.ReadInt(); } if (version >= 7) { - _physicalResistanceSeed = reader.ReadInt(); - _physicalDamage = reader.ReadInt(); + m_PhysicalResistance = reader.ReadInt(); + PhysicalDamage = reader.ReadInt(); - _fireResistSeed = reader.ReadInt(); - _fireDamage = reader.ReadInt(); + m_FireResistance = reader.ReadInt(); + FireDamage = reader.ReadInt(); - _coldResistSeed = reader.ReadInt(); - _coldDamage = reader.ReadInt(); + m_ColdResistance = reader.ReadInt(); + ColdDamage = reader.ReadInt(); - _poisonResistSeed = reader.ReadInt(); - _poisonDamage = reader.ReadInt(); + m_PoisonResistance = reader.ReadInt(); + PoisonDamage = reader.ReadInt(); - _energyResistSeed = reader.ReadInt(); - _energyDamage = reader.ReadInt(); + m_EnergyResistance = 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(); - _isBonded = reader.ReadBool(); - _bondingBegin = reader.ReadDateTime(); - _ownerAbandonTime = reader.ReadDateTime(); + IsDeadPet = reader.ReadBool(); + m_IsBonded = reader.ReadBool(); + BondingBegin = reader.ReadDateTime(); + OwnerAbandonTime = reader.ReadDateTime(); } - _hasGeneratedLoot = version < 11 || reader.ReadBool(); + m_HasGeneratedLoot = version < 11 || reader.ReadBool(); - _isParagon = version >= 12 && reader.ReadBool(); + m_Paragon = version >= 12 && reader.ReadBool(); if (version >= 13 && reader.ReadBool()) { - _friends = reader.ReadEntityList(); + Friends = reader.ReadEntityList(); } - else if (version < 13 && _controlOrder >= OrderType.Unfriend) + else if (version < 13 && m_ControlOrder >= OrderType.Unfriend) { - ++_controlOrder; + ++m_ControlOrder; } if (version < 16 && Loyalty != MaxLoyalty) @@ -2327,8 +2186,8 @@ namespace Server.Mobiles if (version >= 14) { - _removeIfUntamed = reader.ReadBool(); - _removeStep = reader.ReadInt(); + RemoveIfUntamed = reader.ReadBool(); + RemoveStep = reader.ReadInt(); } var deleteTime = TimeSpan.Zero; @@ -2345,18 +2204,18 @@ namespace Server.Mobiles deleteTime = TimeSpan.FromDays(3.0); } - _pendingDeleteTimer = new DeleteTimer(this, deleteTime); - _pendingDeleteTimer.Start(); + m_DeleteTimer = new DeleteTimer(this, deleteTime); + m_DeleteTimer.Start(); } if (version >= 18) { - _corpseNameOverride = reader.ReadString(); + CorpseNameOverride = reader.ReadString(); } if (version >= 19) { - _homeMap = reader.ReadMap(); + HomeMap = reader.ReadMap(); } if (version >= 22) @@ -2369,57 +2228,25 @@ namespace Server.Mobiles MigrateMoveSpeeds(); } - // Legacy saves carry no bucket; the ctor guessed the type default. If the - // loaded speeds do not conform, the creature is custom. - if (_speedClass != SpeedLevel.None && ShouldSerializeSpeeds()) - { - _speedClass = SpeedLevel.None; - _speedEntry = null; - } - - // Feed the masters through the consolidated reference so the - // AfterDeserialization fan-out is uniform across both load paths. - _master = _controlMaster ?? _summonMaster; - - if (version <= 14 && _isParagon && Hue == 0x31) + if (version <= 14 && m_Paragon && Hue == 0x31) { Hue = Paragon.Hue; // Paragon hue fixed, should now be 0x501. } - } - - [AfterDeserialization] - private void AfterDeserialization() - { - _controlMaster = _controlled ? _master : null; - _summonMaster = _summoned ? _master : null; 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(_currentAI); + ChangeAIType(m_CurrentAI); AddFollowers(); if (IsAnimatedDead) { - AnimateDeadSpell.Register(_summonMaster, this); + AnimateDeadSpell.Register(m_SummonMaster, this); } } @@ -2474,7 +2301,8 @@ namespace Server.Mobiles return true; } - // Happens for all questers, even those with nothing to offer right now. + // Note: Yes, this happens for all questers (regardless of type, e.g. escorts), + // even if they can't offer you anything at the moment if (MLQuestSystem.Enabled && CanGiveMLQuest && from is PlayerMobile mobile) { // You need to mark your quest items so I don't take the wrong object. Then speak to me. @@ -2505,7 +2333,7 @@ namespace Server.Mobiles AIType.AI_Vendor => new VendorAI(this), AIType.AI_Mage => new MageAI(this), AIType.AI_Predator => - //TODO Implement PredatorAI + // m_AI = new PredatorAI(this); new MeleeAI(this), AIType.AI_Thief => new ThiefAI(this), _ => null @@ -2525,7 +2353,7 @@ namespace Server.Mobiles public void RemoveFollowers() { - var master = _controlMaster ?? _summonMaster; + var master = m_ControlMaster ?? m_SummonMaster; if (master != null) { master.Followers -= Math.Min(ControlSlots, master.Followers); @@ -2539,7 +2367,7 @@ namespace Server.Mobiles public void AddFollowers() { - var master = _controlMaster ?? _summonMaster; + var master = m_ControlMaster ?? m_SummonMaster; if (master != null) { master.Followers += ControlSlots; @@ -2585,7 +2413,7 @@ namespace Server.Mobiles public virtual void OnGaveMeleeAttack(Mobile defender, int damage) { - var p = _isParagon ? PoisonImpl.IncreaseLevel(HitPoison) : HitPoison; + var p = m_Paragon ? PoisonImpl.IncreaseLevel(HitPoison) : HitPoison; if (p != null && HitPoisonChance >= Utility.RandomDouble()) { @@ -2614,17 +2442,17 @@ namespace Server.Mobiles AIObject = null; } - if (_pendingDeleteTimer != null) + if (m_DeleteTimer != null) { - _pendingDeleteTimer.Stop(); - _pendingDeleteTimer = null; + m_DeleteTimer.Stop(); + m_DeleteTimer = null; } FocusMob = null; if (IsAnimatedDead) { - AnimateDeadSpell.Unregister(_summonMaster, this); + AnimateDeadSpell.Unregister(m_SummonMaster, this); } if (Summoned && SummonMaster != null) @@ -2643,6 +2471,13 @@ namespace Server.Mobiles base.OnAfterDelete(); } + /* + * This function can be overridden.. so a "Strongest" mobile, can have a different definition depending + * on who check for value + * -Could add a FightMode.Preferred + * + */ + public virtual double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) { if (bPlayerOnly && !m.Player) @@ -2658,7 +2493,8 @@ namespace Server.Mobiles }; } - // Turn: negative = left, positive = right. + // Turn, - for left, + for right + // Basic for now, needs work public virtual void Turn(int iTurnSteps) { var v = (int)Direction; @@ -2675,7 +2511,7 @@ namespace Server.Mobiles public bool IsHurt() => Hits != HitsMax; - public double GetHomeDistance() => this.GetDistanceToSqrt(_home); + public double GetHomeDistance() => this.GetDistanceToSqrt(m_Home); public virtual int GetTeamSize(int iRange) { @@ -2702,7 +2538,7 @@ namespace Server.Mobiles aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true)); } - var ct = _controlOrder; + var ct = m_ControlOrder; if (AIObject != null) { @@ -2776,7 +2612,7 @@ namespace Server.Mobiles AIObject?.GetContextMenuEntries(from, ref list); } - if (_tamable && !_controlled && from.Alive) + if (m_bTamable && !_controlled && from.Alive) { list.Add(new TameEntry(from.Female ? AllowFemaleTamer : AllowMaleTamer)); } @@ -2827,7 +2663,7 @@ namespace Server.Mobiles } public override bool IsHarmfulCriminal(Mobile target) => - (!Controlled || target != _controlMaster) && (!Summoned || target != _summonMaster) && + (!Controlled || target != m_ControlMaster) && (!Summoned || target != m_SummonMaster) && (target is not BaseCreature { InitialInnocent: true } creature || creature.Controlled) && (target is not PlayerMobile mobile || mobile.PermaFlags.Count <= 0) && base.IsHarmfulCriminal(target); @@ -2837,13 +2673,13 @@ namespace Server.Mobiles if (Controlled || Summoned) { - if (_controlMaster?.Player == true) + if (m_ControlMaster?.Player == true) { - _controlMaster.CriminalAction(false); + m_ControlMaster.CriminalAction(false); } - else if (_summonMaster?.Player == true) + else if (m_SummonMaster?.Player == true) { - _summonMaster.CriminalAction(false); + m_SummonMaster.CriminalAction(false); } } } @@ -2852,7 +2688,7 @@ namespace Server.Mobiles { base.DoHarmful(target, indirect); - if (target == this || target == _controlMaster || target == _summonMaster || !Controlled && !Summoned) + if (target == this || target == m_ControlMaster || target == m_SummonMaster || !Controlled && !Summoned) { return; } @@ -2902,11 +2738,12 @@ namespace Server.Mobiles { if (Combatant != null) { - return false; // in combat, not idling + return false; // in combat.. not idling } if (m_IdleReleaseTime > DateTime.MinValue) { + // idling... if (Core.Now >= m_IdleReleaseTime) { m_IdleReleaseTime = DateTime.MinValue; @@ -2918,7 +2755,7 @@ namespace Server.Mobiles if (Utility.Random(100) < 95) { - return false; // chose not to enter the idle state + return false; // not idling, but don't want to enter idle state } var idleSeconds = Utility.RandomMinMax(NPCSpeeds.MinIdleSeconds, NPCSpeeds.MaxIdleSeconds); @@ -2958,6 +2795,12 @@ namespace Server.Mobiles return true; // entered idle state } + /* + this way, due to the huge number of locations this will have to be changed + Perhaps we can change this in the future when fixing game play is not the + major issue. + */ + public virtual void CheckedAnimate(int action, int frameCount, int repeatCount, bool forward, bool repeat, int delay) { if (!Mounted) @@ -3026,7 +2869,7 @@ namespace Server.Mobiles SpeechType?.OnMovement(this, m, oldLocation); - // Notice sound + /* Begin notice sound */ if ((!m.Hidden || m.AccessLevel == AccessLevel.Player) && m.Player && FightMode != FightMode.Aggressor && FightMode != FightMode.None && Combatant == null && !Controlled && !Summoned && !BardPacified && InRange(m.Location, 18) && !InRange(oldLocation, 18)) @@ -3038,6 +2881,7 @@ namespace Server.Mobiles PlaySound(GetAngerSound()); } + /* End notice sound */ if (MLQuestSystem.Enabled && CanShout && m is PlayerMobile mobile) { @@ -3111,7 +2955,7 @@ namespace Server.Mobiles list.Add(TotalWeight == 1 ? 1072788 : 1072789, TotalWeight); // Weight: ~1_WEIGHT~ stones } - if (_controlOrder == OrderType.Guard) + if (m_ControlOrder == OrderType.Guard) { list.Add(1080078); // guarding } @@ -3123,7 +2967,7 @@ namespace Server.Mobiles } else if (Controlled && Commandable) { - // Deliberate: show only (bonded), never (bonded) and (tame) together. + // Intentional difference (showing ONLY bonded when bonded instead of bonded & tame) if (IsBonded) { list.Add(1049608); // (bonded) @@ -3185,7 +3029,7 @@ namespace Server.Mobiles { if (treasureLevel >= 0) { - if (_isParagon && Paragon.ChestChance > Utility.RandomDouble()) + if (m_Paragon && Paragon.ChestChance > Utility.RandomDouble()) { PackItem(new ParagonChest(Name, treasureLevel)); } @@ -3195,7 +3039,7 @@ namespace Server.Mobiles } } - if (_isParagon && Paragon.ChocolateIngredientChance > Utility.RandomDouble()) + if (m_Paragon && Paragon.ChocolateIngredientChance > Utility.RandomDouble()) { switch (Utility.Random(4)) { @@ -3223,9 +3067,9 @@ namespace Server.Mobiles } } - if (!Summoned && !NoKillAwards && !_hasGeneratedLoot) + if (!Summoned && !NoKillAwards && !m_HasGeneratedLoot) { - _hasGeneratedLoot = true; + m_HasGeneratedLoot = true; GenerateLoot(false); } @@ -3430,7 +3274,7 @@ namespace Server.Mobiles MondainsLegacy.GiveArtifactTo(mob); } } - else if (_isParagon) + else if (m_Paragon) { if (Paragon.CheckArtifactChance(mob, this)) { @@ -3439,6 +3283,9 @@ namespace Server.Mobiles } } + [GeneratedEvent(nameof(CreatureDeathEvent))] + public static partial void CreatureDeathEvent(BaseCreature bc); + public override void OnDeath(Container c) { if (IsBonded) @@ -3461,6 +3308,7 @@ namespace Server.Mobiles ProcessDelta(); SendIncomingPacket(); + // TODO: This can be done in Parallel if there are lots of them. var aggressors = Aggressors; for (var i = 0; i < aggressors.Count; ++i) @@ -3500,7 +3348,7 @@ namespace Server.Mobiles OwnerAbandonTime = DateTime.MinValue; } - CreatureEvents.CreatureDeathEvent(this); + CreatureDeathEvent(this); CheckStatTimers(); return; @@ -3534,6 +3382,7 @@ namespace Server.Mobiles if (ds.m_Mobile == killer) { + // If the titles system gets feature flagged, it will be supported titles.Add(ds.m_Mobile); fame.Add(totalFame); karma.Add(totalKarma); @@ -3629,17 +3478,20 @@ namespace Server.Mobiles c.Delete(); } - CreatureEvents.CreatureDeathEvent(this); + CreatureDeathEvent(this); } + [GeneratedEvent(nameof(CreatureDeletedEvent))] + public static partial void CreatureDeletedEvent(BaseCreature bc); + public override void OnDelete() { - CreatureEvents.CreatureDeletedEvent(this); + CreatureDeletedEvent(this); - var m = _controlMaster; + var m = m_ControlMaster; SetControlMaster(null); - SummonMaster = null; // uncontrolled summons have no control master to clear through + SummonMaster = null; ReceivedHonorContext?.Cancel(); base.OnDelete(); @@ -3685,11 +3537,6 @@ namespace Server.Mobiles Controlled = false; ControlTarget = null; ControlOrder = OrderType.None; - - if (_summoned) - { - SummonMaster = null; - } } else { @@ -3714,16 +3561,11 @@ namespace Server.Mobiles ControlTarget = null; ControlOrder = OrderType.Come; - if (_summoned) - { - SummonMaster = m; - } - - if (_pendingDeleteTimer != null) + if (m_DeleteTimer != null) { - _pendingDeleteTimer.Stop(); - _pendingDeleteTimer = null; + m_DeleteTimer.Stop(); + m_DeleteTimer = null; } } @@ -3917,7 +3759,7 @@ namespace Server.Mobiles { // *rummages through a corpse and takes an item* PublicOverheadMessage(MessageType.Emote, 0x3B2, 1008086); - //TODO Instance rummaged loot + // TODO: Instancing of Rummaged stuff. return true; } } @@ -3938,14 +3780,14 @@ namespace Server.Mobiles return BardMaster; } - if (_controlled && _controlMaster != null) + if (_controlled && m_ControlMaster != null) { - return _controlMaster; + return m_ControlMaster; } - if (_summoned && _summonMaster != null) + if (_summoned && m_SummonMaster != null) { - return _summonMaster; + return m_SummonMaster; } return base.GetDamageMaster(damagee); @@ -4217,17 +4059,17 @@ namespace Server.Mobiles if (this is not BaseEscortable && !Summoned && !Deleted && !IsStabled) { StopDeleteTimer(); - _pendingDeleteTimer = new DeleteTimer(this, TimeSpan.FromDays(3.0)); - _pendingDeleteTimer.Start(); + m_DeleteTimer = new DeleteTimer(this, TimeSpan.FromDays(3.0)); + m_DeleteTimer.Start(); } } public void StopDeleteTimer() { - if (_pendingDeleteTimer != null) + if (m_DeleteTimer != null) { - _pendingDeleteTimer.Stop(); - _pendingDeleteTimer = null; + m_DeleteTimer.Stop(); + m_DeleteTimer = null; } } @@ -4263,7 +4105,11 @@ namespace Server.Mobiles } } - // Solen-style acid; override for other harmful drops (kappa slime, etc.). + /* + Solen Style, override me for other mobiles/items: + kappa+acidslime, grizzles+whatever, etc. + */ + public virtual Item NewHarmfulItem() => new Acid(TimeSpan.FromSeconds(10), 30, 30); public virtual void StopFlee() @@ -4304,7 +4150,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 && _team == c._team + OppositionGroup?.IsEnemy(this, m) != true && m is BaseCreature c && m_Team == c.m_Team && (_summoned || _controlled) == (c._summoned || c._controlled); public virtual Allegiance GetFactionAllegiance(Mobile mob) @@ -4496,15 +4342,16 @@ namespace Server.Mobiles if (Core.SE) { - _loyalty = MaxLoyalty; + m_Loyalty = MaxLoyalty; } - else if (_loyalty < MaxLoyalty) + else if (m_Loyalty < MaxLoyalty) { + // Calculate the loyalty increase var loyaltyIncrease = Utility.CoinFlips(amount, MaxLoyaltyIncrease) * 10; - if (loyaltyIncrease > 0) + if (loyaltyIncrease > 0) // Only update if there's an actual increase { - _loyalty = Math.Min(MaxLoyalty, _loyalty + loyaltyIncrease); + m_Loyalty = Math.Min(MaxLoyalty, m_Loyalty + loyaltyIncrease); SayTo(from, 502060); // Your pet looks happier. } } @@ -4520,7 +4367,7 @@ namespace Server.Mobiles if (IsBondable && !IsBonded) { - var master = _controlMaster; + var master = m_ControlMaster; if (master != null && master == from) // So friends can't start the bonding process { @@ -4707,6 +4554,7 @@ namespace Server.Mobiles } } + /* Sanity check */ if (baseToSet > theirSkill.CapFixedPoint || m.Skills.Total - theirSkill.BaseFixedPoint + baseToSet > m.Skills.Cap) { @@ -4873,14 +4721,14 @@ namespace Server.Mobiles public void SetDamage(int val) { - _damageMin = val; - _damageMax = val; + m_DamageMin = val; + m_DamageMax = val; } public void SetDamage(int min, int max) { - _damageMin = min; - _damageMax = max; + m_DamageMin = min; + m_DamageMax = max; } public void SetHits(int val) @@ -5014,27 +4862,27 @@ namespace Server.Mobiles { case ResistanceType.Physical: { - _physicalResistanceSeed = val; + m_PhysicalResistance = val; break; } case ResistanceType.Fire: { - _fireResistSeed = val; + m_FireResistance = val; break; } case ResistanceType.Cold: { - _coldResistSeed = val; + m_ColdResistance = val; break; } case ResistanceType.Poison: { - _poisonResistSeed = val; + m_PoisonResistance = val; break; } case ResistanceType.Energy: { - _energyResistSeed = val; + m_EnergyResistance = val; break; } } @@ -5170,55 +5018,23 @@ namespace Server.Mobiles } } - // A type's constant bucket; runtime state changes assign SpeedClass instead. - public virtual SpeedLevel DefaultSpeedClass => SpeedLevel.None; - - // Resolved once per creature; serialization consults the table four times per mob - // per save (and again on elided loads), so the dictionary walk must not repeat. - private NPCSpeeds.SpeedClassEntry _speedEntry; - - private NPCSpeeds.SpeedClassEntry SpeedEntry => _speedEntry ??= NPCSpeeds.FindEntry(_speedClass); - - private SpeedLevel? _resolvedDefaultSpeedClass; - - // The bucket a fresh spawn of this type resolves to (construction-time only). - private SpeedLevel ResolvedDefaultSpeedClass => - _resolvedDefaultSpeedClass ??= NPCSpeeds.ResolveDefaultLevel(this); + // If this needs to be serialized, recommend creating a hash or registry id. Don't serialize strings. + public virtual SpeedLevel SpeedClass => SpeedLevel.None; public virtual void GetSpeeds(out double activeSpeed, out double passiveSpeed) { - var entry = SpeedEntry; - - if (entry == null) - { - if (_speedClass == SpeedLevel.None) - { - // A custom creature is its own reference. - activeSpeed = _activeSpeed; - passiveSpeed = _passiveSpeed; - return; - } - - throw new InvalidOperationException( - $"{GetType()} names bucket {_speedClass} but the table has no entry - is {"Data/npc-speeds.json"} missing?" - ); - } - - activeSpeed = entry.ActiveSpeed; - passiveSpeed = entry.PassiveSpeed; + NPCSpeeds.GetSpeeds(this, out activeSpeed, out passiveSpeed); } - // Move speeds are optional (0 = inherit), so this tolerates an unloaded table. public virtual void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed) { - var entry = SpeedEntry; - - activeMoveSpeed = entry?.ActiveMoveSpeed ?? 0; - passiveMoveSpeed = entry?.PassiveMoveSpeed ?? 0; + NPCSpeeds.GetMoveSpeeds(this, out activeMoveSpeed, out passiveMoveSpeed); } - // Pre-v22 saves carry no movement clock. Think speeds matching today's GetSpeeds - // mean never hand-tuned: adopt today's move values; tuned creatures keep inheriting. + // Pre-v22 saves carry no movement clock. A creature whose serialized think speeds + // still match what it would spawn with today was never hand-tuned: adopt today's + // move values so existing worlds (and pets) pick up npc-speeds pacing without a + // respawn. Tuned creatures keep movement inheriting their think clock. internal void MigrateMoveSpeeds() { GetSpeeds(out var activeSpeed, out var passiveSpeed); @@ -5267,7 +5083,7 @@ namespace Server.Mobiles GenerateLoot(); - if (_isParagon) + if (m_Paragon) { if (Fame < 1250) { @@ -5667,6 +5483,8 @@ namespace Server.Mobiles var onSelf = patient == this; + // DoBeneficial( patient ); + RevealingAction(); if (!onSelf) @@ -5709,7 +5527,7 @@ namespace Server.Mobiles { patient.SendLocalizedMessage(1010059); // You have been cured of all poisons. - CheckSkill(SkillName.Healing, 0.0, 60.0 + poisonLevel * 10.0); //TODO Verify formula + CheckSkill(SkillName.Healing, 0.0, 60.0 + poisonLevel * 10.0); // TODO: Verify formula CheckSkill(SkillName.Anatomy, 0.0, 100.0); } } @@ -5908,6 +5726,7 @@ namespace Server.Mobiles using var toRelease = PooledRefQueue.Create(); + // added array for wild creatures in house regions to be removed using var toRemove = PooledRefQueue.Create(); foreach (var m in World.Mobiles.Values) @@ -5965,7 +5784,7 @@ namespace Server.Mobiles } } - // Wild creatures squatting in houses are removed outright. + // added lines to check if a wild creature in a house region has to be removed or not if (!c.Controlled && !c.IsStabled && (c.Region.IsPartOf() && c.CanBeDamaged() || c.RemoveIfUntamed && c.Spawner == null)) { @@ -5987,13 +5806,12 @@ namespace Server.Mobiles var c = toRelease.Dequeue(); c.Say(1043255, c.Name); // ~1_NAME~ appears to have decided that is better off without a master! - c.Loyalty = BaseCreature.MaxLoyalty; + c.Loyalty = BaseCreature.MaxLoyalty; // Wonderfully Happy c.IsBonded = false; c.BondingBegin = DateTime.MinValue; c.OwnerAbandonTime = DateTime.MinValue; c.ControlTarget = null; - // Release directly: a creature left alone with its AI disabled would - // otherwise never release and permanently hold its owner's follower slots. + // This will prevent no release of creatures left alone with AI disabled (and consequent bug of Followers) c.AIObject.DoOrderRelease(); c.DropBackpack(); } diff --git a/Projects/UOContent/Mobiles/CreatureEvents.cs b/Projects/UOContent/Mobiles/CreatureEvents.cs deleted file mode 100644 index d1fdc9112..000000000 --- a/Projects/UOContent/Mobiles/CreatureEvents.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 fca41ec25..178436fb9 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(CreatureEvents.CreatureDeathEvent))] + [OnEvent(nameof(CreatureDeathEvent))] public static void StopEffect(Mobile m, bool message = false) { if (m_Table.Remove(m, out var timer)) diff --git a/Projects/UOContent/Mobiles/NPCSpeeds.cs b/Projects/UOContent/Mobiles/NPCSpeeds.cs index 91a649871..8f5e908bb 100644 --- a/Projects/UOContent/Mobiles/NPCSpeeds.cs +++ b/Projects/UOContent/Mobiles/NPCSpeeds.cs @@ -8,7 +8,7 @@ namespace Server.Mobiles; public enum SpeedLevel { - None, // no bucket: the creature's own speeds are authoritative (custom) + None, VerySlow, Slow, Medium, @@ -26,28 +26,33 @@ public static class NPCSpeeds public static int MinIdleSeconds { get; private set; } public static int MaxIdleSeconds { get; private set; } - // Construction-time resolution of a type's bucket: an explicit DefaultSpeedClass, - // else the table's type list, else Medium so unconfigured creatures never construct - // at 0/0. None only when the table itself is unloaded (test fixtures). - public static SpeedLevel ResolveDefaultLevel(BaseCreature bc) + public static void GetSpeeds(BaseCreature bc, out double activeSpeed, out double passiveSpeed) { - if (bc.DefaultSpeedClass != SpeedLevel.None) + if ((bc.SpeedClass == SpeedLevel.None || !_speedsByLevel.TryGetValue(bc.SpeedClass, out var sp)) && + !_speedsByType.TryGetValue(bc.GetType(), out sp)) { - return bc.DefaultSpeedClass; + sp = _speedsByLevel[SpeedLevel.Medium]; } - if (_speedsByType.TryGetValue(bc.GetType(), out var sp)) - { - return sp.Level; - } - - return _speedsByLevel.ContainsKey(SpeedLevel.Medium) ? SpeedLevel.Medium : SpeedLevel.None; + activeSpeed = sp.ActiveSpeed; + passiveSpeed = sp.PassiveSpeed; } - // Null for None (custom) or an unloaded table. Creatures cache the result — the - // table is immutable after Configure. - public static SpeedClassEntry FindEntry(SpeedLevel level) => - level == SpeedLevel.None ? null : _speedsByLevel.GetValueOrDefault(level); + // Move speeds are optional (0 = inherit), so this tolerates a missing entry or table. + public static void GetMoveSpeeds(BaseCreature bc, out double activeMoveSpeed, out double passiveMoveSpeed) + { + if ((bc.SpeedClass == SpeedLevel.None || !_speedsByLevel.TryGetValue(bc.SpeedClass, out var sp)) && + !_speedsByType.TryGetValue(bc.GetType(), out sp) && + !_speedsByLevel.TryGetValue(SpeedLevel.Medium, out sp)) + { + activeMoveSpeed = 0; + passiveMoveSpeed = 0; + return; + } + + activeMoveSpeed = sp.ActiveMoveSpeed; + passiveMoveSpeed = sp.PassiveMoveSpeed; + } public static void RegisterSpeed(SpeedClassEntry entry) { diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index d60a10aa7..83c0e4cca 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -3553,6 +3553,7 @@ namespace Server.Mobiles pet.Internalize(); pet.SetControlMaster(null); + pet.SummonMaster = null; pet.IsStabled = true; pet.StabledBy = this; @@ -3600,6 +3601,12 @@ namespace Server.Mobiles if (Followers + pet.ControlSlots <= FollowersMax) { pet.SetControlMaster(this); + + if (pet.Summoned) + { + 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 0fb347fd4..847209869 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs @@ -255,6 +255,7 @@ namespace Server.Mobiles pet.Internalize(); pet.SetControlMaster(null); + pet.SummonMaster = null; pet.IsStabled = true; pet.StabledBy = from; @@ -355,6 +356,12 @@ namespace Server.Mobiles private void DoClaim(Mobile from, BaseCreature pet) { pet.SetControlMaster(from); + + if (pet.Summoned) + { + pet.SummonMaster = from; + } + pet.ControlTarget = from; pet.ControlOrder = OrderType.Follow; diff --git a/Projects/UOContent/Regions/BaseRegion.cs b/Projects/UOContent/Regions/BaseRegion.cs index b18c7fa18..776ec269d 100644 --- a/Projects/UOContent/Regions/BaseRegion.cs +++ b/Projects/UOContent/Regions/BaseRegion.cs @@ -113,7 +113,7 @@ public class BaseRegion : Region m_RectBuffer2.RemoveAt(k); var sz = rect.Start.Z; - var ez = rect.End.X; + var ez = rect.End.Z; if (l1 < l2) { diff --git a/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs b/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs index 578bdc482..c0f3c3ed0 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(CreatureEvents.CreatureDeathEvent))] - [OnEvent(nameof(CreatureEvents.CreatureDeletedEvent))] + [OnEvent(nameof(BaseCreature.CreatureDeathEvent))] + [OnEvent(nameof(BaseCreature.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 6a5d4850a..585d37284 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(CreatureEvents.CreatureDeathEvent))] + [OnEvent(nameof(BaseCreature.CreatureDeathEvent))] [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))] public static void OnDeathEvent(Mobile m) { diff --git a/dev-docs/content-patterns.md b/dev-docs/content-patterns.md index 2c78fcd67..ec56e1bb3 100644 --- a/dev-docs/content-patterns.md +++ b/dev-docs/content-patterns.md @@ -262,9 +262,8 @@ All "speed" values are **delays in seconds** (smaller = faster). A creature runs (combat decisions, target acquisition, spell timing). - **Move clock** — `ActiveMoveSpeed`/`PassiveMoveSpeed`/`CurrentMoveSpeed`: seconds per step. Inherits the matching think value until overridden, so a creature configured with - only think speeds behaves as one clock. The properties read the raw override (`0` = - inheriting); `CurrentMoveSpeed` is the resolved pace. Any value is legal — steps are - scheduled independently of think ticks, so the two need not divide evenly. + only think speeds behaves as one clock. Any value is legal — steps are scheduled + independently of think ticks, so the two need not divide evenly. Speeds normally come from `Distribution/Data/npc-speeds.json` (via `SpeedClass` or type lists); `activeMove`/`passiveMove` are optional per bucket. Prefer data over code: