using System.Collections.Concurrent; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.Mastery; /// Thread-safe in-memory perk state; seeds the configured dev player (NEO-47). public sealed class InMemoryPlayerPerkStateStore(IOptions options) : IPlayerPerkStateStore { private readonly ConcurrentDictionary byPlayer = CreateInitialMap(options.Value); private readonly ConcurrentDictionary playerLocks = new(StringComparer.OrdinalIgnoreCase); private static ConcurrentDictionary CreateInitialMap(GamePositionOptions o) { var id = NormalizePlayerId(o.DevPlayerId); var map = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); map[id] = new PlayerPerkState(); return map; } /// public bool CanWritePlayer(string playerId) { var key = NormalizePlayerId(playerId); return key.Length > 0 && byPlayer.ContainsKey(key); } /// public PerkStateSnapshot GetSnapshot(string playerId) { var key = NormalizePlayerId(playerId); if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner)) { return PerkStateSnapshot.Empty; } lock (playerLocks.GetOrAdd(key, _ => new object())) { return inner.ToSnapshot(); } } /// public bool TrySetBranchPick(string playerId, string skillId, int tierIndex, string branchId) { var key = NormalizePlayerId(playerId); var sid = skillId.Trim(); var bid = branchId.Trim(); if (key.Length == 0 || sid.Length == 0 || bid.Length == 0 || tierIndex < 1 || !byPlayer.TryGetValue(key, out var inner)) { return false; } lock (playerLocks.GetOrAdd(key, _ => new object())) { if (inner.HasBranchPick(sid, tierIndex)) { return false; } inner.SetBranchPick(sid, tierIndex, bid); return true; } } /// public bool TryAddUnlockedPerks(string playerId, IEnumerable perkIds) { var key = NormalizePlayerId(playerId); if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner)) { return false; } lock (playerLocks.GetOrAdd(key, _ => new object())) { foreach (var raw in perkIds) { var pid = raw.Trim(); if (pid.Length == 0) { continue; } inner.AddUnlockedPerk(pid); } return true; } } /// public bool TryRemoveBranchPick(string playerId, string skillId, int tierIndex) { var key = NormalizePlayerId(playerId); var sid = skillId.Trim(); if (key.Length == 0 || sid.Length == 0 || tierIndex < 1 || !byPlayer.TryGetValue(key, out var inner)) { return false; } lock (playerLocks.GetOrAdd(key, _ => new object())) { return inner.RemoveBranchPick(sid, tierIndex); } } /// public bool TryResetPerkState(string playerId) { var key = NormalizePlayerId(playerId); if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner)) { return false; } lock (playerLocks.GetOrAdd(key, _ => new object())) { inner.Reset(); return true; } } private static string NormalizePlayerId(string? playerId) { var t = playerId?.Trim(); if (string.IsNullOrEmpty(t)) { return string.Empty; } return t.ToLowerInvariant(); } private sealed class PlayerPerkState { private readonly Dictionary> branchPicks = new(StringComparer.Ordinal); private readonly HashSet unlockedPerks = new(StringComparer.Ordinal); public bool HasBranchPick(string skillId, int tierIndex) => branchPicks.TryGetValue(skillId, out var tiers) && tiers.ContainsKey(tierIndex); public void SetBranchPick(string skillId, int tierIndex, string branchId) { if (!branchPicks.TryGetValue(skillId, out var tiers)) { tiers = new Dictionary(); branchPicks[skillId] = tiers; } tiers[tierIndex] = branchId; } public void AddUnlockedPerk(string perkId) => unlockedPerks.Add(perkId); public bool RemoveBranchPick(string skillId, int tierIndex) => branchPicks.TryGetValue(skillId, out var tiers) && tiers.Remove(tierIndex); public void Reset() { branchPicks.Clear(); unlockedPerks.Clear(); } public PerkStateSnapshot ToSnapshot() { var bySkill = new Dictionary>(StringComparer.Ordinal); foreach (var (skillId, tiers) in branchPicks) { bySkill[skillId] = new Dictionary(tiers); } return new PerkStateSnapshot(bySkill, new HashSet(unlockedPerks, StringComparer.Ordinal)); } } }