From 3b97e8d39c36de5e83c6eab3571d12f2eed264a5 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 3 Jul 2023 09:45:22 -0700 Subject: [PATCH] fix: Fixes stabled, abilities targeting self, and unsummon memory leak (#1418) ## **MAJOR CHANGE** * `Stabled` has been moved to `PlayerMobile`. * New methods added, `PlayerMobile.AddStabled` and `PlayerMobile.RemoveStabled`. * Added `PlayerMobile.AddFollower` and `PlayerMobile.RemoveFollower`. * `Stabled`, `AutoStabled`, and `AllFollowers` are now `HashSet` and **_CAN BE NULL_**. ### Summary * Fixes monster abilities causing harm to the monster through reflect * Adds `CanTriggerAgainstSelf` to override this for healing or some other self-affecting ability * Fixes a major memory leak where `UnsummonTimer` from animated dead spell lasts up-to 24hrs and therefore holds onto references of dead/deleted mobs. * Fixes another minor leak where a mob is not unregistered from the animated dead spell list until the next spell cast. --- Projects/Server/Mobiles/Mobile.Migrations.cs | 18 ++ Projects/Server/Mobiles/Mobile.cs | 39 +-- .../Serialization/SerializationExtensions.cs | 20 +- Projects/UOContent/Commands/Handlers.cs | 47 ++- Projects/UOContent/Items/Misc/Teleporter.cs | 2 +- .../Special/Solen Items/BallOfSummoning.cs | 14 +- .../Mobiles/Abilities/MonsterAbility.cs | 6 +- Projects/UOContent/Mobiles/BaseCreature.cs | 59 ++-- Projects/UOContent/Mobiles/PlayerMobile.cs | 280 +++++++++++------- .../Mobiles/Vendors/NPC/AnimalTrainer.cs | 124 ++++---- .../Spells/Necromancy/AnimateDeadSpell.cs | 15 +- .../UOContent/Spells/Ninjitsu/MirrorImage.cs | 2 +- Projects/UOContent/Spells/UnsummonTimer.cs | 45 ++- 13 files changed, 391 insertions(+), 280 deletions(-) create mode 100644 Projects/Server/Mobiles/Mobile.Migrations.cs diff --git a/Projects/Server/Mobiles/Mobile.Migrations.cs b/Projects/Server/Mobiles/Mobile.Migrations.cs new file mode 100644 index 000000000..849060e97 --- /dev/null +++ b/Projects/Server/Mobiles/Mobile.Migrations.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Server; + +public partial class Mobile +{ + // Migrating Stabled property to PlayerMobile + public static Dictionary> StableMigrations { get; private set; } + + public static void AddToStabledMigration(Mobile m, HashSet stabled) + { + if (stabled?.Count > 0) + { + StableMigrations ??= new Dictionary>(); + StableMigrations[m] = stabled; + } + } +} diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 39c8277bd..da5fa449f 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -180,7 +180,7 @@ public delegate int AOSStatusHandler(Mobile from, int index); /// /// Base class representing players, npcs, and creatures. /// -public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyListEntity +public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyListEntity { // Allow four warmode changes in 0.5 seconds, any more will be delay for two seconds private const int WarmodeCatchCount = 4; @@ -426,8 +426,6 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis } } - public List Stabled { get; private set; } - [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public VirtueInfo Virtues { get; private set; } @@ -2279,7 +2277,7 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis public virtual void Serialize(IGenericWriter writer) { - writer.Write(33); // version + writer.Write(34); // version writer.WriteDeltaTime(LastStrGain); writer.WriteDeltaTime(LastIntGain); @@ -2317,8 +2315,15 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis // writer.Write(CreationTime); - Stabled.Tidy(); - writer.Write(Stabled); + // if (Stabled == null) + // { + // writer.Write(0); + // } + // else + // { + // Stabled.Tidy(); + // writer.Write(Stabled); + // } writer.Write(CantWalk); @@ -2448,11 +2453,6 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis } } - for (var i = 0; i < Stabled.Count; i++) - { - Stabled[i].Delete(); - } - SendRemovePacket(); m_Guild?.OnDelete(this); @@ -6074,6 +6074,11 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis switch (version) { + case 34: + { + // Moved Stabled to PlayerMobile + goto case 33; + } case 33: { // Removed created @@ -6148,7 +6153,11 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis case 22: // Just removed followers case 21: { - Stabled = reader.ReadEntityList(); + if (version < 34) + { + // Migrated to PlayerMobile + AddToStabledMigration(this, reader.ReadEntitySet(true)); + } goto case 20; } @@ -6285,11 +6294,6 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis } case 0: { - if (version < 21) - { - Stabled = new List(); - } - if (version < 18) { Virtues = new VirtueInfo(); @@ -7721,7 +7725,6 @@ public class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyLis Aggressors = new List(); Aggressed = new List(); Virtues = new VirtueInfo(); - Stabled = new List(); DamageEntries = new List(); NextSkillTime = Core.TickCount; diff --git a/Projects/Server/Serialization/SerializationExtensions.cs b/Projects/Server/Serialization/SerializationExtensions.cs index 4ee793941..5650c5398 100644 --- a/Projects/Server/Serialization/SerializationExtensions.cs +++ b/Projects/Server/Serialization/SerializationExtensions.cs @@ -48,10 +48,18 @@ public static class SerializationExtensions return entity?.Created <= reader.LastSerialized ? entity : null; } - public static List ReadEntityList(this IGenericReader reader) where T : class, ISerializable + public static List ReadEntityList( + this IGenericReader reader, + bool nullIfEmpty = false + ) where T : class, ISerializable { var count = reader.ReadInt(); + if (count == 0 && nullIfEmpty) + { + return null; + } + var list = new List(count); for (var i = 0; i < count; ++i) @@ -66,10 +74,18 @@ public static class SerializationExtensions return list; } - public static HashSet ReadEntitySet(this IGenericReader reader) where T : class, ISerializable + public static HashSet ReadEntitySet( + this IGenericReader reader, + bool nullIfEmpty = false + ) where T : class, ISerializable { var count = reader.ReadInt(); + if (count == 0 && nullIfEmpty) + { + return null; + } + var set = new HashSet(count); for (var i = 0; i < count; ++i) diff --git a/Projects/UOContent/Commands/Handlers.cs b/Projects/UOContent/Commands/Handlers.cs index 9c8d59c4a..c62957329 100644 --- a/Projects/UOContent/Commands/Handlers.cs +++ b/Projects/UOContent/Commands/Handlers.cs @@ -287,37 +287,34 @@ namespace Server.Commands { var pets = pm.AllFollowers; - if (pets.Count > 0) + if (!(pets?.Count > 0)) { - CommandLogging.WriteLine( - from, - $"{from.AccessLevel} {CommandLogging.Format(from)} getting all followers of {CommandLogging.Format(pm)}" - ); + from.SendMessage("There were no pets found for that player."); + return; + } - if (pets.Count == 1) - { - from.SendMessage($"That player has {pets.Count} pet."); - } - else - { - from.SendMessage($"That player has {pets.Count} pets."); - } + CommandLogging.WriteLine( + from, + $"{from.AccessLevel} {CommandLogging.Format(from)} getting all followers of {CommandLogging.Format(pm)}" + ); - for (var i = 0; i < pets.Count; ++i) - { - var pet = pets[i]; - - if (pet is IMount mount) - { - mount.Rider = null; // make sure it's dismounted - } - - pet.MoveToWorld(from.Location, from.Map); - } + if (pets.Count == 1) + { + from.SendMessage($"That player has {pets.Count} pet."); } else { - from.SendMessage("There were no pets found for that player."); + from.SendMessage($"That player has {pets.Count} pets."); + } + + foreach (var pet in pets) + { + if (pet is IMount mount) + { + mount.Rider = null; // make sure it's dismounted + } + + pet.MoveToWorld(from.Location, from.Map); } } else if (obj is Mobile master && master.Player) diff --git a/Projects/UOContent/Items/Misc/Teleporter.cs b/Projects/UOContent/Items/Misc/Teleporter.cs index 047390984..af981b4a1 100644 --- a/Projects/UOContent/Items/Misc/Teleporter.cs +++ b/Projects/UOContent/Items/Misc/Teleporter.cs @@ -867,7 +867,7 @@ public partial class ConditionTeleporter : Teleporter } if (GetFlag(ConditionFlag.DenyFollowers) && - (m.Followers != 0 || m is PlayerMobile mobile && mobile.AutoStabled.Count != 0)) + (m.Followers != 0 || m is PlayerMobile mobile && mobile.AutoStabled?.Count != 0)) { m.SendLocalizedMessage(1077250); // No pets permitted beyond this point. return false; diff --git a/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs b/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs index fcb6a1293..22bb66795 100644 --- a/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs +++ b/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs @@ -166,7 +166,7 @@ public partial class BallOfSummoning : Item, TranslocationItem { var pet = Pet; - if (Deleted || pet == null || RootParent != from) + if (Deleted || pet == null || RootParent != from || from is not PlayerMobile pm) { return; } @@ -186,7 +186,7 @@ public partial class BallOfSummoning : Item, TranslocationItem // The Crystal Ball fills with a blue mist. Your pet is not responding to the summons. this.SendLocalizedMessageTo(from, 1054125, 0x5); } - else if ((!pet.Controlled || pet.ControlMaster != from) && !from.Stabled.Contains(pet)) + else if ((!pet.Controlled || pet.ControlMaster != from) && pm.Stabled?.Contains(pet) != true) { // The Crystal Ball fills with a grey mist. You are not the owner of the pet you are attempting to summon. this.SendLocalizedMessageTo(from, 1054126, 0x8FD); @@ -221,7 +221,7 @@ public partial class BallOfSummoning : Item, TranslocationItem { var pet = Pet; - if (pet == null) + if (pet == null || from is not PlayerMobile pm) { return; } @@ -242,12 +242,8 @@ public partial class BallOfSummoning : Item, TranslocationItem pet.IsStabled = false; pet.StabledBy = null; - from.Stabled.Remove(pet); - - if (from is PlayerMobile mobile) - { - mobile.AutoStabled.Remove(pet); - } + pm.RemoveStabled(pet); + pm.AutoStabled?.Remove(pet); } pet.MoveToWorld(from.Location, from.Map); diff --git a/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs index 396f9de26..abe9597ac 100644 --- a/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs +++ b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs @@ -18,6 +18,9 @@ public abstract partial class MonsterAbility public virtual TimeSpan MinTriggerCooldown => TimeSpan.Zero; public virtual TimeSpan MaxTriggerCooldown => TimeSpan.Zero; + // To prevent reflect from harming the monster + public virtual bool CanTriggerAgainstSelf => false; + public bool WillTrigger(MonsterAbilityTrigger trigger) => (AbilityTrigger & trigger) != 0; /// @@ -54,7 +57,8 @@ public abstract partial class MonsterAbility /// public virtual void Trigger(MonsterAbilityTrigger trigger, BaseCreature source, Mobile target) { - if (MinTriggerCooldown <= TimeSpan.Zero && MaxTriggerCooldown <= TimeSpan.Zero) + if (!CanTriggerAgainstSelf && target == source || + MinTriggerCooldown <= TimeSpan.Zero && MaxTriggerCooldown <= TimeSpan.Zero) { return; } diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 254f49043..d2c75f505 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -2082,23 +2082,9 @@ namespace Server.Mobiles OwnerAbandonTime = reader.ReadDateTime(); } - if (version >= 11) - { - m_HasGeneratedLoot = reader.ReadBool(); - } - else - { - m_HasGeneratedLoot = true; - } + m_HasGeneratedLoot = version < 11 || reader.ReadBool(); - if (version >= 12) - { - m_Paragon = reader.ReadBool(); - } - else - { - m_Paragon = false; - } + m_Paragon = version >= 12 && reader.ReadBool(); if (version >= 13 && reader.ReadBool()) { @@ -2275,30 +2261,17 @@ namespace Server.Mobiles { if (m_ControlMaster != null) { - m_ControlMaster.Followers -= ControlSlots; + m_ControlMaster.Followers -= Math.Min(ControlSlots, m_ControlMaster.Followers); if (m_ControlMaster is PlayerMobile mobile) { - mobile.AllFollowers.Remove(this); - if (mobile.AutoStabled.Contains(this)) - { - mobile.AutoStabled.Remove(this); - } + mobile.AllFollowers?.Remove(this); + mobile.AutoStabled?.Remove(this); } } else if (m_SummonMaster != null) { - m_SummonMaster.Followers -= ControlSlots; - (m_SummonMaster as PlayerMobile)?.AllFollowers.Remove(this); - } - - if (m_ControlMaster?.Followers < 0) - { - m_ControlMaster.Followers = 0; - } - - if (m_SummonMaster?.Followers < 0) - { - m_SummonMaster.Followers = 0; + m_SummonMaster.Followers -= Math.Min(ControlSlots, m_SummonMaster.Followers); + (m_SummonMaster as PlayerMobile)?.AllFollowers?.Remove(this); } } @@ -2307,17 +2280,17 @@ namespace Server.Mobiles if (m_ControlMaster != null) { m_ControlMaster.Followers += ControlSlots; - if (m_ControlMaster is PlayerMobile mobile) + if (m_ControlMaster is PlayerMobile pm) { - mobile.AllFollowers.Add(this); + pm.AddFollower(this); } } else if (m_SummonMaster != null) { m_SummonMaster.Followers += ControlSlots; - if (m_SummonMaster is PlayerMobile mobile) + if (m_SummonMaster is PlayerMobile pm) { - mobile.AllFollowers.Add(this); + pm.AddFollower(this); } } } @@ -2407,6 +2380,8 @@ namespace Server.Mobiles MLQuestSystem.HandleDeletion(this); } + UnsummonTimer.StopTimer(this); + base.OnAfterDelete(); } @@ -3440,6 +3415,8 @@ namespace Server.Mobiles } } + RemoveFollowers(); + base.OnDeath(c); if (DeleteCorpseOnDeath) @@ -3455,6 +3432,7 @@ namespace Server.Mobiles SummonMaster = null; ReceivedHonorContext?.Cancel(); + base.OnDelete(); m?.InvalidateProperties(); } @@ -3834,11 +3812,10 @@ namespace Server.Mobiles public static void TeleportPets(Mobile master, Point3D loc, Map map, bool onlyBonded = false) { - if (master is PlayerMobile pm) + if (master is PlayerMobile { AllFollowers: not null } pm) { - for (var i = 0; i < pm.AllFollowers.Count; i++) + foreach (var m in pm.AllFollowers) { - var m = pm.AllFollowers[i]; if (m.Map == master.Map && master.InRange(m, 3) && m is BaseCreature { Controlled: true, ControlOrder: OrderType.Guard or OrderType.Follow or OrderType.Come } pet && pet.ControlMaster == master && (!onlyBonded || pet.IsBonded)) diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index f1a23eb6c..7660a98d7 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -143,7 +143,7 @@ namespace Server.Mobiles private Dictionary m_AcquiredRecipes; - private List m_AllFollowers; + private HashSet _allFollowers; private int m_BeardModID = -1, m_BeardModHue; // TODO: Pool BuffInfo objects @@ -181,7 +181,6 @@ namespace Server.Mobiles private DateTime m_NextJustAward; - private int m_NextMovementTime; private int m_NextProtectionCheck = 10; private DateTime m_NextSmithBulkOrder; private DateTime m_NextTailorBulkOrder; @@ -200,8 +199,6 @@ namespace Server.Mobiles public PlayerMobile() { - AutoStabled = new List(); - VisibilityList = new List(); PermaFlags = new List(); RecentlyReported = new List(); @@ -384,11 +381,16 @@ namespace Server.Mobiles public List RecentlyReported { get; set; } - public List AutoStabled { get; private set; } + // WARNING - This can be null!! + public HashSet Stabled { get; private set; } + + // WARNING - This can be null!! + public HashSet AutoStabled { get; private set; } public bool NinjaWepCooldown { get; set; } - public List AllFollowers => m_AllFollowers ??= new List(); + // WARNING - This can be null!! + public HashSet AllFollowers => _allFollowers; public RankDefinition GuildRank { @@ -961,6 +963,18 @@ namespace Server.Mobiles { Timer.StartTimer(CheckPets); } + + var stableMigrations = StableMigrations; + if (stableMigrations?.Count > 0) + { + foreach (var (player, stabled) in stableMigrations) + { + if (player is PlayerMobile pm) + { + pm.Stabled = stabled; + } + } + } } private static void TargetedSkillUse(Mobile from, IEntity target, int skillId) @@ -1062,8 +1076,8 @@ namespace Server.Mobiles foreach (var m in World.Mobiles.Values) { if (m is PlayerMobile pm && - ((!pm.Mounted || pm.Mount is EtherealMount) && pm.AllFollowers.Count > pm.AutoStabled.Count || - pm.Mounted && pm.AllFollowers.Count > pm.AutoStabled.Count + 1)) + ((!pm.Mounted || pm.Mount is EtherealMount) && pm.AllFollowers?.Count > pm.AutoStabled?.Count || + pm.Mounted && pm.AllFollowers?.Count > (pm.AutoStabled?.Count ?? 0) + 1)) { pm.AutoStablePets(); /* autostable checks summons, et al: no need here */ } @@ -1723,7 +1737,7 @@ namespace Server.Mobiles } } - var speed = ComputeMovementSpeed(d); + // var speed = ComputeMovementSpeed(d); if (!Alive) { @@ -1731,17 +1745,8 @@ namespace Server.Mobiles } var res = base.Move(d); - MovementImpl.IgnoreMovableImpassables = false; - - if (!res) - { - return false; - } - - m_NextMovementTime += speed; - - return true; + return res; } public override bool CheckMovement(Direction d, out int newZ) @@ -2880,6 +2885,11 @@ namespace Server.Mobiles switch (version) { + case 30: + { + Stabled = reader.ReadEntitySet(true); + goto case 29; + } case 29: { if (reader.ReadBool()) @@ -2912,7 +2922,7 @@ namespace Server.Mobiles } case 26: { - AutoStabled = reader.ReadEntityList(); + AutoStabled = reader.ReadEntitySet(true); goto case 25; } @@ -3129,11 +3139,6 @@ namespace Server.Mobiles } case 0: { - if (version < 26) - { - AutoStabled = new List(); - } - break; } } @@ -3164,14 +3169,15 @@ namespace Server.Mobiles IgnoreMobiles = true; } - var list = Stabled; - - for (var i = 0; i < list.Count; ++i) + if (Stabled != null) { - if (list[i] is BaseCreature bc) + foreach (var stabled in Stabled) { - bc.IsStabled = true; - bc.StabledBy = this; + if (stabled is BaseCreature bc) + { + bc.IsStabled = true; + bc.StabledBy = this; + } } } @@ -3188,7 +3194,17 @@ namespace Server.Mobiles { base.Serialize(writer); - writer.Write(29); // version + writer.Write(30); // version + + if (Stabled == null) + { + writer.Write(0); + } + else + { + Stabled.Tidy(); + writer.Write(Stabled); + } if (m_StuckMenuUses != null) { @@ -3208,8 +3224,15 @@ namespace Server.Mobiles writer.Write(PeacedUntil); writer.Write(AnkhNextUse); - AutoStabled.Tidy(); - writer.Write(AutoStabled); + if (AutoStabled == null) + { + writer.Write(0); + } + else + { + AutoStabled.Tidy(); + writer.Write(AutoStabled); + } if (m_AcquiredRecipes == null) { @@ -3480,11 +3503,11 @@ namespace Server.Mobiles } } - if (Core.ML) + if (Core.ML && AllFollowers != null) { - for (var i = AllFollowers.Count - 1; i >= 0; i--) + foreach (var follower in AllFollowers) { - if (AllFollowers[i] is BaseCreature c && c.ControlOrder == OrderType.Guard) + if (follower is BaseCreature { ControlOrder: OrderType.Guard }) { list.Add(501129); // guarded break; @@ -3576,79 +3599,127 @@ namespace Server.Mobiles return true; } + public void AddFollower(Mobile m) + { + _allFollowers ??= new HashSet(); + _allFollowers.Add(m); + } + + public void AddStabled(Mobile m) + { + Stabled ??= new HashSet(); + Stabled.Add(m); + } + + public bool RemoveStabled(Mobile m) + { + if (Stabled?.Remove(m) == true) + { + if (Stabled.Count == 0) + { + Stabled = null; + } + + return true; + } + + return false; + } + + public bool RemoveFollower(Mobile m) + { + if (_allFollowers?.Remove(m) == true) + { + if (_allFollowers.Count == 0) + { + _allFollowers = null; + } + + return true; + } + + return false; + } + public void AutoStablePets() { - if (Core.SE && AllFollowers.Count > 0) + var allFollowers = _allFollowers; + + if (!Core.SE || !(allFollowers?.Count > 0)) { - for (var i = m_AllFollowers.Count - 1; i >= 0; --i) + return; + } + + foreach (var follower in allFollowers) + { + if (follower is not BaseCreature pet || pet.ControlMaster == null) { - if (AllFollowers[i] is not BaseCreature pet || pet.ControlMaster == null) - { - continue; - } - - if (pet.Summoned) - { - if (pet.Map != Map) - { - pet.PlaySound(pet.GetAngerSound()); - Timer.StartTimer(pet.Delete); - } - - continue; - } - - if ((pet as IMount)?.Rider != null) - { - continue; - } - - if (pet is PackLlama or PackHorse or Beetle && pet.Backpack?.Items.Count > 0) - { - continue; - } - - if (pet is BaseEscortable) - { - continue; - } - - pet.ControlTarget = null; - pet.ControlOrder = OrderType.Stay; - pet.Internalize(); - - pet.SetControlMaster(null); - pet.SummonMaster = null; - - pet.IsStabled = true; - pet.StabledBy = this; - - pet.Loyalty = BaseCreature.MaxLoyalty; // Wonderfully happy - - Stabled.Add(pet); - AutoStabled.Add(pet); + continue; } + + if (pet.Summoned) + { + if (pet.Map != Map) + { + pet.PlaySound(pet.GetAngerSound()); + Timer.StartTimer(pet.Delete); + } + + continue; + } + + if ((pet as IMount)?.Rider != null) + { + continue; + } + + if (pet is PackLlama or PackHorse or Beetle && pet.Backpack?.Items.Count > 0) + { + continue; + } + + if (pet is BaseEscortable) + { + continue; + } + + pet.ControlTarget = null; + pet.ControlOrder = OrderType.Stay; + pet.Internalize(); + + pet.SetControlMaster(null); + pet.SummonMaster = null; + + pet.IsStabled = true; + pet.StabledBy = this; + + pet.Loyalty = BaseCreature.MaxLoyalty; // Wonderfully happy + + Stabled ??= new HashSet(); + Stabled.Add(pet); + + AutoStabled ??= new HashSet(); + AutoStabled.Add(pet); } } public void ClaimAutoStabledPets() { - if (!Core.SE || AutoStabled.Count <= 0) + if (!Core.SE || !(AutoStabled?.Count > 0)) { return; } if (!Alive) { - SendLocalizedMessage( - 1076251 - ); // Your pet was unable to join you while you are a ghost. Please re-login once you have ressurected to claim your pets. + // Your pet was unable to join you while you are a ghost. Please re-login once you have ressurected to claim your pets. + SendLocalizedMessage(1076251); return; } - for (var i = AutoStabled.Count - 1; i >= 0; --i) + foreach (var stabled in AutoStabled) { - if (AutoStabled[i] is not BaseCreature pet) + if (stabled is not BaseCreature pet) { continue; } @@ -3658,11 +3729,7 @@ namespace Server.Mobiles pet.IsStabled = false; pet.StabledBy = null; - if (Stabled.Contains(pet)) - { - Stabled.Remove(pet); - } - + Stabled?.Remove(pet); continue; } @@ -3685,21 +3752,16 @@ namespace Server.Mobiles pet.Loyalty = BaseCreature.MaxLoyalty; // Wonderfully Happy - if (Stabled.Contains(pet)) - { - Stabled.Remove(pet); - } + Stabled?.Remove(pet); } else { - SendLocalizedMessage( - 1049612, - pet.Name - ); // ~1_NAME~ remained in the stables because you have too many followers. + // ~1_NAME~ remained in the stables because you have too many followers. + SendLocalizedMessage(1049612, pet.Name); } } - AutoStabled.Clear(); + AutoStabled = null; } public void RecoverAmmo() @@ -4144,6 +4206,16 @@ namespace Server.Mobiles { ReceivedHonorContext?.Cancel(); SentHonorContext?.Cancel(); + + if (Stabled != null) + { + foreach (var stabled in Stabled) + { + stabled.Delete(); + } + + Stabled = null; + } } public override int ComputeMovementSpeed(Direction dir, bool checkTurning = true) diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs b/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs index b9d61b49b..a276c2b6a 100644 --- a/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs +++ b/Projects/UOContent/Mobiles/Vendors/NPC/AnimalTrainer.cs @@ -1,5 +1,6 @@ using ModernUO.Serialization; using System.Collections.Generic; +using Server.Collections; using Server.ContextMenus; using Server.Gumps; using Server.Items; @@ -41,11 +42,11 @@ namespace Server.Mobiles public override void AddCustomContextEntries(Mobile from, List list) { - if (from.Alive) + if (from is PlayerMobile { Alive: true } pm) { list.Add(new StableEntry(this, from)); - if (from.Stabled.Count > 0) + if (pm.Stabled?.Count > 0) { list.Add(new ClaimAllEntry(this, from)); } @@ -105,31 +106,38 @@ namespace Server.Mobiles public void BeginClaimList(Mobile from) { - if (Deleted || !from.CheckAlive()) + if (Deleted || !from.CheckAlive() || from is not PlayerMobile pm) { return; } var list = new List(); - for (var i = 0; i < from.Stabled.Count; ++i) + if (pm.Stabled?.Count > 0) { - var pet = from.Stabled[i] as BaseCreature; + using var queue = PooledRefQueue.Create(); - if (pet?.Deleted != false) + foreach (var m in pm.Stabled) { - if (pet != null) + if (m is BaseCreature pet) { + if (!pet.Deleted) + { + list.Add(pet); + break; + } + pet.IsStabled = false; pet.StabledBy = null; } - from.Stabled.RemoveAt(i); - --i; - continue; + queue.Enqueue(m); } - list.Add(pet); + while (queue.Count > 0) + { + pm.RemoveStabled(queue.Dequeue()); + } } if (list.Count > 0) @@ -144,7 +152,7 @@ namespace Server.Mobiles public void EndClaimList(Mobile from, BaseCreature pet) { - if (pet?.Deleted != false || from.Map != Map || !from.Stabled.Contains(pet) || !from.CheckAlive()) + if (pet?.Deleted != false || from.Map != Map || from is not PlayerMobile pm || pm.Stabled?.Contains(pet) != true || !from.CheckAlive()) { return; } @@ -159,9 +167,8 @@ namespace Server.Mobiles { DoClaim(from, pet); - from.Stabled.Remove(pet); - - (from as PlayerMobile)?.AutoStabled.Remove(pet); + pm.RemoveStabled(pet); + pm.AutoStabled?.Remove(pet); } else { @@ -176,8 +183,6 @@ namespace Server.Mobiles return; } - Container bank = from.FindBankNoCreate(); - if (!(from.Backpack?.GetAmount(typeof(Gold)) >= 30) && !(Banker.GetBalance(from) >= 30)) { @@ -197,7 +202,7 @@ namespace Server.Mobiles public void EndStable(Mobile from, BaseCreature pet) { - if (Deleted || !from.CheckAlive()) + if (Deleted || !from.CheckAlive() || from is not PlayerMobile pm) { return; } @@ -236,14 +241,12 @@ namespace Server.Mobiles { SayTo(from, 1042564); // I'm sorry. Your pet seems to be busy. } - else if (from.Stabled.Count >= GetMaxStabled(from)) + else if (pm.Stabled?.Count >= GetMaxStabled(from)) { SayTo(from, 1042565); // You have too many pets in the stables! } else { - Container bank = from.FindBankNoCreate(); - if (from.Backpack?.ConsumeTotal(typeof(Gold), 30) == true || Banker.Withdraw(from, 30)) { pet.ControlTarget = null; @@ -261,14 +264,10 @@ namespace Server.Mobiles pet.Loyalty = MaxLoyalty; // Wonderfully happy } - from.Stabled.Add(pet); + pm.AddStabled(pet); - SayTo( - from, - Core.AOS - ? 1049677 - : 502679 - ); // [AOS: Your pet has been stabled.] Very well, thy pet is stabled. Thou mayst recover it by saying 'claim' to me. In one real world week, I shall sell it off if it is not claimed! + // [AOS: Your pet has been stabled.] Very well, thy pet is stabled. Thou mayst recover it by saying 'claim' to me. In one real world week, I shall sell it off if it is not claimed! + SayTo(from, Core.AOS ? 1049677 : 502679); } else { @@ -279,7 +278,7 @@ namespace Server.Mobiles public void Claim(Mobile from, string petName = null) { - if (Deleted || !from.CheckAlive()) + if (Deleted || !from.CheckAlive() || from is not PlayerMobile pm) { return; } @@ -289,44 +288,51 @@ namespace Server.Mobiles var claimByName = petName != null; - for (var i = 0; i < from.Stabled.Count; ++i) + if (pm.Stabled?.Count > 0) { - var pet = from.Stabled[i] as BaseCreature; + using var queue = PooledRefQueue.Create(); - if (pet?.Deleted != false) + foreach (var m in pm.Stabled) { - if (pet != null) + var pet = m as BaseCreature; + + if (pet?.Deleted != false) { - pet.IsStabled = false; - pet.StabledBy = null; + if (pet != null) + { + pet.IsStabled = false; + pet.StabledBy = null; + } + + queue.Enqueue(pet); + continue; + } + + ++stabled; + + if (claimByName && !pet.Name.InsensitiveEquals(petName)) + { + continue; + } + + if (CanClaim(from, pet)) + { + DoClaim(from, pet); + + queue.Enqueue(pet); + + claimed = true; + pm.AutoStabled?.Remove(pet); + } + else + { + SayTo(from, 1049612, pet.Name); // ~1_NAME~ remained in the stables because you have too many followers. } - from.Stabled.RemoveAt(i); - --i; - continue; } - ++stabled; - - if (claimByName && !pet.Name.InsensitiveEquals(petName)) + while (queue.Count > 0) { - continue; - } - - if (CanClaim(from, pet)) - { - DoClaim(from, pet); - - from.Stabled.RemoveAt(i); - - (from as PlayerMobile)?.AutoStabled.Remove(pet); - - --i; - - claimed = true; - } - else - { - SayTo(from, 1049612, pet.Name); // ~1_NAME~ remained in the stables because you have too many followers. + pm.RemoveStabled(queue.Dequeue()); } } diff --git a/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs b/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs index 13172b7ce..f31f9ba3c 100644 --- a/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs +++ b/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs @@ -239,9 +239,7 @@ namespace Server.Spells.Necromancy return; } - list.Remove(summoned); - - if (list.Count == 0) + if (list.Remove(summoned) && list.Count == 0) { _table.Remove(master); } @@ -278,10 +276,17 @@ namespace Server.Spells.Necromancy if (list.Count > 3) { - Timer.StartTimer(list[0].Kill); + var toKill = list[0]; + Unregister(master, toKill); + toKill.Kill(); } - Timer.StartTimer(TimeSpan.FromSeconds(2.0), TimeSpan.FromSeconds(2.0), () => Summoned_Damage(summoned)); + Timer.DelayCall( + TimeSpan.FromMilliseconds(1650), + TimeSpan.FromMilliseconds(1650), + Summoned_Damage, + summoned + ); } private static void Summoned_Damage(Mobile mob) diff --git a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs index 3096708c0..120980f87 100644 --- a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs +++ b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs @@ -161,7 +161,7 @@ namespace Server.Mobiles ControlOrder = OrderType.Follow; ControlTarget = caster; - var duration = TimeSpan.FromSeconds(30 + caster.Skills.Ninjitsu.Fixed / 40); + var duration = TimeSpan.FromSeconds(30.0 + caster.Skills.Ninjitsu.Value / 4.0); new UnsummonTimer(this, duration).Start(); SummonEnd = Core.Now + duration; diff --git a/Projects/UOContent/Spells/UnsummonTimer.cs b/Projects/UOContent/Spells/UnsummonTimer.cs index aad0c97e8..e34623a9f 100644 --- a/Projects/UOContent/Spells/UnsummonTimer.cs +++ b/Projects/UOContent/Spells/UnsummonTimer.cs @@ -1,23 +1,40 @@ using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; using Server.Mobiles; -namespace Server.Spells +namespace Server.Spells; + +public class UnsummonTimer : Timer { - internal class UnsummonTimer : Timer + // Track timers since some of them are really long and might hold references to long dead/deleted mobs + private static readonly Dictionary _timers = new(); + private BaseCreature _creature; + + public static void StopTimer(BaseCreature creature) { - private readonly BaseCreature m_Creature; - - public UnsummonTimer(BaseCreature creature, TimeSpan delay) : base(delay) + if (_timers.Remove(creature, out var timer)) { - m_Creature = creature; - } - - protected override void OnTick() - { - if (!m_Creature.Deleted) - { - m_Creature.Delete(); - } + timer.Stop(); } } + + public UnsummonTimer(BaseCreature creature, TimeSpan delay) : base(delay) + { + _creature = creature; + + ref var timer = ref CollectionsMarshal.GetValueRefOrAddDefault(_timers, creature, out bool exists); + if (exists) + { + timer.Stop(); + } + + timer = this; + } + + protected override void OnTick() + { + // BaseCreature.OnAfterDelete will remove the creature from the timers table + _creature?.Delete(); + } }