fix: Fixes DeltaQueue recursion, StatMod bug, and memory leak (#1233)

* Fixes recursion with DeltaQueue causing multiple sets of packets to be sent to the client.
* Fixes StatMods that are expired not being checked properly.
* Adds a timer to properly expire/remove stat mods instead of relying on a side-effect.
This commit is contained in:
Kamron Batman 2022-11-08 09:08:05 -08:00 committed by GitHub
parent d7d914df6c
commit 59dcd24bed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 61 additions and 26 deletions

View file

@ -8392,6 +8392,33 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
return false;
}
public virtual void RemoveStatMod(StatMod mod)
{
if (mod == null)
{
return;
}
// Remove it just in case it was orphaned somehow.
mod.Remove();
if (_statMods == null)
{
return;
}
if (_statMods.Remove(mod))
{
CheckStatTimers();
Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
}
if (_statMods.Count == 0)
{
_statMods = null;
}
}
public virtual void RemoveStatMod(string name)
{
if (_statMods == null || name == null)
@ -8405,19 +8432,19 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
if (mod.Name == name)
{
_statMods.RemoveAt(i);
mod.Remove();
CheckStatTimers();
Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
}
}
if (_statMods.Count == 0)
{
_statMods = null;
}
}
public virtual StatMod GetStatMod(string name)
public virtual StatMod GetStatMod(string name, bool includeElapsed = false)
{
if (_statMods == null || name == null)
{
@ -8427,7 +8454,7 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
for (var i = 0; i < _statMods.Count; i++)
{
var mod = _statMods[i];
if (mod.Name == name)
if (mod.Name == name && !mod.HasElapsed() || includeElapsed)
{
return mod;
}
@ -8471,41 +8498,26 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
}
/// <summary>
/// Computes the total modified offset for the specified stat type. Expired <see cref="StatMod" /> instances are removed.
/// Computes the total modified offset for the specified stat type.
/// </summary>
public int GetStatOffset(StatType type)
{
_statMods ??= new List<StatMod>();
if (_statMods.Count <= 0)
if (_statMods == null || _statMods.Count == 0)
{
return 0;
}
var offset = 0;
using var queue = PooledRefQueue<StatMod>.Create(8);
for (var i = 0; i < _statMods.Count; i++)
{
var mod = _statMods[i];
if (mod.HasElapsed())
{
queue.Enqueue(mod);
}
else if ((mod.Type & type) != 0)
if ((mod.Type & type) != 0 && !mod.HasElapsed())
{
offset += mod.Offset;
}
}
while (queue.Count > 0)
{
var mod = queue.Dequeue();
_statMods.Remove(mod);
Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
CheckStatTimers();
}
return offset;
}

View file

@ -33,6 +33,11 @@ public partial class StatMod : MobileMod
[SerializableField(3, setter: "private")]
private int _offset;
// Added a timer and removed the processing of expirations in GetStatOffset which caused recursions in DeltaQueue
// ProcessDeltaQueue -> Dequeue -> ProcessDelta -> SendStats packet -> get_HitsMax ->
// - GetStatOffset -> Delta -> Queue to DeltaQueue
private TimerExecutionToken _timerToken;
public StatMod(Mobile owner) : base(owner)
{
}
@ -43,7 +48,26 @@ public partial class StatMod : MobileMod
_offset = offset;
_duration = duration;
_added = Core.Now;
if (_duration > TimeSpan.Zero)
{
Timer.StartTimer(duration, RemoveFromOwner, out _timerToken);
}
}
public bool HasElapsed() => _duration != TimeSpan.Zero && Core.Now - _added >= _duration;
public bool HasElapsed() => _duration > TimeSpan.Zero && Core.Now - _added >= _duration;
[AfterDeserialization]
private void AfterDeserialization()
{
if (_duration > TimeSpan.Zero && Core.Now - _added < _duration)
{
Timer.StartTimer(_duration, RemoveFromOwner, out _timerToken);
}
}
private void RemoveFromOwner() => Owner?.RemoveStatMod(this);
// Called by Mobile.RemoveStatMod()
public void Remove() => _timerToken.Cancel();
}

View file

@ -301,20 +301,19 @@ namespace Server.Spells
public static bool AddStatBonus(Mobile caster, Mobile target, StatType type, int bonus, TimeSpan duration)
{
var offset = bonus;
var name = $"[Magic] {type} Buff";
var mod = target.GetStatMod(name);
if (mod?.Offset < 0)
{
target.AddStatMod(new StatMod(type, name, mod.Offset + offset, duration));
target.AddStatMod(new StatMod(type, name, mod.Offset + bonus, duration));
return true;
}
if (mod == null || mod.Offset <= offset)
if (mod == null || mod.Offset <= bonus)
{
target.AddStatMod(new StatMod(type, name, offset, duration));
target.AddStatMod(new StatMod(type, name, bonus, duration));
return true;
}