neon-sprawl/server/NeonSprawl.Server/Game/AbilityInput/InMemoryPlayerAbilityCooldo...

63 lines
2.2 KiB
C#

using System.Collections.Concurrent;
namespace NeonSprawl.Server.Game.AbilityInput;
/// <summary>Thread-safe in-memory cooldown map (NEO-32); not persisted across process restarts.</summary>
public sealed class InMemoryPlayerAbilityCooldownStore : IPlayerAbilityCooldownStore
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<int, DateTimeOffset>> 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<int, DateTimeOffset>());
perSlot[slotIndex] = now + duration;
}
public bool TryGetCooldownEndUtc(string playerId, int slotIndex, DateTimeOffset now, out DateTimeOffset endUtc)
{
return TryGetActiveCooldownEnd(playerId, slotIndex, now, out endUtc);
}
/// <summary>Shared read path: returns true with <paramref name="endUtc"/> when cooldown is active; removes slot entry when <paramref name="now"/> &gt;= stored end.</summary>
private static bool TryGetActiveCooldownEnd(
ConcurrentDictionary<string, ConcurrentDictionary<int, DateTimeOffset>> 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);
}
/// <inheritdoc />
public void ClearPlayer(string playerId) => _ = ends.TryRemove(playerId, out _);
}