128 lines
3.7 KiB
C#
128 lines
3.7 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Options;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
|
|
namespace NeonSprawl.Server.Game.Items;
|
|
|
|
/// <summary>Thread-safe in-memory inventory; seeds configured dev player with an empty snapshot (NEO-54).</summary>
|
|
public sealed class InMemoryPlayerInventoryStore(IOptions<GamePositionOptions> options) : IPlayerInventoryStore
|
|
{
|
|
private readonly ConcurrentDictionary<string, PlayerInventorySnapshot> byPlayer = CreateInitialMap(options.Value);
|
|
|
|
private readonly ConcurrentDictionary<string, object> playerLocks = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private static ConcurrentDictionary<string, PlayerInventorySnapshot> CreateInitialMap(GamePositionOptions o)
|
|
{
|
|
var id = NormalizePlayerId(o.DevPlayerId);
|
|
var map = new ConcurrentDictionary<string, PlayerInventorySnapshot>(StringComparer.OrdinalIgnoreCase);
|
|
map[id] = PlayerInventorySnapshot.Empty();
|
|
return map;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool TryMutateSnapshot(
|
|
string playerId,
|
|
Func<PlayerInventorySnapshot, PlayerInventoryMutationWrite> 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 = CloneSlots(source.BagSlots),
|
|
EquipmentSlots = CloneSlots(source.EquipmentSlots),
|
|
};
|
|
|
|
private static InventorySlotState[] CloneSlots(InventorySlotState[] slots)
|
|
{
|
|
var copy = new InventorySlotState[slots.Length];
|
|
for (var i = 0; i < slots.Length; i++)
|
|
{
|
|
var s = slots[i];
|
|
copy[i] = new InventorySlotState(s.SlotIndex, s.ItemId, s.Quantity);
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
|
|
private static string NormalizePlayerId(string? playerId)
|
|
{
|
|
var t = playerId?.Trim();
|
|
if (string.IsNullOrEmpty(t))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return t.ToLowerInvariant();
|
|
}
|
|
}
|