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); } } }