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 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-23 12:40:03 -07:00
parent f46780d554
commit f2f8313b42
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
2 changed files with 22 additions and 22 deletions

View file

@ -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

View file

@ -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)