109 lines
3.4 KiB
C#
109 lines
3.4 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Options;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
|
|
namespace NeonSprawl.Server.Game.Skills;
|
|
|
|
/// <summary>Thread-safe in-memory progression; seeds the configured dev player with an empty XP map (NEO-38).</summary>
|
|
public sealed class InMemoryPlayerSkillProgressionStore(IOptions<GamePositionOptions> options) : IPlayerSkillProgressionStore
|
|
{
|
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, int>> byPlayer = CreateInitialMap(options.Value);
|
|
|
|
/// <remarks>Locks per normalized player id for coherent read/compare/write on inner dictionaries.</remarks>
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|