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 _logger; private readonly StatsDb _statsDb; private readonly Persistence _persistence; private readonly OverpassService _overpassService; private readonly ConcurrentDictionary _lobbies = new(); private readonly ConcurrentDictionary _joinCodes = new(); // code -> lobbyId private readonly ConcurrentDictionary _codeExpiry = new(); private readonly ConcurrentDictionary _joinRateLimiters = new(); // Znaky pro join code - bez podobných (0/O, 1/I/L) private const string JoinCodeChars = "23456789ABCDEFGHJKMNPQRSTUVWXYZ"; public LobbyManager(ServerConfig config, ILogger 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(); 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 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 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 /// Full map data from Overpass (server-side only, for task placement) public MapData? MapData { get; set; } /// Compact map data payload for clients public MapDataPayload? MapDataPayload { get; set; } /// Whether Overpass API was enabled for this lobby public bool OverpassEnabled { get; set; } } // Jednoduchý rate limiter public class RateLimiter { private readonly int _maxRequests; private readonly TimeSpan _window; private readonly Queue _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 CreateLogger(this ILogger logger) { if (logger is ILoggerFactory factory) { return factory.CreateLogger(); } // Fallback - vrátíme prázdný logger return new LoggerFactory().CreateLogger(); } }