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 Dictionary Clone(ConcurrentDictionary source) { var copy = new Dictionary(source.Count); foreach (var kv in source) { copy[kv.Key] = kv.Value; } return copy; } private static Dictionary EmptyBindings() => []; private static string NormalizePlayerId(string? playerId) { var t = playerId?.Trim(); if (string.IsNullOrEmpty(t)) { return string.Empty; } return t.ToLowerInvariant(); } }