using System.Collections.Concurrent; namespace NeonSprawl.Server.Game.Npc; /// Thread-safe in-memory aggro holders for prototype NPC instances (NEO-92). public sealed class InMemoryThreatStateStore : IThreatStateStore { private readonly ConcurrentDictionary holderByNpcId = new(StringComparer.Ordinal); private readonly ConcurrentDictionary idLocks = new(StringComparer.Ordinal); /// public bool TryGet(string? npcInstanceId, out ThreatStateSnapshot snapshot) { var key = NormalizeNpcInstanceId(npcInstanceId); if (key.Length == 0 || !PrototypeNpcRegistry.TryGet(key, out _)) { snapshot = default; return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { if (!holderByNpcId.TryGetValue(key, out var holder)) { holder = null; holderByNpcId[key] = holder; } snapshot = new ThreatStateSnapshot(key, holder); return true; } } /// public bool TrySetHolder(string? npcInstanceId, string? playerIdLowercaseOrNull) { var key = NormalizeNpcInstanceId(npcInstanceId); if (key.Length == 0 || !PrototypeNpcRegistry.TryGet(key, out _)) { return false; } var normalizedHolder = NormalizePlayerId(playerIdLowercaseOrNull); lock (idLocks.GetOrAdd(key, _ => new object())) { holderByNpcId[key] = normalizedHolder; return true; } } /// public bool TryClearHolder(string? npcInstanceId) => TrySetHolder(npcInstanceId, null); private static string NormalizeNpcInstanceId(string? npcInstanceId) => npcInstanceId?.Trim().ToLowerInvariant() ?? string.Empty; private static string? NormalizePlayerId(string? playerId) { if (string.IsNullOrWhiteSpace(playerId)) { return null; } return playerId.Trim().ToLowerInvariant(); } }