using System.Collections.Concurrent;
namespace NeonSprawl.Server.Game.AbilityInput;
/// Thread-safe in-memory cooldown map (NEO-32); not persisted across process restarts.
public sealed class InMemoryPlayerAbilityCooldownStore : IPlayerAbilityCooldownStore
{
private readonly ConcurrentDictionary> ends = new(StringComparer.Ordinal);
public bool IsOnCooldown(string playerId, int slotIndex, DateTimeOffset now)
{
return TryGetActiveCooldownEnd(playerId, slotIndex, now, out _);
}
public void StartCooldown(string playerId, int slotIndex, DateTimeOffset now, TimeSpan duration)
{
var perSlot = ends.GetOrAdd(playerId, _ => new ConcurrentDictionary());
perSlot[slotIndex] = now + duration;
}
public bool TryGetCooldownEndUtc(string playerId, int slotIndex, DateTimeOffset now, out DateTimeOffset endUtc)
{
return TryGetActiveCooldownEnd(playerId, slotIndex, now, out endUtc);
}
/// Shared read path: returns true with when cooldown is active; removes slot entry when >= stored end.
private static bool TryGetActiveCooldownEnd(
ConcurrentDictionary> ends,
string playerId,
int slotIndex,
DateTimeOffset now,
out DateTimeOffset endUtc)
{
endUtc = default;
if (!ends.TryGetValue(playerId, out var perSlot))
{
return false;
}
if (!perSlot.TryGetValue(slotIndex, out var end))
{
return false;
}
if (now >= end)
{
perSlot.TryRemove(slotIndex, out _);
return false;
}
endUtc = end;
return true;
}
private bool TryGetActiveCooldownEnd(string playerId, int slotIndex, DateTimeOffset now, out DateTimeOffset endUtc)
{
return TryGetActiveCooldownEnd(ends, playerId, slotIndex, now, out endUtc);
}
}