using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Game.AbilityInput;
/// Thread-safe in-memory hotbar loadout store; seeds configured dev player with an empty loadout.
public sealed class InMemoryPlayerHotbarLoadoutStore(IOptions options) : IPlayerHotbarLoadoutStore
{
private readonly ConcurrentDictionary> byPlayer =
CreateInitialMap(options.Value);
private static ConcurrentDictionary> CreateInitialMap(GamePositionOptions o)
{
var id = NormalizePlayerId(o.DevPlayerId);
var map = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
map[id] = new ConcurrentDictionary();
return map;
}
public bool TryGetBindings(string playerId, out IReadOnlyDictionary bindings)
{
var key = NormalizePlayerId(playerId);
if (key.Length == 0 || !byPlayer.TryGetValue(key, out var map))
{
bindings = EmptyBindings();
return false;
}
bindings = Clone(map);
return true;
}
public bool TryUpsertBindings(string playerId, IReadOnlyList updates, out IReadOnlyDictionary bindings)
{
var key = NormalizePlayerId(playerId);
if (key.Length == 0 || !byPlayer.TryGetValue(key, out var map))
{
bindings = EmptyBindings();
return false;
}
foreach (var update in updates)
{
map[update.SlotIndex] = update.AbilityId;
}
bindings = Clone(map);
return true;
}
private static IReadOnlyDictionary Clone(ConcurrentDictionary source)
{
var copy = new Dictionary(source.Count);
foreach (var kv in source)
{
copy[kv.Key] = kv.Value;
}
return copy;
}
private static IReadOnlyDictionary EmptyBindings() => new Dictionary();
private static string NormalizePlayerId(string? playerId)
{
var t = playerId?.Trim();
if (string.IsNullOrEmpty(t))
{
return string.Empty;
}
return t.ToLowerInvariant();
}
}