using System.Collections.Concurrent; using System.Net.WebSockets; namespace HSchool.Server.Net; /// Tracks live connections and hands out client ids. internal sealed class ClientRegistry { private readonly ConcurrentDictionary _clients = new(); private readonly object _names = new(); private uint _nextPlayerId; public int Count => _clients.Count; /// Snapshot-free enumeration; safe because the dictionary is concurrent. public IEnumerable All => _clients.Values; public GameClient Add(WebSocket socket) { var playerId = Interlocked.Increment(ref _nextPlayerId); var client = new GameClient(playerId, socket); _clients[playerId] = client; return client; } public GameClient? Find(uint playerId) => _clients.GetValueOrDefault(playerId); public void Remove(uint playerId) => _clients.TryRemove(playerId, out _); public bool IsUserNameOnline(string normalizedUserName) { lock (_names) { return HasUserName(normalizedUserName); } } /// /// One live socket per name. Checked under the same lock as /// so a second tab cannot sneak a Welcome in between the HTTP check and SetUserName. /// public bool TryClaimUserName(GameClient client, string userName) { lock (_names) { if (HasUserName(userName)) { return false; } client.SetUserName(userName); return true; } } private bool HasUserName(string normalizedUserName) { foreach (var existing in _clients.Values) { if (existing.UserName is null) { continue; } if (string.Equals(existing.NormalizedUserName, normalizedUserName, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } }