using System.Collections.Concurrent; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.Quests; /// Thread-safe in-memory quest progress; seeds the configured dev player (NEO-116). public sealed class InMemoryPlayerQuestStateStore(IOptions options) : IPlayerQuestStateStore { private sealed class ProgressRow( QuestProgressStatus status, int currentStepIndex, Dictionary objectiveCounters, DateTimeOffset? completedAt) { public QuestProgressStatus Status { get; private set; } = status; public int CurrentStepIndex { get; private set; } = currentStepIndex; public Dictionary ObjectiveCounters { get; } = objectiveCounters; public DateTimeOffset? CompletedAt { get; private set; } = completedAt; public QuestStepState ToSnapshot(string playerId, string questId) => new( playerId, questId, Status, CurrentStepIndex, new Dictionary(ObjectiveCounters, StringComparer.Ordinal), CompletedAt); public void SetStepIndex(int newStepIndex) { CurrentStepIndex = newStepIndex; ObjectiveCounters.Clear(); } public void SetObjectiveCounter(string objectiveId, int newCount) => ObjectiveCounters[objectiveId] = newCount; public void MarkCompleted(DateTimeOffset completedAt) { Status = QuestProgressStatus.Completed; CompletedAt = completedAt; } public static ProgressRow CreateActive() => new(QuestProgressStatus.Active, 0, new Dictionary(StringComparer.Ordinal), null); } private readonly HashSet knownPlayers = CreateKnownPlayers(options.Value); private readonly ConcurrentDictionary byKey = new(StringComparer.Ordinal); private readonly ConcurrentDictionary keyLocks = new(StringComparer.Ordinal); private static HashSet CreateKnownPlayers(GamePositionOptions o) { var id = QuestProgressIds.NormalizePlayerId(o.DevPlayerId); if (id.Length == 0) { throw new InvalidOperationException("Game:DevPlayerId must be non-empty."); } return new HashSet(StringComparer.OrdinalIgnoreCase) { id }; } /// public bool CanWritePlayer(string playerId) { var player = QuestProgressIds.NormalizePlayerId(playerId); return player.Length > 0 && knownPlayers.Contains(player); } /// public bool TryGetProgress(string playerId, string questId, out QuestStepState snapshot) { snapshot = null!; var player = QuestProgressIds.NormalizePlayerId(playerId); var quest = QuestProgressIds.NormalizeQuestId(questId); var key = QuestProgressIds.MakeProgressKey(player, quest); if (key.Length == 0 || !knownPlayers.Contains(player)) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { if (!byKey.TryGetValue(key, out var row)) { return false; } snapshot = row.ToSnapshot(player, quest); return true; } } /// public bool TryActivate(string playerId, string questId, out QuestStepState snapshot) { snapshot = null!; var player = QuestProgressIds.NormalizePlayerId(playerId); var quest = QuestProgressIds.NormalizeQuestId(questId); var key = QuestProgressIds.MakeProgressKey(player, quest); if (key.Length == 0 || !knownPlayers.Contains(player)) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { if (byKey.TryGetValue(key, out var existing)) { snapshot = existing.ToSnapshot(player, quest); return false; } var row = ProgressRow.CreateActive(); byKey[key] = row; snapshot = row.ToSnapshot(player, quest); return true; } } /// public bool TryAdvanceStep(string playerId, string questId, int newStepIndex, out QuestStepState snapshot) { snapshot = null!; if (newStepIndex < 0) { return false; } var player = QuestProgressIds.NormalizePlayerId(playerId); var quest = QuestProgressIds.NormalizeQuestId(questId); var key = QuestProgressIds.MakeProgressKey(player, quest); if (key.Length == 0 || !knownPlayers.Contains(player)) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { if (!byKey.TryGetValue(key, out var row) || row.Status != QuestProgressStatus.Active || newStepIndex <= row.CurrentStepIndex) { if (byKey.TryGetValue(key, out var existing)) { snapshot = existing.ToSnapshot(player, quest); } return false; } row.SetStepIndex(newStepIndex); snapshot = row.ToSnapshot(player, quest); return true; } } /// public bool TryUpdateObjectiveCounter( string playerId, string questId, string objectiveId, int newCount, out QuestStepState snapshot) { snapshot = null!; if (newCount < 0) { return false; } var player = QuestProgressIds.NormalizePlayerId(playerId); var quest = QuestProgressIds.NormalizeQuestId(questId); var objective = QuestProgressIds.NormalizeObjectiveId(objectiveId); var key = QuestProgressIds.MakeProgressKey(player, quest); if (key.Length == 0 || objective.Length == 0 || !knownPlayers.Contains(player)) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { if (!byKey.TryGetValue(key, out var row) || row.Status != QuestProgressStatus.Active) { if (byKey.TryGetValue(key, out var existing)) { snapshot = existing.ToSnapshot(player, quest); } return false; } row.SetObjectiveCounter(objective, newCount); snapshot = row.ToSnapshot(player, quest); return true; } } /// public bool TryMarkComplete(string playerId, string questId, DateTimeOffset completedAt, out QuestStepState snapshot) { snapshot = null!; var player = QuestProgressIds.NormalizePlayerId(playerId); var quest = QuestProgressIds.NormalizeQuestId(questId); var key = QuestProgressIds.MakeProgressKey(player, quest); if (key.Length == 0 || !knownPlayers.Contains(player)) { return false; } lock (keyLocks.GetOrAdd(key, _ => new object())) { if (!byKey.TryGetValue(key, out var row)) { return false; } if (row.Status == QuestProgressStatus.Completed) { snapshot = row.ToSnapshot(player, quest); return false; } row.MarkCompleted(completedAt); snapshot = row.ToSnapshot(player, quest); return true; } } }