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);
}
}
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 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));
}
}
}