perf: stop allocating stat/skill mod lists for every mobile (#2604)

## Summary

`_statMods` and `_skillMods` are created lazily by `AddStatMod` / `AddSkillMod` and nulled when they empty, and every reader already null-checks. The eager `new List<T>()` in `DefaultMobileInit` and `Deserialize` therefore allocated two dead 32-byte objects for every mobile. On a ~500k-mobile world that is ~32 MB and 1M gen2 objects that hold nothing.

- Removes the four eager allocations.
- Removes the `StatMods` accessor (no references).
- Documents `SkillMods` as `null` when no mods are active (its one caller in `Skills.cs` already checks).

First of three PRs from the lazy per-mobile collections design; `DamageEntries` and `Aggressors`/`Aggressed` follow separately.

## Breaking change

- `Mobile.SkillMods` may now be `null` (it was never null after construction before). External callers that enumerate it or read `.Count` must null-check.
- `Mobile.StatMods` is removed. Use `GetStatMod(name)` / `AddStatMod` / `RemoveStatMod`.

Save format is untouched: neither list is serialized.

## Testing

- `dotnet build -c Release` clean.
- New `MobileLazyModListTests` plus full `Server.Tests` (840) and `UOContent.Tests` (756).
This commit is contained in:
Kamron Batman 2026-09-01 23:23:32 -07:00 committed by GitHub
parent d3bf283e2d
commit 708a354337
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -6478,9 +6478,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
m_DexLock = (StatLockType)reader.ReadByte();
m_IntLock = (StatLockType)reader.ReadByte();
_statMods = new List<StatMod>();
_skillMods = new List<SkillMod>();
if (version < 32)
{
if (reader.ReadBool())
@ -7813,8 +7810,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
m_FollowersMax = 5;
Skills = new Skills(this);
Items = new List<Item>();
_statMods = new List<StatMod>();
_skillMods = new List<SkillMod>();
Map = Map.Internal;
AutoPageNotify = true;
Aggressors = new List<AggressorInfo>();