Two tabs with the cookie already set never POST /api/session, so name-online on login did not cover the design rule. Claim the name under the same lock as the online check. Co-authored-by: Cursor <cursoragent@cursor.com>
74 lines
2.0 KiB
C#
74 lines
2.0 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Net.WebSockets;
|
|
|
|
namespace HSchool.Server.Net;
|
|
|
|
/// <summary>Tracks live connections and hands out client ids.</summary>
|
|
internal sealed class ClientRegistry
|
|
{
|
|
private readonly ConcurrentDictionary<uint, GameClient> _clients = new();
|
|
private readonly object _names = new();
|
|
private uint _nextPlayerId;
|
|
|
|
public int Count => _clients.Count;
|
|
|
|
/// <summary>Snapshot-free enumeration; safe because the dictionary is concurrent.</summary>
|
|
public IEnumerable<GameClient> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// One live socket per name. Checked under the same lock as <see cref="IsUserNameOnline"/>
|
|
/// so a second tab cannot sneak a Welcome in between the HTTP check and SetUserName.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|