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.
This commit is contained in:
Kamron Batman 2023-07-03 09:45:22 -07:00 committed by GitHub
parent d99e72db8f
commit 3b97e8d39c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 391 additions and 280 deletions

View file

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Server;
public partial class Mobile
{
// Migrating Stabled property to PlayerMobile
public static Dictionary<Mobile, HashSet<Mobile>> StableMigrations { get; private set; }
public static void AddToStabledMigration(Mobile m, HashSet<Mobile> stabled)
{
if (stabled?.Count > 0)
{
StableMigrations ??= new Dictionary<Mobile, HashSet<Mobile>>();
StableMigrations[m] = stabled;
}
}
}

View file

@ -180,7 +180,7 @@ public delegate int AOSStatusHandler(Mobile from, int index);
/// <summary>
/// Base class representing players, npcs, and creatures.
/// </summary>
public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyListEntity
public partial class Mobile : IHued, IComparable<Mobile>, 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<Mobile>, ISpawnable, IObjectPropertyLis
}
}
public List<Mobile> Stabled { get; private set; }
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public VirtueInfo Virtues { get; private set; }
@ -2279,7 +2277,7 @@ public class Mobile : IHued, IComparable<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, ISpawnable, IObjectPropertyLis
case 22: // Just removed followers
case 21:
{
Stabled = reader.ReadEntityList<Mobile>();
if (version < 34)
{
// Migrated to PlayerMobile
AddToStabledMigration(this, reader.ReadEntitySet<Mobile>(true));
}
goto case 20;
}
@ -6285,11 +6294,6 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
}
case 0:
{
if (version < 21)
{
Stabled = new List<Mobile>();
}
if (version < 18)
{
Virtues = new VirtueInfo();
@ -7721,7 +7725,6 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
Aggressors = new List<AggressorInfo>();
Aggressed = new List<AggressorInfo>();
Virtues = new VirtueInfo();
Stabled = new List<Mobile>();
DamageEntries = new List<DamageEntry>();
NextSkillTime = Core.TickCount;

View file

@ -48,10 +48,18 @@ public static class SerializationExtensions
return entity?.Created <= reader.LastSerialized ? entity : null;
}
public static List<T> ReadEntityList<T>(this IGenericReader reader) where T : class, ISerializable
public static List<T> ReadEntityList<T>(
this IGenericReader reader,
bool nullIfEmpty = false
) where T : class, ISerializable
{
var count = reader.ReadInt();
if (count == 0 && nullIfEmpty)
{
return null;
}
var list = new List<T>(count);
for (var i = 0; i < count; ++i)
@ -66,10 +74,18 @@ public static class SerializationExtensions
return list;
}
public static HashSet<T> ReadEntitySet<T>(this IGenericReader reader) where T : class, ISerializable
public static HashSet<T> ReadEntitySet<T>(
this IGenericReader reader,
bool nullIfEmpty = false
) where T : class, ISerializable
{
var count = reader.ReadInt();
if (count == 0 && nullIfEmpty)
{
return null;
}
var set = new HashSet<T>(count);
for (var i = 0; i < count; ++i)

View file

@ -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)

View file

@ -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;

View file

@ -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);

View file

@ -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;
/// <summary>
@ -54,7 +57,8 @@ public abstract partial class MonsterAbility
/// </summary>
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;
}

View file

@ -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))

View file

@ -143,7 +143,7 @@ namespace Server.Mobiles
private Dictionary<int, bool> m_AcquiredRecipes;
private List<Mobile> m_AllFollowers;
private HashSet<Mobile> _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<Mobile>();
VisibilityList = new List<Mobile>();
PermaFlags = new List<Mobile>();
RecentlyReported = new List<Mobile>();
@ -384,11 +381,16 @@ namespace Server.Mobiles
public List<Mobile> RecentlyReported { get; set; }
public List<Mobile> AutoStabled { get; private set; }
// WARNING - This can be null!!
public HashSet<Mobile> Stabled { get; private set; }
// WARNING - This can be null!!
public HashSet<Mobile> AutoStabled { get; private set; }
public bool NinjaWepCooldown { get; set; }
public List<Mobile> AllFollowers => m_AllFollowers ??= new List<Mobile>();
// WARNING - This can be null!!
public HashSet<Mobile> 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<Mobile>(true);
goto case 29;
}
case 29:
{
if (reader.ReadBool())
@ -2912,7 +2922,7 @@ namespace Server.Mobiles
}
case 26:
{
AutoStabled = reader.ReadEntityList<Mobile>();
AutoStabled = reader.ReadEntitySet<Mobile>(true);
goto case 25;
}
@ -3129,11 +3139,6 @@ namespace Server.Mobiles
}
case 0:
{
if (version < 26)
{
AutoStabled = new List<Mobile>();
}
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<Mobile>();
_allFollowers.Add(m);
}
public void AddStabled(Mobile m)
{
Stabled ??= new HashSet<Mobile>();
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<Mobile>();
Stabled.Add(pet);
AutoStabled ??= new HashSet<Mobile>();
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)

View file

@ -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<ContextMenuEntry> 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<BaseCreature>();
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<Mobile>.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<Mobile>.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());
}
}

View file

@ -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)

View file

@ -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;

View file

@ -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<BaseCreature, UnsummonTimer> _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();
}
}