using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Game.Skills;
/// Thread-safe in-memory progression; seeds the configured dev player with an empty XP map (NEO-38).
public sealed class InMemoryPlayerSkillProgressionStore(IOptions options) : IPlayerSkillProgressionStore
{
private readonly ConcurrentDictionary> byPlayer = CreateInitialMap(options.Value);
/// Locks per normalized player id for coherent read/compare/write on inner dictionaries.
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 ConcurrentDictionary(StringComparer.Ordinal);
return map;
}
///
public IReadOnlyDictionary GetXpTotals(string playerId)
{
var key = NormalizePlayerId(playerId);
if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner))
{
return ReadOnlyDic.Empty;
}
lock (playerLocks.GetOrAdd(key, _ => new object()))
{
return new Dictionary(inner, StringComparer.Ordinal);
}
}
///
public bool TryApplyXpDelta(string playerId, string skillId, int amount, out int previousXp, out int newXp)
{
previousXp = 0;
newXp = 0;
var key = NormalizePlayerId(playerId);
if (key.Length == 0 || skillId.Trim().Length == 0 || !byPlayer.TryGetValue(key, out var inner))
{
return false;
}
var sid = skillId.Trim();
lock (playerLocks.GetOrAdd(key, _ => new object()))
{
inner.TryGetValue(sid, out var prev);
previousXp = prev;
newXp = prev + amount;
if (newXp < 0)
{
return false;
}
inner[sid] = newXp;
return true;
}
}
///
public bool TrySetSkillXpTotal(string playerId, string skillId, int xp)
{
if (xp < 0)
{
return false;
}
var key = NormalizePlayerId(playerId);
var sid = skillId.Trim();
if (key.Length == 0 || sid.Length == 0 || !byPlayer.TryGetValue(key, out var inner))
{
return false;
}
lock (playerLocks.GetOrAdd(key, _ => new object()))
{
if (xp == 0)
{
return inner.TryRemove(sid, out _);
}
inner[sid] = xp;
return true;
}
}
///
public bool TrySetSkillXpTotals(string playerId, IReadOnlyDictionary skillXpTotals)
{
var key = NormalizePlayerId(playerId);
if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner))
{
return false;
}
foreach (var (skillId, xp) in skillXpTotals)
{
if (skillId.Trim().Length == 0 || xp < 0)
{
return false;
}
}
lock (playerLocks.GetOrAdd(key, _ => new object()))
{
foreach (var (skillId, xp) in skillXpTotals)
{
var sid = skillId.Trim();
if (xp == 0)
{
inner.TryRemove(sid, out _);
}
else
{
inner[sid] = xp;
}
}
return true;
}
}
private static string NormalizePlayerId(string? playerId)
{
var t = playerId?.Trim();
if (string.IsNullOrEmpty(t))
{
return string.Empty;
}
return t.ToLowerInvariant();
}
private static class ReadOnlyDic
{
internal static readonly IReadOnlyDictionary Empty = new Dictionary(StringComparer.Ordinal);
}
}