using System.Collections.Concurrent; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.Items; /// Thread-safe in-memory inventory; seeds configured dev player with an empty snapshot (NEO-54). public sealed class InMemoryPlayerInventoryStore(IOptions options) : IPlayerInventoryStore { private readonly ConcurrentDictionary byPlayer = CreateInitialMap(options.Value); private readonly ConcurrentDictionary playerLocks = new(StringComparer.OrdinalIgnoreCase); private static ConcurrentDictionary CreateInitialMap(GamePositionOptions o) { var id = NormalizePlayerId(o.DevPlayerId); var map = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); map[id] = PlayerInventorySnapshot.Empty(); return map; } /// public bool TryGetSnapshot(string playerId, out PlayerInventorySnapshot snapshot) { var key = NormalizePlayerId(playerId); if (key.Length == 0) { snapshot = PlayerInventorySnapshot.Empty(); return false; } lock (playerLocks.GetOrAdd(key, _ => new object())) { if (!byPlayer.TryGetValue(key, out var current)) { snapshot = PlayerInventorySnapshot.Empty(); return false; } snapshot = Clone(current); return true; } } /// public bool TryReplaceSnapshot(string playerId, PlayerInventorySnapshot snapshot) { var key = NormalizePlayerId(playerId); if (key.Length == 0) { return false; } lock (playerLocks.GetOrAdd(key, _ => new object())) { if (!byPlayer.ContainsKey(key)) { return false; } byPlayer[key] = Clone(snapshot); return true; } } /// public bool TryMutateSnapshot( string playerId, Func mutator, out PlayerInventorySnapshot result) { var key = NormalizePlayerId(playerId); if (key.Length == 0) { result = PlayerInventorySnapshot.Empty(); return false; } lock (playerLocks.GetOrAdd(key, _ => new object())) { if (!byPlayer.TryGetValue(key, out var current)) { result = PlayerInventorySnapshot.Empty(); return false; } var write = mutator(Clone(current)); result = Clone(write.Value); if (write.Write) { byPlayer[key] = Clone(write.Value); } return true; } } private static PlayerInventorySnapshot Clone(PlayerInventorySnapshot source) => new() { BagSlots = PlayerInventorySnapshot.CloneSlots(source.BagSlots), EquipmentSlots = PlayerInventorySnapshot.CloneSlots(source.EquipmentSlots), }; private static string NormalizePlayerId(string? playerId) { var t = playerId?.Trim(); if (string.IsNullOrEmpty(t)) { return string.Empty; } return t.ToLowerInvariant(); } }