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:
parent
d99e72db8f
commit
3b97e8d39c
13 changed files with 391 additions and 280 deletions
18
Projects/Server/Mobiles/Mobile.Migrations.cs
Normal file
18
Projects/Server/Mobiles/Mobile.Migrations.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue