From f2f8313b42c72f580cca4d9fb99961dbc0ee3493 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:40:03 -0700 Subject: [PATCH] perf: cache the resolved speed entry per creature Serialization consults the speed table four times per mob per save (the Should* flag checks) and again through the Default* methods on elided loads - each a SpeedClass/type dictionary walk. The resolved SpeedClassEntry is now cached on the creature (one reference; the table is immutable after Configure), so those become a null-check and field reads. GetSpeeds/GetMoveSpeeds stay the virtual override point, so stubs and forks that override them still steer elision; only their default implementations read the cache. NPCSpeeds' per-call lookups collapse into FindEntry. Co-Authored-By: Claude Fable 5 --- Projects/UOContent/Mobiles/BaseCreature.cs | 19 ++++++++++++++-- Projects/UOContent/Mobiles/NPCSpeeds.cs | 25 +++++----------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 0713fd531..efe0eb646 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -5078,14 +5078,29 @@ namespace Server.Mobiles // If this needs to be serialized, recommend creating a hash or registry id. Don't serialize strings. public virtual SpeedLevel SpeedClass => SpeedLevel.None; + // Resolved once per creature; serialization consults the table four times per mob + // per save (and again on elided loads), so the dictionary walk must not repeat. + private NPCSpeeds.SpeedClassEntry _speedEntry; + + private NPCSpeeds.SpeedClassEntry SpeedEntry => _speedEntry ??= NPCSpeeds.FindEntry(this); + public virtual void GetSpeeds(out double activeSpeed, out double passiveSpeed) { - NPCSpeeds.GetSpeeds(this, out activeSpeed, out passiveSpeed); + var entry = SpeedEntry ?? throw new InvalidOperationException( + $"{GetType()} has no speed entry - is {"Data/npc-speeds.json"} missing?" + ); + + activeSpeed = entry.ActiveSpeed; + passiveSpeed = entry.PassiveSpeed; } + // Move speeds are optional (0 = inherit), so this tolerates an unloaded table. public virtual void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed) { - NPCSpeeds.GetMoveSpeeds(this, out activeMoveSpeed, out passiveMoveSpeed); + var entry = SpeedEntry; + + activeMoveSpeed = entry?.ActiveMoveSpeed ?? 0; + passiveMoveSpeed = entry?.PassiveMoveSpeed ?? 0; } // Pre-v22 saves carry no movement clock. Think speeds matching today's GetSpeeds diff --git a/Projects/UOContent/Mobiles/NPCSpeeds.cs b/Projects/UOContent/Mobiles/NPCSpeeds.cs index 8f5e908bb..9a8785b5e 100644 --- a/Projects/UOContent/Mobiles/NPCSpeeds.cs +++ b/Projects/UOContent/Mobiles/NPCSpeeds.cs @@ -26,32 +26,17 @@ public static class NPCSpeeds public static int MinIdleSeconds { get; private set; } public static int MaxIdleSeconds { get; private set; } - public static void GetSpeeds(BaseCreature bc, out double activeSpeed, out double passiveSpeed) + // Null when the table is unloaded (test fixtures). Creatures cache the result — the + // table is immutable after Configure. + public static SpeedClassEntry FindEntry(BaseCreature bc) { if ((bc.SpeedClass == SpeedLevel.None || !_speedsByLevel.TryGetValue(bc.SpeedClass, out var sp)) && !_speedsByType.TryGetValue(bc.GetType(), out sp)) { - sp = _speedsByLevel[SpeedLevel.Medium]; + _speedsByLevel.TryGetValue(SpeedLevel.Medium, out sp); } - activeSpeed = sp.ActiveSpeed; - passiveSpeed = sp.PassiveSpeed; - } - - // Move speeds are optional (0 = inherit), so this tolerates a missing entry or table. - public static void GetMoveSpeeds(BaseCreature bc, out double activeMoveSpeed, out double passiveMoveSpeed) - { - if ((bc.SpeedClass == SpeedLevel.None || !_speedsByLevel.TryGetValue(bc.SpeedClass, out var sp)) && - !_speedsByType.TryGetValue(bc.GetType(), out sp) && - !_speedsByLevel.TryGetValue(SpeedLevel.Medium, out sp)) - { - activeMoveSpeed = 0; - passiveMoveSpeed = 0; - return; - } - - activeMoveSpeed = sp.ActiveMoveSpeed; - passiveMoveSpeed = sp.PassiveMoveSpeed; + return sp; } public static void RegisterSpeed(SpeedClassEntry entry)