namespace NeonSprawl.Server.Game.Targeting;
/// Thread-safe prototype lock store; player keys are trimmed with ordinal case-insensitive lookup (same spirit as position APIs).
public sealed class InMemoryPlayerTargetLockStore : IPlayerTargetLockStore
{
private readonly object mutex = new();
private readonly Dictionary rows = new(StringComparer.OrdinalIgnoreCase);
private readonly record struct PlayerLockRow(string? LockIdLowercase, int Sequence);
public (string? LockIdLowercase, int Sequence) GetLockState(string playerId)
{
var key = playerId.Trim();
if (key.Length == 0)
{
return (null, 0);
}
lock (mutex)
{
return rows.TryGetValue(key, out var row) ? (row.LockIdLowercase, row.Sequence) : (null, 0);
}
}
public (string? LockIdLowercase, int Sequence) ApplyClear(string playerId)
{
var key = playerId.Trim();
if (key.Length == 0)
{
return (null, 0);
}
lock (mutex)
{
if (!rows.TryGetValue(key, out var prev))
{
return (null, 0);
}
if (prev.LockIdLowercase is null)
{
return (null, prev.Sequence);
}
var next = new PlayerLockRow(null, prev.Sequence + 1);
rows[key] = next;
// NEO-27 telemetry hook site: `target_changed` fires when lock id transitions.
// TODO(E9.M1): emit cataloged telemetry event (player id, previous/next lock, cause=clear).
return (null, next.Sequence);
}
}
public (string? LockIdLowercase, int Sequence) ApplySet(string playerId, string targetIdLowercase)
{
var key = playerId.Trim();
if (key.Length == 0)
{
return (null, 0);
}
lock (mutex)
{
var prev = rows.TryGetValue(key, out var r) ? r : new PlayerLockRow(null, 0);
if (string.Equals(prev.LockIdLowercase, targetIdLowercase, StringComparison.Ordinal))
{
return (prev.LockIdLowercase, prev.Sequence);
}
var next = new PlayerLockRow(targetIdLowercase, prev.Sequence + 1);
rows[key] = next;
// NEO-27 telemetry hook site: `target_changed` fires when lock id transitions.
// TODO(E9.M1): emit cataloged telemetry event (player id, previous/next lock, cause=tab/select).
return (next.LockIdLowercase, next.Sequence);
}
}
}