using System.Collections.Concurrent; namespace NeonSprawl.Server.Game.Encounters; /// Thread-safe in-memory encounter completion flags (NEO-104). public sealed class InMemoryEncounterCompletionStore : IEncounterCompletionStore { private readonly ConcurrentDictionary completedAtByKey = new(StringComparer.Ordinal); private readonly ConcurrentDictionary keyLocks = new(StringComparer.Ordinal); /// public bool IsCompleted(string playerId, string encounterId) { var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId); if (key.Length == 0) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { return completedAtByKey.ContainsKey(key); } } /// public bool TryMarkCompleted(string playerId, string encounterId, DateTimeOffset completedAt) { var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId); if (key.Length == 0) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { if (completedAtByKey.ContainsKey(key)) { return false; } completedAtByKey[key] = completedAt; return true; } } /// public bool TryGetCompletedAt(string playerId, string encounterId, out DateTimeOffset completedAt) { completedAt = default; var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId); if (key.Length == 0) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { return completedAtByKey.TryGetValue(key, out completedAt); } } /// Dev fixture only — removes one player+encounter completion row (NEO-108 Bruno reset). public bool TryClear(string playerId, string encounterId) { var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId); if (key.Length == 0) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { var removed = completedAtByKey.TryRemove(key, out _); keyLocks.TryRemove(key, out _); return removed; } } }