81 lines
2.6 KiB
C#
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);
|
|
}
|
|
}
|