using System.Collections.Concurrent; namespace NeonSprawl.Server.Game.Npc; /// Thread-safe in-memory NPC runtime state for prototype instances (NEO-93). public sealed class InMemoryNpcRuntimeStateStore : INpcRuntimeStateStore { private readonly ConcurrentDictionary rowsByNpcId = new(StringComparer.Ordinal); private readonly ConcurrentDictionary idLocks = new(StringComparer.Ordinal); /// public DateTimeOffset LastAdvancedUtc { get; set; } = DateTimeOffset.MinValue; /// public bool TryGet(string? npcInstanceId, out NpcRuntimeStateSnapshot snapshot) { var key = NormalizeNpcInstanceId(npcInstanceId); if (key.Length == 0 || !PrototypeNpcRegistry.TryGet(key, out _)) { snapshot = default; return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { var row = rowsByNpcId.GetOrAdd(key, static id => NpcRuntimeRow.CreateIdle(id)); snapshot = row.ToSnapshot(); return true; } } /// public bool TryWrite(string? npcInstanceId, in NpcRuntimeStateSnapshot snapshot) { var key = NormalizeNpcInstanceId(npcInstanceId); if (key.Length == 0 || !PrototypeNpcRegistry.TryGet(key, out _)) { return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { rowsByNpcId[key] = NpcRuntimeRow.FromSnapshot(snapshot with { NpcInstanceId = key }); return true; } } /// public bool TryResetPrototypeRow(string? npcInstanceId) { var key = NormalizeNpcInstanceId(npcInstanceId); if (key.Length == 0 || !PrototypeNpcRegistry.TryGet(key, out _)) { return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { rowsByNpcId[key] = NpcRuntimeRow.CreateIdle(key); return true; } } /// public void ResetAllPrototypeRows() { foreach (var npcInstanceId in PrototypeNpcRegistry.GetInstanceIdsInOrder()) { _ = TryResetPrototypeRow(npcInstanceId); } LastAdvancedUtc = DateTimeOffset.MinValue; } private static string NormalizeNpcInstanceId(string? npcInstanceId) => npcInstanceId?.Trim().ToLowerInvariant() ?? string.Empty; private sealed class NpcRuntimeRow { public required string NpcInstanceId { get; init; } public NpcBehaviorState BehaviorState { get; init; } public DateTimeOffset PhaseStartedUtc { get; init; } public ActiveTelegraphSnapshot? ActiveTelegraph { get; init; } public static NpcRuntimeRow CreateIdle(string npcInstanceId) => new() { NpcInstanceId = npcInstanceId, BehaviorState = NpcBehaviorState.Idle, PhaseStartedUtc = DateTimeOffset.MinValue, ActiveTelegraph = null, }; public NpcRuntimeStateSnapshot ToSnapshot() => new(NpcInstanceId, BehaviorState, PhaseStartedUtc, ActiveTelegraph); public static NpcRuntimeRow FromSnapshot(NpcRuntimeStateSnapshot snapshot) => new() { NpcInstanceId = snapshot.NpcInstanceId, BehaviorState = snapshot.BehaviorState, PhaseStartedUtc = snapshot.PhaseStartedUtc, ActiveTelegraph = snapshot.ActiveTelegraph, }; } }