neon-sprawl/server/NeonSprawl.Server/Game/Gigs/InMemoryPlayerGigProgressio...

81 lines
2.6 KiB
C#

using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Game.Gigs;
/// <summary>Thread-safe in-memory gig progression; seeds the configured dev player with an empty XP map (NEO-44).</summary>
public sealed class InMemoryPlayerGigProgressionStore(IOptions<GamePositionOptions> options) : IPlayerGigProgressionStore
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, int>> byPlayer = CreateInitialMap(options.Value);
private readonly ConcurrentDictionary<string, object> playerLocks = new(StringComparer.OrdinalIgnoreCase);
private static ConcurrentDictionary<string, ConcurrentDictionary<string, int>> CreateInitialMap(GamePositionOptions o)
{
var id = NormalizePlayerId(o.DevPlayerId);
var map = new ConcurrentDictionary<string, ConcurrentDictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
map[id] = new ConcurrentDictionary<string, int>(StringComparer.Ordinal);
return map;
}
/// <inheritdoc />
public IReadOnlyDictionary<string, int> 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<string, int>(inner, StringComparer.Ordinal);
}
}
/// <inheritdoc />
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<string, int> Empty = new Dictionary<string, int>(StringComparer.Ordinal);
}
}