132 lines
4.1 KiB
C#
132 lines
4.1 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Options;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
|
|
namespace NeonSprawl.Server.Game.Mastery;
|
|
|
|
/// <summary>Thread-safe in-memory perk state; seeds the configured dev player (NEO-47).</summary>
|
|
public sealed class InMemoryPlayerPerkStateStore(IOptions<GamePositionOptions> options) : IPlayerPerkStateStore
|
|
{
|
|
private readonly ConcurrentDictionary<string, PlayerPerkState> byPlayer = CreateInitialMap(options.Value);
|
|
|
|
private readonly ConcurrentDictionary<string, object> playerLocks = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private static ConcurrentDictionary<string, PlayerPerkState> CreateInitialMap(GamePositionOptions o)
|
|
{
|
|
var id = NormalizePlayerId(o.DevPlayerId);
|
|
var map = new ConcurrentDictionary<string, PlayerPerkState>(StringComparer.OrdinalIgnoreCase);
|
|
map[id] = new PlayerPerkState();
|
|
return map;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public PerkStateSnapshot GetSnapshot(string playerId)
|
|
{
|
|
var key = NormalizePlayerId(playerId);
|
|
if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner))
|
|
{
|
|
return PerkStateSnapshot.Empty;
|
|
}
|
|
|
|
lock (playerLocks.GetOrAdd(key, _ => new object()))
|
|
{
|
|
return inner.ToSnapshot();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool TrySetBranchPick(string playerId, string skillId, int tierIndex, string branchId)
|
|
{
|
|
var key = NormalizePlayerId(playerId);
|
|
var sid = skillId.Trim();
|
|
var bid = branchId.Trim();
|
|
if (key.Length == 0 || sid.Length == 0 || bid.Length == 0 || tierIndex < 1
|
|
|| !byPlayer.TryGetValue(key, out var inner))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
lock (playerLocks.GetOrAdd(key, _ => new object()))
|
|
{
|
|
if (inner.HasBranchPick(sid, tierIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
inner.SetBranchPick(sid, tierIndex, bid);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool TryAddUnlockedPerks(string playerId, IEnumerable<string> perkIds)
|
|
{
|
|
var key = NormalizePlayerId(playerId);
|
|
if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
lock (playerLocks.GetOrAdd(key, _ => new object()))
|
|
{
|
|
foreach (var raw in perkIds)
|
|
{
|
|
var pid = raw.Trim();
|
|
if (pid.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
inner.AddUnlockedPerk(pid);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static string NormalizePlayerId(string? playerId)
|
|
{
|
|
var t = playerId?.Trim();
|
|
if (string.IsNullOrEmpty(t))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return t.ToLowerInvariant();
|
|
}
|
|
|
|
private sealed class PlayerPerkState
|
|
{
|
|
private readonly Dictionary<string, Dictionary<int, string>> branchPicks = new(StringComparer.Ordinal);
|
|
|
|
private readonly HashSet<string> unlockedPerks = new(StringComparer.Ordinal);
|
|
|
|
public bool HasBranchPick(string skillId, int tierIndex) =>
|
|
branchPicks.TryGetValue(skillId, out var tiers) && tiers.ContainsKey(tierIndex);
|
|
|
|
public void SetBranchPick(string skillId, int tierIndex, string branchId)
|
|
{
|
|
if (!branchPicks.TryGetValue(skillId, out var tiers))
|
|
{
|
|
tiers = new Dictionary<int, string>();
|
|
branchPicks[skillId] = tiers;
|
|
}
|
|
|
|
tiers[tierIndex] = branchId;
|
|
}
|
|
|
|
public void AddUnlockedPerk(string perkId) => unlockedPerks.Add(perkId);
|
|
|
|
public PerkStateSnapshot ToSnapshot()
|
|
{
|
|
var bySkill = new Dictionary<string, IReadOnlyDictionary<int, string>>(StringComparer.Ordinal);
|
|
foreach (var (skillId, tiers) in branchPicks)
|
|
{
|
|
bySkill[skillId] = new Dictionary<int, string>(tiers);
|
|
}
|
|
|
|
return new PerkStateSnapshot(bySkill, new HashSet<string>(unlockedPerks, StringComparer.Ordinal));
|
|
}
|
|
}
|
|
}
|