ModernUO/Projects/UOContent/Mobiles/NPCSpeeds.cs
Kamron Batman cdcf82cfd8
refactor: None means custom; the type list resolves only at construction
The None -> type list -> Medium chain was construction-time defaulting
(so creatures without a bucket or a SetSpeed call never spawn at 0/0),
not a live semantic. The constructor now resolves it once into
_speedClass itself, so at runtime a concrete bucket means table-backed
and None means the creature's own speeds are authoritative - which is
what SpeedLevel.Custom was; it is removed. Runtime entry resolution
collapses to a single level lookup, and the SpeedClass byte still
elides by comparing against the (cached) resolved type default.

Legacy loads guess the type default and demote to None when the loaded
speeds do not conform, so pre-codegen customized creatures (SetSpeed
vendors) come out honestly labeled. A constructor guard keeps a missing
speed table loud instead of spawning 0-delay creatures that spin their
AI timers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 13:13:46 -07:00

102 lines
3.1 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
using Server.Json;
namespace Server.Mobiles;
public enum SpeedLevel
{
None, // no bucket: the creature's own speeds are authoritative (custom)
VerySlow,
Slow,
Medium,
Fast,
VeryFast
}
public static class NPCSpeeds
{
private const string _tablePath = "Data/npc-speeds.json";
private static readonly Dictionary<Type, SpeedClassEntry> _speedsByType = new();
private static readonly Dictionary<SpeedLevel, SpeedClassEntry> _speedsByLevel = new();
// Time period to lock NPCs into idling
public static int MinIdleSeconds { get; private set; }
public static int MaxIdleSeconds { get; private set; }
// Construction-time resolution of a type's bucket: an explicit DefaultSpeedClass,
// else the table's type list, else Medium so unconfigured creatures never construct
// at 0/0. None only when the table itself is unloaded (test fixtures).
public static SpeedLevel ResolveDefaultLevel(BaseCreature bc)
{
if (bc.DefaultSpeedClass != SpeedLevel.None)
{
return bc.DefaultSpeedClass;
}
if (_speedsByType.TryGetValue(bc.GetType(), out var sp))
{
return sp.Level;
}
return _speedsByLevel.ContainsKey(SpeedLevel.Medium) ? SpeedLevel.Medium : SpeedLevel.None;
}
// Null for None (custom) or an unloaded table. Creatures cache the result — the
// table is immutable after Configure.
public static SpeedClassEntry FindEntry(SpeedLevel level) =>
level == SpeedLevel.None ? null : _speedsByLevel.GetValueOrDefault(level);
public static void RegisterSpeed(SpeedClassEntry entry)
{
_speedsByLevel[entry.Level] = entry;
foreach (var type in entry.Types)
{
_speedsByType[type] = entry;
}
}
public static void Configure()
{
MinIdleSeconds = ServerConfiguration.GetSetting("movement.delay.npcMinIdle", 15);
MaxIdleSeconds = ServerConfiguration.GetSetting("movement.delay.npcMaxIdle", 25);
var path = Path.Combine(Core.BaseDirectory, _tablePath);
if (!File.Exists(path))
{
return;
}
var speeds = JsonConfig.Deserialize<SpeedClassEntry[]>(path);
for (var i = 0; i < speeds.Length; i++)
{
RegisterSpeed(speeds[i]);
}
}
public record SpeedClassEntry
{
[JsonPropertyName("level")]
public SpeedLevel Level { get; init; }
[JsonPropertyName("active")]
public double ActiveSpeed { get; init; }
[JsonPropertyName("passive")]
public double PassiveSpeed { get; init; }
// Movement clock (seconds per step); absent/0 = inherit the matching think value.
[JsonPropertyName("activeMove")]
public double ActiveMoveSpeed { get; init; }
[JsonPropertyName("passiveMove")]
public double PassiveMoveSpeed { get; init; }
[JsonPropertyName("types")]
public HashSet<Type> Types { get; init; }
}
}