Server
This commit is contained in:
300
LobbyManager.cs
Normal file
300
LobbyManager.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
// Správa lobby - vytváření, join codes, vyhledávání
|
||||
public class LobbyManager
|
||||
{
|
||||
private readonly ServerConfig _config;
|
||||
private readonly ILogger<LobbyManager> _logger;
|
||||
private readonly StatsDb _statsDb;
|
||||
private readonly Persistence _persistence;
|
||||
private readonly OverpassService _overpassService;
|
||||
|
||||
private readonly ConcurrentDictionary<string, LobbyActor> _lobbies = new();
|
||||
private readonly ConcurrentDictionary<string, string> _joinCodes = new(); // code -> lobbyId
|
||||
private readonly ConcurrentDictionary<string, DateTime> _codeExpiry = new();
|
||||
private readonly ConcurrentDictionary<string, RateLimiter> _joinRateLimiters = new();
|
||||
|
||||
// Znaky pro join code - bez podobných (0/O, 1/I/L)
|
||||
private const string JoinCodeChars = "23456789ABCDEFGHJKMNPQRSTUVWXYZ";
|
||||
|
||||
public LobbyManager(ServerConfig config, ILogger<LobbyManager> logger, StatsDb statsDb, Persistence persistence, OverpassService overpassService)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_statsDb = statsDb;
|
||||
_persistence = persistence;
|
||||
_overpassService = overpassService;
|
||||
}
|
||||
|
||||
public async Task<(LobbyActor? Lobby, string? JoinCode, string? Error)> CreateLobbyAsync(
|
||||
string ownerId, string ownerName, CreateLobby request)
|
||||
{
|
||||
var lobbyId = Guid.NewGuid().ToString("N")[..16];
|
||||
var joinCode = GenerateJoinCode();
|
||||
|
||||
var playAreaCenter = request.PlayAreaCenter ?? new Position(50.0, 14.0); // Default Praha
|
||||
var playAreaRadius = request.PlayAreaRadius > 0 ? request.PlayAreaRadius : 500;
|
||||
|
||||
// NOTE: Map data is now fetched when game starts, not on lobby creation
|
||||
// This allows all clients to see a loading screen while data is fetched
|
||||
|
||||
var settings = new LobbySettings
|
||||
{
|
||||
LobbyId = lobbyId,
|
||||
JoinCode = joinCode,
|
||||
Password = request.Password,
|
||||
PlayAreaCenter = playAreaCenter,
|
||||
PlayAreaRadius = playAreaRadius,
|
||||
ImpostorCount = request.ImpostorCount > 0 ? request.ImpostorCount : _config.DefaultImpostorCount,
|
||||
TaskCount = request.TaskCount > 0 ? request.TaskCount : _config.DefaultTaskCount,
|
||||
TiePolicy = _config.DefaultTiePolicy,
|
||||
EmergencyMeetingCooldownMs = _config.EmergencyMeetingCooldownMs,
|
||||
MaxEmergencyMeetingsPerPlayer = _config.MaxEmergencyMeetingsPerPlayer,
|
||||
AllowJoinInProgress = false,
|
||||
MapData = null,
|
||||
MapDataPayload = null,
|
||||
OverpassEnabled = _config.OverpassEnabled
|
||||
};
|
||||
|
||||
var lobbyLogger = _logger.CreateLogger<LobbyActor>();
|
||||
var lobby = new LobbyActor(lobbyId, settings, _config, lobbyLogger, _statsDb, _persistence, _overpassService);
|
||||
|
||||
if (!_lobbies.TryAdd(lobbyId, lobby))
|
||||
{
|
||||
return (null, null, "Nepodařilo se vytvořit lobby");
|
||||
}
|
||||
|
||||
_joinCodes[joinCode] = lobbyId;
|
||||
_codeExpiry[joinCode] = DateTime.UtcNow.AddMilliseconds(_config.JoinCodeTtlMs);
|
||||
|
||||
// Přidáme vlastníka
|
||||
await lobby.AddPlayerAsync(ownerId, ownerName, isOwner: true);
|
||||
|
||||
_logger.LogInformation("Lobby {LobbyId} vytvořeno, join code: {JoinCode}", lobbyId, joinCode);
|
||||
|
||||
return (lobby, joinCode, null);
|
||||
}
|
||||
|
||||
public async Task<(LobbyActor? Lobby, string? Error)> JoinLobbyAsync(
|
||||
string clientIp, string clientUuid, string displayName, string joinCode, string? password)
|
||||
{
|
||||
// Rate limiting per IP
|
||||
var limiter = _joinRateLimiters.GetOrAdd(clientIp, _ => new RateLimiter(_config.JoinRateLimitPerMinute, TimeSpan.FromMinutes(1)));
|
||||
if (!limiter.TryAcquire())
|
||||
{
|
||||
return (null, "Příliš mnoho pokusů o připojení. Počkej chvíli.");
|
||||
}
|
||||
|
||||
// Najdeme lobby podle join code
|
||||
if (!_joinCodes.TryGetValue(joinCode.ToUpperInvariant(), out var lobbyId))
|
||||
{
|
||||
return (null, "Neplatný join code");
|
||||
}
|
||||
|
||||
// Kontrola expirace
|
||||
if (_codeExpiry.TryGetValue(joinCode.ToUpperInvariant(), out var expiry) && expiry < DateTime.UtcNow)
|
||||
{
|
||||
_joinCodes.TryRemove(joinCode.ToUpperInvariant(), out _);
|
||||
_codeExpiry.TryRemove(joinCode.ToUpperInvariant(), out _);
|
||||
return (null, "Join code vypršel");
|
||||
}
|
||||
|
||||
if (!_lobbies.TryGetValue(lobbyId, out var lobby))
|
||||
{
|
||||
return (null, "Lobby již neexistuje");
|
||||
}
|
||||
|
||||
// Kontrola hesla
|
||||
if (!string.IsNullOrEmpty(lobby.Settings.Password) && lobby.Settings.Password != password)
|
||||
{
|
||||
return (null, "Špatné heslo");
|
||||
}
|
||||
|
||||
// Kontrola fáze
|
||||
if (lobby.Phase != GamePhase.Lobby && !lobby.Settings.AllowJoinInProgress)
|
||||
{
|
||||
return (null, "Hra již probíhá");
|
||||
}
|
||||
|
||||
// Kontrola počtu hráčů
|
||||
if (lobby.PlayerCount >= _config.MaxPlayersPerLobby)
|
||||
{
|
||||
return (null, "Lobby je plné");
|
||||
}
|
||||
|
||||
await lobby.AddPlayerAsync(clientUuid, displayName, isOwner: false);
|
||||
|
||||
_logger.LogInformation("Hráč {ClientUuid} se připojil do lobby {LobbyId}", clientUuid, lobbyId);
|
||||
|
||||
return (lobby, null);
|
||||
}
|
||||
|
||||
public LobbyActor? GetLobby(string lobbyId)
|
||||
{
|
||||
_lobbies.TryGetValue(lobbyId, out var lobby);
|
||||
return lobby;
|
||||
}
|
||||
|
||||
public LobbyActor? GetLobbyByJoinCode(string joinCode)
|
||||
{
|
||||
if (_joinCodes.TryGetValue(joinCode.ToUpperInvariant(), out var lobbyId))
|
||||
{
|
||||
return GetLobby(lobbyId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<LobbyActor> GetAllLobbies()
|
||||
{
|
||||
return _lobbies.Values;
|
||||
}
|
||||
|
||||
public void RemoveLobby(string lobbyId)
|
||||
{
|
||||
if (_lobbies.TryRemove(lobbyId, out var lobby))
|
||||
{
|
||||
// Odstraníme join code
|
||||
var joinCode = lobby.Settings.JoinCode;
|
||||
_joinCodes.TryRemove(joinCode, out _);
|
||||
_codeExpiry.TryRemove(joinCode, out _);
|
||||
|
||||
lobby.Dispose();
|
||||
_logger.LogInformation("Lobby {LobbyId} odstraněno", lobbyId);
|
||||
}
|
||||
}
|
||||
|
||||
// Čištění idle lobby
|
||||
public async Task CleanupIdleLobbiesAsync()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var idleThreshold = TimeSpan.FromMilliseconds(_config.IdleLobbyTtlMs);
|
||||
|
||||
foreach (var (lobbyId, lobby) in _lobbies)
|
||||
{
|
||||
if (now - lobby.LastActivity > idleThreshold)
|
||||
{
|
||||
_logger.LogInformation("Ruším idle lobby {LobbyId}", lobbyId);
|
||||
await lobby.ArchiveAndCloseAsync();
|
||||
RemoveLobby(lobbyId);
|
||||
}
|
||||
}
|
||||
|
||||
// Čištění expirovaných join codes
|
||||
foreach (var (code, expiry) in _codeExpiry)
|
||||
{
|
||||
if (expiry < now)
|
||||
{
|
||||
_joinCodes.TryRemove(code, out _);
|
||||
_codeExpiry.TryRemove(code, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateJoinCode()
|
||||
{
|
||||
Span<byte> randomBytes = stackalloc byte[6];
|
||||
RandomNumberGenerator.Fill(randomBytes);
|
||||
|
||||
var sb = new StringBuilder(6);
|
||||
foreach (var b in randomBytes)
|
||||
{
|
||||
sb.Append(JoinCodeChars[b % JoinCodeChars.Length]);
|
||||
}
|
||||
|
||||
var code = sb.ToString();
|
||||
|
||||
// Pokud kód existuje, generujeme znovu
|
||||
if (_joinCodes.ContainsKey(code))
|
||||
{
|
||||
return GenerateJoinCode();
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
public int LobbyCount => _lobbies.Count;
|
||||
|
||||
public int TotalPlayerCount => _lobbies.Values.Sum(l => l.PlayerCount);
|
||||
}
|
||||
|
||||
public class LobbySettings
|
||||
{
|
||||
public required string LobbyId { get; set; }
|
||||
public required string JoinCode { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public Position PlayAreaCenter { get; set; }
|
||||
public double PlayAreaRadius { get; set; }
|
||||
public int ImpostorCount { get; set; }
|
||||
public int TaskCount { get; set; }
|
||||
public TiePolicy TiePolicy { get; set; }
|
||||
public int EmergencyMeetingCooldownMs { get; set; }
|
||||
public int MaxEmergencyMeetingsPerPlayer { get; set; }
|
||||
public bool AllowJoinInProgress { get; set; }
|
||||
public Position EmergencyMeetingLocation { get; set; } // Střed play area jako default
|
||||
|
||||
/// <summary>Full map data from Overpass (server-side only, for task placement)</summary>
|
||||
public MapData? MapData { get; set; }
|
||||
|
||||
/// <summary>Compact map data payload for clients</summary>
|
||||
public MapDataPayload? MapDataPayload { get; set; }
|
||||
|
||||
/// <summary>Whether Overpass API was enabled for this lobby</summary>
|
||||
public bool OverpassEnabled { get; set; }
|
||||
}
|
||||
|
||||
// Jednoduchý rate limiter
|
||||
public class RateLimiter
|
||||
{
|
||||
private readonly int _maxRequests;
|
||||
private readonly TimeSpan _window;
|
||||
private readonly Queue<DateTime> _requests = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
public RateLimiter(int maxRequests, TimeSpan window)
|
||||
{
|
||||
_maxRequests = maxRequests;
|
||||
_window = window;
|
||||
}
|
||||
|
||||
public bool TryAcquire()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var cutoff = now - _window;
|
||||
|
||||
// Odstraníme staré požadavky
|
||||
while (_requests.Count > 0 && _requests.Peek() < cutoff)
|
||||
{
|
||||
_requests.Dequeue();
|
||||
}
|
||||
|
||||
if (_requests.Count >= _maxRequests)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_requests.Enqueue(now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extension pro vytvoření sub-loggeru
|
||||
public static class LoggerExtensions
|
||||
{
|
||||
public static ILogger<T> CreateLogger<T>(this ILogger logger)
|
||||
{
|
||||
if (logger is ILoggerFactory factory)
|
||||
{
|
||||
return factory.CreateLogger<T>();
|
||||
}
|
||||
// Fallback - vrátíme prázdný logger
|
||||
return new LoggerFactory().CreateLogger<T>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user