using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Game.Gigs;
/// Thread-safe in-memory gig progression; seeds the configured dev player with an empty XP map (NEO-44).
public sealed class InMemoryPlayerGigProgressionStore(IOptions options) : IPlayerGigProgressionStore
{
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 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 gigId, int amount, out int previousXp, out int newXp)
{
previousXp = 0;
newXp = 0;
var key = NormalizePlayerId(playerId);
if (key.Length == 0 || gigId.Trim().Length == 0 || !byPlayer.TryGetValue(key, out var inner))
{
return false;
}
var gid = gigId.Trim();
lock (playerLocks.GetOrAdd(key, _ => new object()))
{
inner.TryGetValue(gid, out var prev);
previousXp = prev;
newXp = prev + amount;
if (newXp < 0)
{
return false;
}
inner[gid] = newXp;
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);
}
}