perf: keep damage entries in an inline intrusive list (#2605)
## Summary `Mobile.DamageEntries` was a `List<DamageEntry>` allocated for every mobile, including the ~99% that never take damage. It is now an inline `ValueLinkList<DamageEntry>` (24 bytes in the `Mobile` object, no separate allocation) ordered least recent → most recent. - `DamageEntry` implements `IValueLinkListNode<DamageEntry>`. - `RegisterDamage` moves the entry to the tail in O(1) instead of `Remove` + `Add` on a list. - Expired entries are always a head prefix, so pruning walks from the head and stops at the first live entry. The `DamageEntries` getter prunes on access. - `DamageEntries` is exposed as `ref readonly`; enumerate with `foreach` or `.ByDescending()`. Mutation goes through `RegisterDamage` / `ClearDamageEntries`. - `BaseCreature.GetLootingRights` and `BaseCreature.ComputeBonusDamage` take `in ValueLinkList<DamageEntry>`; all callers compile unchanged. Files that `foreach` over `DamageEntries` need `using Server.Collections;` for the enumerator extension. - RunUO migration docs (`dev-docs/runuo-migration-docs/09`, `11`) and the `migrate-items-mobiles` skill document the change. Saves one object and 16 bytes per mobile (~8 MB and 500k gen2 objects on a 500k world). Second of three PRs from the lazy per-mobile collections design (first: #2604). Branched from `main`; the two diffs touch disjoint hunks of `Mobile.cs`. ## Breaking change - `Mobile.DamageEntries` is no longer a `List<DamageEntry>`. Indexing, `.Clear()`, `.Add()`, `.Remove()` no longer compile; use `foreach`, `.ByDescending()`, `.Count`, `ClearDamageEntries()`, and `RegisterDamage`. Calling a `ValueLinkList` mutator on the `ref readonly` property compiles but operates on a copy while still unlinking the real nodes; do not. - `BaseCreature.GetLootingRights` and `BaseCreature.ComputeBonusDamage` signatures changed to `(in ValueLinkList<DamageEntry>, …)`. Save format is untouched: damage entries are not serialized. ## Behavior Recency order, `allowSelf`, tie-breaking in `FindMostTotal`/`FindLeastTotal` (most recent wins), `Responsible` accounting, and loot-rights ordering are unchanged and covered by the new `DamageEntryTests` and `LootingRightsTests`. ## Testing - `dotnet build -c Release` clean. - New `DamageEntryTests` and `LootingRightsTests` plus full `Server.Tests` and `UOContent.Tests`.
This commit is contained in:
parent
708a354337
commit
e52d54b7da
9 changed files with 603 additions and 103 deletions
|
|
@ -42,7 +42,7 @@ public delegate void PromptCallback(Mobile from, string text);
|
|||
|
||||
public delegate void PromptStateCallback<in T>(Mobile from, string text, T state);
|
||||
|
||||
public class DamageEntry
|
||||
public class DamageEntry : IValueLinkListNode<DamageEntry>
|
||||
{
|
||||
public DamageEntry(Mobile damager) => Damager = damager;
|
||||
|
||||
|
|
@ -57,6 +57,11 @@ public class DamageEntry
|
|||
public List<DamageEntry> Responsible { get; set; }
|
||||
|
||||
public static TimeSpan ExpireDelay { get; set; } = TimeSpan.FromMinutes(2.0);
|
||||
|
||||
// Intrusive links for Mobile._damageEntries. Sub-entries in Responsible never join a list.
|
||||
public DamageEntry Next { get; set; }
|
||||
public DamageEntry Previous { get; set; }
|
||||
public bool OnLinkList { get; set; }
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
|
@ -377,7 +382,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
Aggressors = new List<AggressorInfo>();
|
||||
Aggressed = new List<AggressorInfo>();
|
||||
NextSkillTime = Core.TickCount;
|
||||
DamageEntries = new List<DamageEntry>();
|
||||
}
|
||||
|
||||
// Sectors
|
||||
|
|
@ -958,7 +962,23 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
public static VisibleDamageType VisibleDamageType { get; set; }
|
||||
|
||||
public List<DamageEntry> DamageEntries { get; private set; }
|
||||
private ValueLinkList<DamageEntry> _damageEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Damage entries ordered least recent (head) to most recent (tail). Expired entries are
|
||||
/// pruned on access. Enumerate with <c>foreach</c> (ascending) or <c>.ByDescending()</c>.
|
||||
/// Mutate only through <see cref="RegisterDamage"/> and <see cref="ClearDamageEntries"/>.
|
||||
/// Calling a ValueLinkList mutator on this reference compiles, but operates on a defensive copy
|
||||
/// while still unlinking the real nodes — it silently corrupts the list.
|
||||
/// </summary>
|
||||
public ref readonly ValueLinkList<DamageEntry> DamageEntries
|
||||
{
|
||||
get
|
||||
{
|
||||
PruneExpiredDamageEntries();
|
||||
return ref _damageEntries;
|
||||
}
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public Mobile LastKiller { get; set; }
|
||||
|
|
@ -2020,10 +2040,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
Aggressors[i].CanReportMurder = false;
|
||||
}
|
||||
|
||||
if (DamageEntries.Count > 0)
|
||||
{
|
||||
DamageEntries.Clear(); // reset damage entries on full HP
|
||||
}
|
||||
ClearDamageEntries(); // reset damage entries on full HP
|
||||
}
|
||||
else if (CanRegenHits)
|
||||
{
|
||||
|
|
@ -5745,24 +5762,54 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
}
|
||||
}
|
||||
|
||||
// Entries are kept in LastDamage order, so expired entries are always a head prefix.
|
||||
private void PruneExpiredDamageEntries()
|
||||
{
|
||||
#if DEBUG
|
||||
for (var node = _damageEntries._first; node != null; node = node.Next)
|
||||
{
|
||||
Debug.Assert(
|
||||
node.Next == null || node.Next.LastDamage >= node.LastDamage,
|
||||
"Damage entries must be ordered by LastDamage ascending."
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
var first = _damageEntries._first;
|
||||
|
||||
if (first?.HasExpired != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var firstLive = first.Next;
|
||||
|
||||
while (firstLive?.HasExpired == true)
|
||||
{
|
||||
firstLive = firstLive.Next;
|
||||
}
|
||||
|
||||
if (firstLive == null)
|
||||
{
|
||||
_damageEntries.RemoveAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
_damageEntries.RemoveAllBefore(firstLive);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearDamageEntries() => _damageEntries.RemoveAll();
|
||||
|
||||
public Mobile FindMostRecentDamager(bool allowSelf) => FindMostRecentDamageEntry(allowSelf)?.Damager;
|
||||
|
||||
public DamageEntry FindMostRecentDamageEntry(bool allowSelf)
|
||||
{
|
||||
for (var i = DamageEntries.Count - 1; i >= 0; --i)
|
||||
PruneExpiredDamageEntries();
|
||||
|
||||
for (var de = _damageEntries._last; de != null; de = de.Previous)
|
||||
{
|
||||
if (i >= DamageEntries.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var de = DamageEntries[i];
|
||||
|
||||
if (de.HasExpired)
|
||||
{
|
||||
DamageEntries.RemoveAt(i);
|
||||
}
|
||||
else if (allowSelf || de.Damager != this)
|
||||
if (allowSelf || de.Damager != this)
|
||||
{
|
||||
return de;
|
||||
}
|
||||
|
|
@ -5775,21 +5822,11 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
public DamageEntry FindLeastRecentDamageEntry(bool allowSelf)
|
||||
{
|
||||
for (var i = 0; i < DamageEntries.Count; ++i)
|
||||
PruneExpiredDamageEntries();
|
||||
|
||||
for (var de = _damageEntries._first; de != null; de = de.Next)
|
||||
{
|
||||
if (i < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var de = DamageEntries[i];
|
||||
|
||||
if (de.HasExpired)
|
||||
{
|
||||
DamageEntries.RemoveAt(i);
|
||||
--i;
|
||||
}
|
||||
else if (allowSelf || de.Damager != this)
|
||||
if (allowSelf || de.Damager != this)
|
||||
{
|
||||
return de;
|
||||
}
|
||||
|
|
@ -5800,24 +5837,17 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
public Mobile FindMostTotalDamager(bool allowSelf) => FindMostTotalDamageEntry(allowSelf)?.Damager;
|
||||
|
||||
// Walks most recent first with a strict comparison so the most recent entry wins ties,
|
||||
// matching the previous reverse-indexed loop.
|
||||
public DamageEntry FindMostTotalDamageEntry(bool allowSelf)
|
||||
{
|
||||
PruneExpiredDamageEntries();
|
||||
|
||||
DamageEntry mostTotal = null;
|
||||
|
||||
for (var i = DamageEntries.Count - 1; i >= 0; --i)
|
||||
for (var de = _damageEntries._last; de != null; de = de.Previous)
|
||||
{
|
||||
if (i >= DamageEntries.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var de = DamageEntries[i];
|
||||
|
||||
if (de.HasExpired)
|
||||
{
|
||||
DamageEntries.RemoveAt(i);
|
||||
}
|
||||
else if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven > mostTotal.DamageGiven))
|
||||
if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven > mostTotal.DamageGiven))
|
||||
{
|
||||
mostTotal = de;
|
||||
}
|
||||
|
|
@ -5830,46 +5860,28 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
public DamageEntry FindLeastTotalDamageEntry(bool allowSelf)
|
||||
{
|
||||
DamageEntry mostTotal = null;
|
||||
PruneExpiredDamageEntries();
|
||||
|
||||
for (var i = DamageEntries.Count - 1; i >= 0; --i)
|
||||
DamageEntry leastTotal = null;
|
||||
|
||||
for (var de = _damageEntries._last; de != null; de = de.Previous)
|
||||
{
|
||||
if (i >= DamageEntries.Count)
|
||||
if ((allowSelf || de.Damager != this) && (leastTotal == null || de.DamageGiven < leastTotal.DamageGiven))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var de = DamageEntries[i];
|
||||
|
||||
if (de.HasExpired)
|
||||
{
|
||||
DamageEntries.RemoveAt(i);
|
||||
}
|
||||
else if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven < mostTotal.DamageGiven))
|
||||
{
|
||||
mostTotal = de;
|
||||
leastTotal = de;
|
||||
}
|
||||
}
|
||||
|
||||
return mostTotal;
|
||||
return leastTotal;
|
||||
}
|
||||
|
||||
public DamageEntry FindDamageEntryFor(Mobile m)
|
||||
{
|
||||
for (var i = DamageEntries.Count - 1; i >= 0; --i)
|
||||
PruneExpiredDamageEntries();
|
||||
|
||||
for (var de = _damageEntries._last; de != null; de = de.Previous)
|
||||
{
|
||||
if (i >= DamageEntries.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var de = DamageEntries[i];
|
||||
|
||||
if (de.HasExpired)
|
||||
{
|
||||
DamageEntries.RemoveAt(i);
|
||||
}
|
||||
else if (de.Damager == m)
|
||||
if (de.Damager == m)
|
||||
{
|
||||
return de;
|
||||
}
|
||||
|
|
@ -5887,8 +5899,13 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
de.DamageGiven += amount;
|
||||
de.LastDamage = Core.Now;
|
||||
|
||||
DamageEntries.Remove(de);
|
||||
DamageEntries.Add(de);
|
||||
// Move to the tail so the list stays in LastDamage order.
|
||||
if (de.OnLinkList)
|
||||
{
|
||||
_damageEntries.Remove(de);
|
||||
}
|
||||
|
||||
_damageEntries.AddLast(de);
|
||||
|
||||
var master = from.GetDamageMaster(this);
|
||||
|
||||
|
|
@ -7814,7 +7831,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
AutoPageNotify = true;
|
||||
Aggressors = new List<AggressorInfo>();
|
||||
Aggressed = new List<AggressorInfo>();
|
||||
DamageEntries = new List<DamageEntry>();
|
||||
|
||||
NextSkillTime = Core.TickCount;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue