Server
This commit is contained in:
795
AdminPanel.cs
Normal file
795
AdminPanel.cs
Normal file
@@ -0,0 +1,795 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Admin Panel - Webová administrace serveru
|
||||
///
|
||||
/// Funkce:
|
||||
/// - Přehled stavu serveru
|
||||
/// - Real-time spectate mode pro lobby
|
||||
/// - Konfigurace serveru za běhu
|
||||
/// - Informace o hráčích
|
||||
/// - WebSocket pro live updates
|
||||
/// </summary>
|
||||
public class AdminPanel
|
||||
{
|
||||
private readonly ILogger<AdminPanel> _logger;
|
||||
private readonly ServerConfig _config;
|
||||
private readonly LobbyManager _lobbyManager;
|
||||
private readonly StatsDb _statsDb;
|
||||
|
||||
// Bezpečnost
|
||||
private readonly string _adminPasswordHash;
|
||||
private readonly ConcurrentDictionary<string, AdminSession> _sessions = new();
|
||||
private readonly TimeSpan _sessionTimeout = TimeSpan.FromHours(8);
|
||||
|
||||
// WebSocket connections pro spectate
|
||||
private readonly ConcurrentDictionary<string, SpectatorConnection> _spectators = new();
|
||||
|
||||
public AdminPanel(
|
||||
ILogger<AdminPanel> logger,
|
||||
ServerConfig config,
|
||||
LobbyManager lobbyManager,
|
||||
StatsDb statsDb)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_lobbyManager = lobbyManager;
|
||||
_statsDb = statsDb;
|
||||
|
||||
// Hash admin hesla (z env nebo default)
|
||||
var adminPassword = Environment.GetEnvironmentVariable("GEOSUS_ADMIN_PASSWORD") ?? "admin123";
|
||||
_adminPasswordHash = HashPassword(adminPassword);
|
||||
|
||||
_logger.LogInformation("Admin Panel inicializován");
|
||||
}
|
||||
|
||||
#region ═══════════════════════════════════════════════════════════════════
|
||||
// AUTENTIZACE
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
#endregion
|
||||
|
||||
private static string HashPassword(string password)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password + "GeoSus_Salt_2026"));
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private string? ValidateSession(HttpListenerRequest request)
|
||||
{
|
||||
var cookie = request.Cookies["geosus_admin_session"];
|
||||
if (cookie == null) return null;
|
||||
|
||||
if (_sessions.TryGetValue(cookie.Value, out var session))
|
||||
{
|
||||
if (DateTime.UtcNow - session.LastActivity < _sessionTimeout)
|
||||
{
|
||||
session.LastActivity = DateTime.UtcNow;
|
||||
return cookie.Value;
|
||||
}
|
||||
_sessions.TryRemove(cookie.Value, out _);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string CreateSession()
|
||||
{
|
||||
var sessionId = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
_sessions[sessionId] = new AdminSession { LastActivity = DateTime.UtcNow };
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
#region ═══════════════════════════════════════════════════════════════════
|
||||
// HTTP HANDLING
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
#endregion
|
||||
|
||||
public async Task HandleRequestAsync(HttpListenerContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
var path = request.Url?.AbsolutePath ?? "/";
|
||||
|
||||
try
|
||||
{
|
||||
// Login endpoint - vždy přístupný
|
||||
if (path == "/admin/login" && request.HttpMethod == "POST")
|
||||
{
|
||||
await HandleLoginAsync(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Statické soubory pro login stránku - bez autentizace
|
||||
if (path == "/admin" || path == "/admin/")
|
||||
{
|
||||
await ServeStaticFileAsync(response, "admin.html", "text/html");
|
||||
return;
|
||||
}
|
||||
|
||||
// CSS a JS musí být dostupné bez autentizace (pro login stránku)
|
||||
if (path == "/admin/app.js")
|
||||
{
|
||||
await ServeStaticFileAsync(response, "admin.js", "application/javascript");
|
||||
return;
|
||||
}
|
||||
|
||||
if (path == "/admin/style.css")
|
||||
{
|
||||
await ServeStaticFileAsync(response, "admin.css", "text/css");
|
||||
return;
|
||||
}
|
||||
|
||||
// Všechno ostatní vyžaduje autentizaci
|
||||
var sessionId = ValidateSession(request);
|
||||
if (sessionId == null)
|
||||
{
|
||||
response.StatusCode = 401;
|
||||
await WriteJsonAsync(response, new { error = "Unauthorized", redirect = "/admin" });
|
||||
return;
|
||||
}
|
||||
|
||||
// WebSocket pro spectate
|
||||
if (path.StartsWith("/admin/ws/spectate/") && request.IsWebSocketRequest)
|
||||
{
|
||||
var lobbyId = path["/admin/ws/spectate/".Length..];
|
||||
await HandleSpectateWebSocketAsync(context, lobbyId, sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// WebSocket pro server stats
|
||||
if (path == "/admin/ws/stats" && request.IsWebSocketRequest)
|
||||
{
|
||||
await HandleStatsWebSocketAsync(context, sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// REST API endpointy
|
||||
switch (path)
|
||||
{
|
||||
case "/admin/api/status":
|
||||
await HandleStatusAsync(response);
|
||||
break;
|
||||
|
||||
case "/admin/api/lobbies":
|
||||
await HandleLobbiesAsync(response);
|
||||
break;
|
||||
|
||||
case "/admin/api/config":
|
||||
if (request.HttpMethod == "GET")
|
||||
await HandleGetConfigAsync(response);
|
||||
else if (request.HttpMethod == "POST")
|
||||
await HandleSetConfigAsync(request, response);
|
||||
break;
|
||||
|
||||
case "/admin/api/players":
|
||||
await HandlePlayersAsync(request, response);
|
||||
break;
|
||||
|
||||
case "/admin/api/kick":
|
||||
await HandleKickAsync(request, response);
|
||||
break;
|
||||
|
||||
case "/admin/api/broadcast":
|
||||
await HandleBroadcastAsync(request, response);
|
||||
break;
|
||||
|
||||
case "/admin/api/logout":
|
||||
_sessions.TryRemove(sessionId, out _);
|
||||
response.SetCookie(new Cookie("geosus_admin_session", "", "/admin") { Expired = true });
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
break;
|
||||
|
||||
default:
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonAsync(response, new { error = "Not found" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Admin panel error: {Path}", path);
|
||||
|
||||
// Don't try to write response for WebSocket requests (already submitted)
|
||||
if (!request.IsWebSocketRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
response.StatusCode = 500;
|
||||
await WriteJsonAsync(response, new { error = ex.Message });
|
||||
}
|
||||
catch { /* Response may already be sent */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region ═══════════════════════════════════════════════════════════════════
|
||||
// LOGIN
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
#endregion
|
||||
|
||||
private async Task HandleLoginAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
using var reader = new StreamReader(request.InputStream);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
var password = data.GetProperty("password").GetString() ?? "";
|
||||
var hash = HashPassword(password);
|
||||
|
||||
if (hash == _adminPasswordHash)
|
||||
{
|
||||
var sessionId = CreateSession();
|
||||
response.SetCookie(new Cookie("geosus_admin_session", sessionId, "/admin")
|
||||
{
|
||||
HttpOnly = true,
|
||||
Expires = DateTime.Now.Add(_sessionTimeout)
|
||||
});
|
||||
|
||||
_logger.LogInformation("Admin login successful from {IP}", request.RemoteEndPoint);
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Admin login failed from {IP}", request.RemoteEndPoint);
|
||||
response.StatusCode = 401;
|
||||
await WriteJsonAsync(response, new { error = "Invalid password" });
|
||||
}
|
||||
}
|
||||
|
||||
#region ═══════════════════════════════════════════════════════════════════
|
||||
// API ENDPOINTS
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
#endregion
|
||||
|
||||
private async Task HandleStatusAsync(HttpListenerResponse response)
|
||||
{
|
||||
var uptime = DateTime.UtcNow - Program.StartTime;
|
||||
var status = new
|
||||
{
|
||||
status = "online",
|
||||
version = "1.0.0",
|
||||
uptime = new
|
||||
{
|
||||
days = uptime.Days,
|
||||
hours = uptime.Hours,
|
||||
minutes = uptime.Minutes,
|
||||
seconds = uptime.Seconds,
|
||||
totalSeconds = (long)uptime.TotalSeconds
|
||||
},
|
||||
lobbies = new
|
||||
{
|
||||
active = _lobbyManager.LobbyCount,
|
||||
players = _lobbyManager.TotalPlayerCount
|
||||
},
|
||||
memory = new
|
||||
{
|
||||
usedMb = GC.GetTotalMemory(false) / 1024 / 1024,
|
||||
gcCollections = new
|
||||
{
|
||||
gen0 = GC.CollectionCount(0),
|
||||
gen1 = GC.CollectionCount(1),
|
||||
gen2 = GC.CollectionCount(2)
|
||||
}
|
||||
},
|
||||
sessions = _sessions.Count,
|
||||
spectators = _spectators.Count
|
||||
};
|
||||
|
||||
await WriteJsonAsync(response, status);
|
||||
}
|
||||
|
||||
private async Task HandleLobbiesAsync(HttpListenerResponse response)
|
||||
{
|
||||
var lobbies = _lobbyManager.GetAllLobbies().Select(l => new
|
||||
{
|
||||
id = l.LobbyId,
|
||||
joinCode = l.JoinCode,
|
||||
phase = l.Phase.ToString(),
|
||||
players = l.GetPlayers().Select(p => new
|
||||
{
|
||||
id = p.PlayerId,
|
||||
name = p.DisplayName,
|
||||
state = p.State.ToString(),
|
||||
role = p.Role.ToString(),
|
||||
position = new { lat = p.Position.Lat, lng = p.Position.Lon },
|
||||
isHost = p.IsHost
|
||||
}),
|
||||
playerCount = l.PlayerCount,
|
||||
settings = new
|
||||
{
|
||||
impostorCount = l.Settings.ImpostorCount,
|
||||
taskCount = l.Settings.TaskCount,
|
||||
center = new { lat = l.Settings.PlayAreaCenter.Lat, lng = l.Settings.PlayAreaCenter.Lon },
|
||||
radius = l.Settings.PlayAreaRadius
|
||||
},
|
||||
createdAt = l.CreatedAt,
|
||||
activeSabotage = l.ActiveSabotage?.Type.ToString()
|
||||
});
|
||||
|
||||
await WriteJsonAsync(response, new { lobbies });
|
||||
}
|
||||
|
||||
private async Task HandleGetConfigAsync(HttpListenerResponse response)
|
||||
{
|
||||
// Vrátíme pouze bezpečné konfigurace (ne hesla apod.)
|
||||
var config = new
|
||||
{
|
||||
tcpPort = _config.TcpPort,
|
||||
httpPort = _config.HttpPort,
|
||||
maxPlayersPerLobby = _config.MaxPlayersPerLobby,
|
||||
killDistanceM = _config.KillDistanceM,
|
||||
killCooldownMs = _config.KillCooldownMs,
|
||||
discussionPhaseMs = _config.DiscussionPhaseMs,
|
||||
votingPhaseMs = _config.VotingPhaseMs,
|
||||
taskStartDistanceM = _config.TaskStartDistanceM,
|
||||
sabotageCooldownMs = _config.SabotageCooldownMs,
|
||||
criticalMeltdownDeadlineMs = _config.CriticalMeltdownDeadlineMs,
|
||||
maxSpeedMps = _config.MaxSpeedMps,
|
||||
teleportThresholdMeters = _config.TeleportThresholdMeters
|
||||
};
|
||||
|
||||
await WriteJsonAsync(response, config);
|
||||
}
|
||||
|
||||
private async Task HandleSetConfigAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
using var reader = new StreamReader(request.InputStream);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
// Aplikuj změny
|
||||
if (data.TryGetProperty("killDistanceM", out var kd)) _config.KillDistanceM = kd.GetDouble();
|
||||
if (data.TryGetProperty("killCooldownMs", out var kc)) _config.KillCooldownMs = kc.GetInt32();
|
||||
if (data.TryGetProperty("discussionPhaseMs", out var dp)) _config.DiscussionPhaseMs = dp.GetInt32();
|
||||
if (data.TryGetProperty("votingPhaseMs", out var vp)) _config.VotingPhaseMs = vp.GetInt32();
|
||||
if (data.TryGetProperty("maxSpeedMps", out var ms)) _config.MaxSpeedMps = ms.GetDouble();
|
||||
|
||||
_logger.LogInformation("Config updated via admin panel");
|
||||
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
}
|
||||
|
||||
private async Task HandlePlayersAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
var query = request.QueryString;
|
||||
var search = query["search"] ?? "";
|
||||
|
||||
var allPlayers = _lobbyManager.GetAllLobbies()
|
||||
.SelectMany(l => l.GetPlayers().Select(p => new
|
||||
{
|
||||
playerId = p.PlayerId,
|
||||
displayName = p.DisplayName,
|
||||
lobbyId = l.LobbyId,
|
||||
joinCode = l.JoinCode,
|
||||
state = p.State.ToString(),
|
||||
role = p.Role.ToString(),
|
||||
position = new { lat = p.Position.Lat, lng = p.Position.Lon },
|
||||
isHost = p.IsHost,
|
||||
cheatScore = p.CheatScore
|
||||
}))
|
||||
.Where(p => string.IsNullOrEmpty(search) ||
|
||||
p.displayName.Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
p.playerId.Contains(search, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
await WriteJsonAsync(response, new { players = allPlayers, total = allPlayers.Count });
|
||||
}
|
||||
|
||||
private async Task HandleKickAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
using var reader = new StreamReader(request.InputStream);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
var playerId = data.GetProperty("playerId").GetString();
|
||||
var reason = data.TryGetProperty("reason", out var r) ? r.GetString() : "Kicked by admin";
|
||||
|
||||
// Najdi hráče a kickni ho
|
||||
foreach (var lobby in _lobbyManager.GetAllLobbies())
|
||||
{
|
||||
if (lobby.TryKickPlayer(playerId!, reason!))
|
||||
{
|
||||
_logger.LogInformation("Player {PlayerId} kicked by admin: {Reason}", playerId, reason);
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonAsync(response, new { error = "Player not found" });
|
||||
}
|
||||
|
||||
private async Task HandleBroadcastAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
using var reader = new StreamReader(request.InputStream);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
var message = data.GetProperty("message").GetString();
|
||||
var lobbyId = data.TryGetProperty("lobbyId", out var l) ? l.GetString() : null;
|
||||
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
{
|
||||
// Broadcast všem
|
||||
foreach (var lobby in _lobbyManager.GetAllLobbies())
|
||||
{
|
||||
lobby.BroadcastSystemMessage(message!);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Broadcast do konkrétní lobby
|
||||
var lobby = _lobbyManager.GetLobby(lobbyId);
|
||||
lobby?.BroadcastSystemMessage(message!);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Admin broadcast: {Message}", message);
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
}
|
||||
|
||||
#region ═══════════════════════════════════════════════════════════════════
|
||||
// WEBSOCKET - SPECTATE
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
#endregion
|
||||
|
||||
private async Task HandleSpectateWebSocketAsync(HttpListenerContext context, string lobbyId, string sessionId)
|
||||
{
|
||||
var wsContext = await context.AcceptWebSocketAsync(null);
|
||||
var ws = wsContext.WebSocket;
|
||||
var spectatorId = Guid.NewGuid().ToString("N")[..8];
|
||||
|
||||
var lobby = _lobbyManager.GetLobby(lobbyId);
|
||||
if (lobby == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ws.CloseAsync(WebSocketCloseStatus.InvalidPayloadData, "Lobby not found", CancellationToken.None);
|
||||
}
|
||||
catch (WebSocketException) { /* Client already disconnected */ }
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = new SpectatorConnection
|
||||
{
|
||||
WebSocket = ws,
|
||||
LobbyId = lobbyId,
|
||||
SessionId = sessionId
|
||||
};
|
||||
|
||||
_spectators[spectatorId] = connection;
|
||||
_logger.LogInformation("Spectator {Id} connected to lobby {LobbyId}", spectatorId, lobbyId);
|
||||
|
||||
try
|
||||
{
|
||||
// Pošli initial state
|
||||
await SendLobbyStateAsync(ws, lobby);
|
||||
|
||||
var buffer = new byte[4096];
|
||||
var updateInterval = TimeSpan.FromMilliseconds(200);
|
||||
var cts = new CancellationTokenSource();
|
||||
var receivedMessage = new TaskCompletionSource<(bool closed, string? message)>();
|
||||
|
||||
// Spustíme receive task separátně
|
||||
var receiveTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (ws.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
return true; // closed
|
||||
}
|
||||
|
||||
if (result.Count > 0)
|
||||
{
|
||||
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
await HandleSpectatorMessageAsync(ws, lobby!, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (WebSocketException) { }
|
||||
return ws.State != WebSocketState.Open;
|
||||
});
|
||||
|
||||
// Hlavní smyčka posílá state
|
||||
while (ws.State == WebSocketState.Open && !receiveTask.IsCompleted)
|
||||
{
|
||||
lobby = _lobbyManager.GetLobby(lobbyId);
|
||||
if (lobby != null)
|
||||
{
|
||||
await SendLobbyStateAsync(ws, lobby);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAny(receiveTask, Task.Delay(updateInterval, cts.Token));
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
}
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
// Gracefully close if still open
|
||||
if (ws.State == WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, lobby == null ? "Lobby closed" : "Closing", CancellationToken.None);
|
||||
}
|
||||
catch (WebSocketException) { /* Already closed by client */ }
|
||||
}
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{
|
||||
// Client disconnected - this is normal
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Spectate WebSocket error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_spectators.TryRemove(spectatorId, out _);
|
||||
_logger.LogInformation("Spectator {Id} disconnected", spectatorId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendLobbyStateAsync(WebSocket ws, LobbyActor lobby)
|
||||
{
|
||||
var state = new
|
||||
{
|
||||
type = "state",
|
||||
lobby = new
|
||||
{
|
||||
id = lobby.LobbyId,
|
||||
joinCode = lobby.JoinCode,
|
||||
phase = lobby.Phase.ToString(),
|
||||
phaseEndTime = lobby.PhaseEndTime?.ToString("o"),
|
||||
|
||||
players = lobby.GetPlayers().Select(p => new
|
||||
{
|
||||
id = p.PlayerId,
|
||||
name = p.DisplayName,
|
||||
state = p.State.ToString(),
|
||||
role = p.Role.ToString(),
|
||||
lat = p.Position.Lat,
|
||||
lng = p.Position.Lon,
|
||||
isHost = p.IsHost,
|
||||
cheatScore = p.CheatScore,
|
||||
killCooldownEnd = p.KillCooldownEnd?.ToString("o"),
|
||||
votedFor = p.VotedFor
|
||||
}),
|
||||
|
||||
tasks = lobby.GetTasks().Select(t => new
|
||||
{
|
||||
id = t.TaskId,
|
||||
name = t.Name,
|
||||
lat = t.Location.Lat,
|
||||
lng = t.Location.Lon,
|
||||
completedBy = t.CompletedBy.ToList()
|
||||
}),
|
||||
|
||||
bodies = lobby.GetBodies().Select(b => new
|
||||
{
|
||||
victimId = b.VictimId,
|
||||
lat = b.Position.Lat,
|
||||
lng = b.Position.Lon,
|
||||
reportedAt = b.ReportedAt?.ToString("o")
|
||||
}),
|
||||
|
||||
sabotage = lobby.ActiveSabotage != null ? new
|
||||
{
|
||||
type = lobby.ActiveSabotage.Type.ToString(),
|
||||
deadline = lobby.ActiveSabotage.Deadline?.ToString("o"),
|
||||
repairStations = lobby.ActiveSabotage.RepairStations.Select(rs => new
|
||||
{
|
||||
id = rs.StationId,
|
||||
lat = rs.Location.Lat,
|
||||
lng = rs.Location.Lon,
|
||||
isRepaired = rs.IsRepaired
|
||||
})
|
||||
} : null,
|
||||
|
||||
votes = lobby.Phase == GamePhase.Voting ? lobby.GetVotes() : null,
|
||||
|
||||
settings = new
|
||||
{
|
||||
center = new { lat = lobby.Settings.PlayAreaCenter.Lat, lng = lobby.Settings.PlayAreaCenter.Lon },
|
||||
radius = lobby.Settings.PlayAreaRadius,
|
||||
impostorCount = lobby.Settings.ImpostorCount,
|
||||
taskCount = lobby.Settings.TaskCount
|
||||
},
|
||||
|
||||
mapData = lobby.MapData // Overpass data pro vykreslení mapy
|
||||
},
|
||||
timestamp = DateTime.UtcNow.ToString("o")
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(state, JsonOptions.Default);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
if (ws.State == WebSocketState.Open)
|
||||
{
|
||||
await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleSpectatorMessageAsync(WebSocket ws, LobbyActor lobby, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(message);
|
||||
var type = data.GetProperty("type").GetString();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "ping":
|
||||
await SendJsonAsync(ws, new { type = "pong" });
|
||||
break;
|
||||
|
||||
case "getPlayerHistory":
|
||||
var playerId = data.GetProperty("playerId").GetString();
|
||||
var stats = _statsDb.GetPlayerStats(playerId!);
|
||||
await SendJsonAsync(ws, new { type = "playerHistory", stats });
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
#region ═══════════════════════════════════════════════════════════════════
|
||||
// WEBSOCKET - SERVER STATS
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
#endregion
|
||||
|
||||
private async Task HandleStatsWebSocketAsync(HttpListenerContext context, string sessionId)
|
||||
{
|
||||
var wsContext = await context.AcceptWebSocketAsync(null);
|
||||
var ws = wsContext.WebSocket;
|
||||
|
||||
try
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
var updateInterval = TimeSpan.FromSeconds(1);
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
// Spustíme receive task separátně - bude běžet celou dobu
|
||||
var receiveTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (ws.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (WebSocketException) { }
|
||||
});
|
||||
|
||||
// Hlavní smyčka posílá stats
|
||||
while (ws.State == WebSocketState.Open && !receiveTask.IsCompleted)
|
||||
{
|
||||
var uptime = DateTime.UtcNow - Program.StartTime;
|
||||
var stats = new
|
||||
{
|
||||
type = "stats",
|
||||
uptime = (long)uptime.TotalSeconds,
|
||||
lobbies = _lobbyManager.LobbyCount,
|
||||
players = _lobbyManager.TotalPlayerCount,
|
||||
memoryMb = GC.GetTotalMemory(false) / 1024 / 1024,
|
||||
spectators = _spectators.Count,
|
||||
timestamp = DateTime.UtcNow.ToString("o")
|
||||
};
|
||||
|
||||
await SendJsonAsync(ws, stats);
|
||||
|
||||
// Čekáme na interval nebo ukončení receive tasku
|
||||
var delayTask = Task.Delay(updateInterval, cts.Token);
|
||||
try
|
||||
{
|
||||
await Task.WhenAny(receiveTask, delayTask);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
}
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
if (ws.State == WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
|
||||
}
|
||||
catch (WebSocketException) { /* Already closed */ }
|
||||
}
|
||||
}
|
||||
catch (WebSocketException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Stats WebSocket error");
|
||||
}
|
||||
}
|
||||
|
||||
#region ═══════════════════════════════════════════════════════════════════
|
||||
// HELPERS
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
#endregion
|
||||
|
||||
private static async Task WriteJsonAsync(HttpListenerResponse response, object data)
|
||||
{
|
||||
response.ContentType = "application/json";
|
||||
var json = JsonSerializer.Serialize(data, JsonOptions.Default);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
response.ContentLength64 = bytes.Length;
|
||||
await response.OutputStream.WriteAsync(bytes);
|
||||
}
|
||||
|
||||
private static async Task SendJsonAsync(WebSocket ws, object data)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data, JsonOptions.Default);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
if (ws.State == WebSocketState.Open)
|
||||
{
|
||||
await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ServeStaticFileAsync(HttpListenerResponse response, string filename, string contentType)
|
||||
{
|
||||
var content = GetEmbeddedResource(filename);
|
||||
if (content == null)
|
||||
{
|
||||
response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
response.ContentType = contentType + "; charset=utf-8";
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
response.ContentLength64 = bytes.Length;
|
||||
await response.OutputStream.WriteAsync(bytes);
|
||||
}
|
||||
|
||||
private string? GetEmbeddedResource(string filename)
|
||||
{
|
||||
// Embedded resources z AdminResources třídy
|
||||
return filename switch
|
||||
{
|
||||
"admin.html" => AdminResources.Html,
|
||||
"admin.css" => AdminResources.Css,
|
||||
"admin.js" => AdminResources.JavaScript,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class AdminSession
|
||||
{
|
||||
public DateTime LastActivity { get; set; }
|
||||
}
|
||||
|
||||
public class SpectatorConnection
|
||||
{
|
||||
public required WebSocket WebSocket { get; set; }
|
||||
public required string LobbyId { get; set; }
|
||||
public required string SessionId { get; set; }
|
||||
}
|
||||
2374
AdminResources.cs
Normal file
2374
AdminResources.cs
Normal file
File diff suppressed because it is too large
Load Diff
192
AntiCheat.cs
Normal file
192
AntiCheat.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// Anti-cheat validace pohybu
|
||||
public class AntiCheat
|
||||
{
|
||||
private readonly ServerConfig _config;
|
||||
private readonly ILogger _logger;
|
||||
private const int PositionHistorySize = 20;
|
||||
|
||||
public AntiCheat(ServerConfig config, ILogger logger)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// Validuje pohyb hráče a vrací případné cheat violation
|
||||
public CheatViolation? ValidateMovement(Player player, Position newPosition)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var timeSinceLastUpdate = (now - player.LastPositionUpdate).TotalSeconds;
|
||||
|
||||
// Příliš krátký interval ignorujeme
|
||||
if (timeSinceLastUpdate < 0.1)
|
||||
return null;
|
||||
|
||||
var distance = player.Position.DistanceTo(newPosition);
|
||||
|
||||
// Teleport detekce
|
||||
if (distance > _config.TeleportThresholdMeters)
|
||||
{
|
||||
var violation = new CheatViolation
|
||||
{
|
||||
Type = CheatViolationType.Teleport,
|
||||
Description = $"Teleport detekován: {distance:F1}m za {timeSinceLastUpdate:F2}s",
|
||||
Severity = 10
|
||||
};
|
||||
|
||||
ApplyViolation(player, violation);
|
||||
return violation;
|
||||
}
|
||||
|
||||
// Speed check
|
||||
var speed = distance / timeSinceLastUpdate;
|
||||
var maxAllowedSpeed = _config.MaxSpeedMps * 1.5; // 50% tolerance
|
||||
|
||||
if (speed > maxAllowedSpeed)
|
||||
{
|
||||
var violation = new CheatViolation
|
||||
{
|
||||
Type = CheatViolationType.SpeedHack,
|
||||
Description = $"Podezřelá rychlost: {speed:F1}m/s (max {_config.MaxSpeedMps}m/s)",
|
||||
Severity = 3
|
||||
};
|
||||
|
||||
ApplyViolation(player, violation);
|
||||
return violation;
|
||||
}
|
||||
|
||||
// Historie pro detekci pattern
|
||||
UpdatePositionHistory(player, newPosition, now);
|
||||
|
||||
// Validace historie - průměrná rychlost za delší období
|
||||
var historyViolation = ValidatePositionHistory(player);
|
||||
if (historyViolation != null)
|
||||
{
|
||||
ApplyViolation(player, historyViolation);
|
||||
return historyViolation;
|
||||
}
|
||||
|
||||
// Pokud je OK, pomalu snižujeme cheat score
|
||||
if (player.CheatScore > 0 && timeSinceLastUpdate > 1)
|
||||
{
|
||||
player.CheatScore = Math.Max(0, player.CheatScore - 1);
|
||||
UpdateCheatStatus(player);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ApplyViolation(Player player, CheatViolation violation)
|
||||
{
|
||||
player.CheatScore += violation.Severity;
|
||||
|
||||
var previousStatus = player.CheatStatus;
|
||||
UpdateCheatStatus(player);
|
||||
|
||||
if (player.CheatStatus != previousStatus)
|
||||
{
|
||||
_logger.LogWarning("Cheat status změněn: {Player} -> {Status} (score: {Score})",
|
||||
player.ClientUuid, player.CheatStatus, player.CheatScore);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Cheat violation: {Player} - {Type}: {Description}",
|
||||
player.ClientUuid, violation.Type, violation.Description);
|
||||
}
|
||||
|
||||
private void UpdateCheatStatus(Player player)
|
||||
{
|
||||
player.CheatStatus = player.CheatScore switch
|
||||
{
|
||||
>= 50 => CheatStatus.Kicked,
|
||||
>= 25 => CheatStatus.Restrict,
|
||||
>= 10 => CheatStatus.Warn,
|
||||
_ => CheatStatus.Ok
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdatePositionHistory(Player player, Position position, DateTime time)
|
||||
{
|
||||
player.PositionHistory.Enqueue((position, time));
|
||||
|
||||
while (player.PositionHistory.Count > PositionHistorySize)
|
||||
{
|
||||
player.PositionHistory.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
private CheatViolation? ValidatePositionHistory(Player player)
|
||||
{
|
||||
if (player.PositionHistory.Count < 5)
|
||||
return null;
|
||||
|
||||
var history = player.PositionHistory.ToArray();
|
||||
var first = history[0];
|
||||
var last = history[^1];
|
||||
|
||||
var totalTime = (last.Time - first.Time).TotalSeconds;
|
||||
if (totalTime < _config.MovementValidationWindowSec)
|
||||
return null;
|
||||
|
||||
// Spočítáme celkovou ujetou vzdálenost
|
||||
double totalDistance = 0;
|
||||
for (int i = 1; i < history.Length; i++)
|
||||
{
|
||||
totalDistance += history[i - 1].Pos.DistanceTo(history[i].Pos);
|
||||
}
|
||||
|
||||
var avgSpeed = totalDistance / totalTime;
|
||||
|
||||
// Pokud průměrná rychlost za celé období překračuje limit
|
||||
if (avgSpeed > _config.MaxSpeedMps * 1.2)
|
||||
{
|
||||
return new CheatViolation
|
||||
{
|
||||
Type = CheatViolationType.SustainedSpeedHack,
|
||||
Description = $"Dlouhodobě vysoká rychlost: {avgSpeed:F1}m/s za {totalTime:F0}s",
|
||||
Severity = 5
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validace akce - zda hráč může provést danou akci
|
||||
public bool CanPerformAction(Player player, string actionType)
|
||||
{
|
||||
if (player.CheatStatus == CheatStatus.Kicked)
|
||||
return false;
|
||||
|
||||
if (player.CheatStatus == CheatStatus.Restrict)
|
||||
{
|
||||
// Omezené akce pro podezřelé hráče
|
||||
var restrictedActions = new[] { "TaskComplete", "Kill", "EmergencyMeeting" };
|
||||
if (restrictedActions.Contains(actionType))
|
||||
{
|
||||
_logger.LogWarning("Akce {Action} blokována pro hráče {Player} (Restrict status)",
|
||||
actionType, player.ClientUuid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class CheatViolation
|
||||
{
|
||||
public CheatViolationType Type { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public int Severity { get; set; }
|
||||
}
|
||||
|
||||
public enum CheatViolationType
|
||||
{
|
||||
SpeedHack,
|
||||
Teleport,
|
||||
SustainedSpeedHack,
|
||||
ActionSpam,
|
||||
InvalidAction
|
||||
}
|
||||
171
Config.cs
Normal file
171
Config.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
// Konfigurace serveru - všechny herní konstanty
|
||||
public class ServerConfig
|
||||
{
|
||||
// Síťové
|
||||
public int TcpPort { get; set; } = 7777;
|
||||
public int HttpPort { get; set; } = 8088;
|
||||
public int MaxPacketSizeBytes { get; set; } = 1048576; // 1MB
|
||||
|
||||
// Timing
|
||||
public int TickMs { get; set; } = 200;
|
||||
public int PositionBroadcastRateMs { get; set; } = 1000;
|
||||
|
||||
// Pohyb & Anti-Cheat
|
||||
public double MaxSpeedMps { get; set; } = 12.0;
|
||||
public double MovementValidationWindowSec { get; set; } = 5.0;
|
||||
public double TeleportThresholdMeters { get; set; } = 50.0;
|
||||
public int CheatScoreWarnThreshold { get; set; } = 10;
|
||||
public int CheatScoreRestrictThreshold { get; set; } = 25;
|
||||
public int CheatScoreKickThreshold { get; set; } = 50;
|
||||
|
||||
// Zabíjení
|
||||
public double KillDistanceM { get; set; } = 10.0;
|
||||
public int KillCooldownMs { get; set; } = 30000;
|
||||
|
||||
// Meetingy
|
||||
public double MeetingArrivalRadiusM { get; set; } = 15.0;
|
||||
public int ArrivalBaseMs { get; set; } = 30000;
|
||||
public int ArrivalSafetyMarginMs { get; set; } = 500;
|
||||
public int AllowedLateMs { get; set; } = 2000;
|
||||
public int DiscussionPhaseMs { get; set; } = 30000;
|
||||
public int VotingPhaseMs { get; set; } = 45000;
|
||||
|
||||
// Emergency meeting
|
||||
public int EmergencyMeetingCooldownMs { get; set; } = 60000;
|
||||
public int MaxEmergencyMeetingsPerPlayer { get; set; } = 1;
|
||||
public double EmergencyMeetingCallRadiusM { get; set; } = 15.0; // Vzdálenost od středu mapy pro svolání emergency meeting
|
||||
public double ReportDistanceM { get; set; } = 5.0;
|
||||
|
||||
// Tasky
|
||||
public double TaskStartDistanceM { get; set; } = 3.0;
|
||||
public int TaskLeaveDebounceMs { get; set; } = 2000;
|
||||
public int TaskProgressKeepaliveMs { get; set; } = 5000;
|
||||
|
||||
// Persistence
|
||||
public int SnapshotEvents { get; set; } = 200;
|
||||
public int SnapshotIntervalMs { get; set; } = 300000;
|
||||
public int WalMaxSizeMb { get; set; } = 10;
|
||||
public string DataPath { get; set; } = "data";
|
||||
|
||||
// Host Migration
|
||||
public int HostTimeoutMs { get; set; } = 15000;
|
||||
public int ReconnectWindowMs { get; set; } = 60000;
|
||||
|
||||
// Lobby
|
||||
public int IdleLobbyTtlMs { get; set; } = 3600000;
|
||||
public int JoinCodeTtlMs { get; set; } = 86400000; // 24h
|
||||
public int MaxPlayersPerLobby { get; set; } = 15;
|
||||
public int JoinRateLimitPerMinute { get; set; } = 10;
|
||||
|
||||
// Security
|
||||
public int SessionKeySizeBytes { get; set; } = 32; // AES-256
|
||||
public int RsaKeySizeBits { get; set; } = 2048;
|
||||
|
||||
// Stats API
|
||||
public int StatsApiRateLimit { get; set; } = 100; // per minute per IP
|
||||
public string? StatsApiKey { get; set; } = null;
|
||||
|
||||
// Hra - výchozí hodnoty pro nové lobby
|
||||
public int DefaultImpostorCount { get; set; } = 1;
|
||||
public int DefaultTaskCount { get; set; } = 5;
|
||||
public TiePolicy DefaultTiePolicy { get; set; } = TiePolicy.NoEject;
|
||||
|
||||
// Sabotage system
|
||||
public int SabotageCooldownMs { get; set; } = 30000; // 30s between sabotages
|
||||
public int CommsBlackoutDurationMs { get; set; } = 30000; // 30s max duration (or until repaired)
|
||||
public int CriticalMeltdownDeadlineMs { get; set; } = 45000; // 45s to fix or impostor wins
|
||||
public double RepairStationDistanceM { get; set; } = 5.0; // Must be within 5m to repair
|
||||
public int RepairStationHoldMs { get; set; } = 3000; // Must hold for 3s to complete repair
|
||||
public int SimultaneousRepairWindowMs { get; set; } = 5000; // 5s window for both stations to be repaired
|
||||
|
||||
// Overpass API (Map Data)
|
||||
public string OverpassApiUrl { get; set; } = "https://overpass-api.de/api/interpreter";
|
||||
public int OverpassTimeoutSec { get; set; } = 30;
|
||||
public int OverpassCacheMaxEntries { get; set; } = 100;
|
||||
public bool OverpassEnabled { get; set; } = true;
|
||||
public double MinTaskSpacingMeters { get; set; } = 20.0; // Minimum spacing between tasks
|
||||
|
||||
public static ServerConfig Load(string path)
|
||||
{
|
||||
ServerConfig config;
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
config = JsonSerializer.Deserialize<ServerConfig>(json, JsonOptions.Default) ?? new ServerConfig();
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new ServerConfig();
|
||||
}
|
||||
|
||||
// Override z environment variables (pro Docker)
|
||||
config.ApplyEnvironmentOverrides();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aplikuje override hodnot z environment variables.
|
||||
/// Prefix: GEOSUS_
|
||||
/// Příklady: GEOSUS_TCP_PORT=7777, GEOSUS_HTTP_PORT=8088
|
||||
/// </summary>
|
||||
private void ApplyEnvironmentOverrides()
|
||||
{
|
||||
// Síťové
|
||||
if (int.TryParse(Environment.GetEnvironmentVariable("GEOSUS_TCP_PORT"), out var tcpPort))
|
||||
TcpPort = tcpPort;
|
||||
if (int.TryParse(Environment.GetEnvironmentVariable("GEOSUS_HTTP_PORT"), out var httpPort))
|
||||
HttpPort = httpPort;
|
||||
|
||||
// Data path
|
||||
var dataPath = Environment.GetEnvironmentVariable("GEOSUS_DATA_PATH");
|
||||
if (!string.IsNullOrEmpty(dataPath))
|
||||
DataPath = dataPath;
|
||||
|
||||
// Overpass API
|
||||
var overpassUrl = Environment.GetEnvironmentVariable("GEOSUS_OVERPASS_URL");
|
||||
if (!string.IsNullOrEmpty(overpassUrl))
|
||||
OverpassApiUrl = overpassUrl;
|
||||
|
||||
// Stats API key
|
||||
var apiKey = Environment.GetEnvironmentVariable("GEOSUS_API_KEY");
|
||||
if (!string.IsNullOrEmpty(apiKey))
|
||||
StatsApiKey = apiKey;
|
||||
}
|
||||
|
||||
public void Save(string path)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(this, JsonOptions.Indented);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TiePolicy
|
||||
{
|
||||
NoEject,
|
||||
Random,
|
||||
EjectLowestId
|
||||
}
|
||||
|
||||
public static class JsonOptions
|
||||
{
|
||||
public static readonly JsonSerializerOptions Default = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public static readonly JsonSerializerOptions Indented = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
WriteIndented = true
|
||||
};
|
||||
}
|
||||
180
DOCKER.md
Normal file
180
DOCKER.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# GeoSus Server - Docker Deployment
|
||||
|
||||
## Rychlý start
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
docker build -t geosus-server .
|
||||
|
||||
# Spusť kontejner
|
||||
docker run -d \
|
||||
--name geosus \
|
||||
-p 7777:7777 \
|
||||
-p 8088:8088 \
|
||||
-v geosus-data:/app/data \
|
||||
geosus-server
|
||||
```
|
||||
|
||||
## Docker Compose (doporučeno)
|
||||
|
||||
```bash
|
||||
# Spuštění
|
||||
docker-compose up -d
|
||||
|
||||
# Sledování logů
|
||||
docker-compose logs -f
|
||||
|
||||
# Zastavení
|
||||
docker-compose down
|
||||
|
||||
# Zastavení včetně smazání dat
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Porty
|
||||
|
||||
| Port | Protokol | Popis |
|
||||
|------|----------|-------|
|
||||
| 7777 | TCP | Herní komunikace (binární protokol s šifrováním) |
|
||||
| 8088 | HTTP | Stats REST API |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Proměnná | Výchozí | Popis |
|
||||
|----------|---------|-------|
|
||||
| `GEOSUS_TCP_PORT` | 7777 | TCP port pro herní komunikaci |
|
||||
| `GEOSUS_HTTP_PORT` | 8088 | HTTP port pro Stats API |
|
||||
| `GEOSUS_DATA_PATH` | data | Cesta k datové složce |
|
||||
| `GEOSUS_OVERPASS_URL` | https://mapz.honzuvkod.dev/api/interpreter | URL Overpass API pro mapová data |
|
||||
| `GEOSUS_API_KEY` | (none) | API klíč pro Stats API (volitelné) |
|
||||
| `DOTNET_ENVIRONMENT` | Production | Prostředí (.NET) |
|
||||
|
||||
## Volumes
|
||||
|
||||
- `/app/data` - Herní data (lobby, statistiky SQLite)
|
||||
|
||||
## Health Check
|
||||
|
||||
Server poskytuje health endpoint:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8088/health
|
||||
```
|
||||
|
||||
Odpověď:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"uptimeSeconds": 3600,
|
||||
"activeLobbies": 2,
|
||||
"connectedPlayers": 15
|
||||
}
|
||||
```
|
||||
|
||||
## Vlastní konfigurace
|
||||
|
||||
Můžeš připojit vlastní `appsettings.json`:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name geosus \
|
||||
-p 7777:7777 \
|
||||
-p 8088:8088 \
|
||||
-v geosus-data:/app/data \
|
||||
-v ./my-settings.json:/app/appsettings.json:ro \
|
||||
geosus-server
|
||||
```
|
||||
|
||||
## Logování
|
||||
|
||||
```bash
|
||||
# Živé logy
|
||||
docker logs -f geosus
|
||||
|
||||
# Posledních 100 řádků
|
||||
docker logs --tail 100 geosus
|
||||
```
|
||||
|
||||
## Firewall
|
||||
|
||||
Nezapomeň otevřít porty na firewallu:
|
||||
|
||||
```bash
|
||||
# UFW (Ubuntu)
|
||||
sudo ufw allow 7777/tcp
|
||||
sudo ufw allow 8088/tcp
|
||||
|
||||
# firewalld (CentOS/RHEL)
|
||||
sudo firewall-cmd --permanent --add-port=7777/tcp
|
||||
sudo firewall-cmd --permanent --add-port=8088/tcp
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Reverse Proxy (volitelné)
|
||||
|
||||
Pro HTTPS můžeš použít nginx jako reverse proxy:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name geosus.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# Stats API
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8088/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
# TCP proxy pro herní komunikaci (stream modul)
|
||||
stream {
|
||||
server {
|
||||
listen 7777;
|
||||
proxy_pass localhost:7777;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Aktualizace
|
||||
|
||||
```bash
|
||||
# Stáhni nový kód
|
||||
git pull
|
||||
|
||||
# Rebuild a restart
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port je obsazený
|
||||
```bash
|
||||
# Zjisti co používá port
|
||||
sudo netstat -tlnp | grep 7777
|
||||
sudo lsof -i :7777
|
||||
```
|
||||
|
||||
### Kontejner padá
|
||||
```bash
|
||||
# Zkontroluj logy
|
||||
docker logs geosus
|
||||
|
||||
# Interaktivní shell
|
||||
docker run -it --rm geosus-server /bin/bash
|
||||
```
|
||||
|
||||
### Problémy s daty
|
||||
```bash
|
||||
# Zkontroluj volume
|
||||
docker volume inspect geosus-data
|
||||
|
||||
# Backup dat
|
||||
docker cp geosus:/app/data ./backup
|
||||
```
|
||||
58
Dockerfile
Normal file
58
Dockerfile
Normal file
@@ -0,0 +1,58 @@
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# GeoSus Server Dockerfile
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Multi-stage build pro optimální velikost image
|
||||
#
|
||||
# Build: docker build -t geosus-server .
|
||||
# Run: docker run -d -p 7777:7777 -p 8088:8088 --name geosus geosus-server
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STAGE 1: Build
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Kopíruj pouze csproj pro využití cache vrstev
|
||||
COPY Server.csproj ./
|
||||
RUN dotnet restore
|
||||
|
||||
# Kopíruj zbytek zdrojáků a builduj
|
||||
COPY . ./
|
||||
RUN dotnet publish -c Release -o /app
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STAGE 2: Runtime
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Vytvoř uživatele bez root práv
|
||||
RUN groupadd -r geosus && useradd -r -g geosus geosus
|
||||
|
||||
# Kopíruj build output
|
||||
COPY --from=build /app ./
|
||||
|
||||
# Vytvoř složku pro data s právy
|
||||
RUN mkdir -p /app/data && chown -R geosus:geosus /app
|
||||
|
||||
# Přepni na non-root uživatele
|
||||
USER geosus
|
||||
|
||||
# Exponuj porty
|
||||
# TCP 7777 - Herní komunikace (binární protokol)
|
||||
# HTTP 8088 - Stats REST API
|
||||
EXPOSE 7777
|
||||
EXPOSE 8088
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8088/health || exit 1
|
||||
|
||||
# Environment variables pro konfiguraci
|
||||
ENV DOTNET_ENVIRONMENT=Production
|
||||
ENV GEOSUS_TCP_PORT=7777
|
||||
ENV GEOSUS_HTTP_PORT=8088
|
||||
|
||||
# Spuštění
|
||||
ENTRYPOINT ["dotnet", "Server.dll"]
|
||||
181
Encryption.cs
Normal file
181
Encryption.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
// Šifrování komunikace - RSA handshake + AES-256-CBC session (kompatibilní s Unity)
|
||||
public class ServerEncryption : IDisposable
|
||||
{
|
||||
private readonly RSA _rsa;
|
||||
private readonly string _publicKeyPem;
|
||||
|
||||
public ServerEncryption(int keySizeBits = 2048)
|
||||
{
|
||||
_rsa = RSA.Create(keySizeBits);
|
||||
_publicKeyPem = ExportPublicKeyPem();
|
||||
}
|
||||
|
||||
public string PublicKeyPem => _publicKeyPem;
|
||||
|
||||
// Dešifruje session key od klienta
|
||||
public (byte[] Key, byte[] IV) DecryptSessionKey(string encryptedKeyBase64, string encryptedIvBase64)
|
||||
{
|
||||
var encryptedKey = Convert.FromBase64String(encryptedKeyBase64);
|
||||
var encryptedIv = Convert.FromBase64String(encryptedIvBase64);
|
||||
|
||||
// Používáme OaepSHA1 pro Unity kompatibilitu
|
||||
var key = _rsa.Decrypt(encryptedKey, RSAEncryptionPadding.OaepSHA1);
|
||||
var iv = _rsa.Decrypt(encryptedIv, RSAEncryptionPadding.OaepSHA1);
|
||||
|
||||
return (key, iv);
|
||||
}
|
||||
|
||||
private string ExportPublicKeyPem()
|
||||
{
|
||||
var publicKey = _rsa.ExportSubjectPublicKeyInfo();
|
||||
var base64 = Convert.ToBase64String(publicKey);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("-----BEGIN PUBLIC KEY-----");
|
||||
for (int i = 0; i < base64.Length; i += 64)
|
||||
{
|
||||
sb.AppendLine(base64.Substring(i, Math.Min(64, base64.Length - i)));
|
||||
}
|
||||
sb.AppendLine("-----END PUBLIC KEY-----");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_rsa.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Session šifrování pro konkrétní klientské spojení - AES-256-CBC + HMAC-SHA256
|
||||
public class SessionCrypto : IDisposable
|
||||
{
|
||||
private readonly byte[] _key;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public SessionCrypto(byte[] key, byte[] iv)
|
||||
{
|
||||
if (key.Length != 32)
|
||||
throw new ArgumentException("Key musí být 32 bajtů pro AES-256");
|
||||
if (iv.Length != 16)
|
||||
throw new ArgumentException("IV musí být 16 bajtů pro AES-CBC");
|
||||
|
||||
_key = key;
|
||||
// IV se nepoužívá přímo - každá zpráva má svůj unikátní IV
|
||||
}
|
||||
|
||||
// Šifruje zprávu s AES-CBC a HMAC
|
||||
public byte[] Encrypt(byte[] plaintext)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.GenerateIV(); // Generujeme nový IV pro každou zprávu
|
||||
|
||||
byte[] ciphertext;
|
||||
using (var encryptor = aes.CreateEncryptor())
|
||||
{
|
||||
ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
|
||||
}
|
||||
|
||||
// Počítáme HMAC přes IV + ciphertext (používáme AES klíč pro HMAC)
|
||||
byte[] hmac;
|
||||
using (var hmacSha = new HMACSHA256(_key))
|
||||
{
|
||||
var dataToSign = new byte[aes.IV.Length + ciphertext.Length];
|
||||
Buffer.BlockCopy(aes.IV, 0, dataToSign, 0, aes.IV.Length);
|
||||
Buffer.BlockCopy(ciphertext, 0, dataToSign, aes.IV.Length, ciphertext.Length);
|
||||
hmac = hmacSha.ComputeHash(dataToSign);
|
||||
}
|
||||
|
||||
// Výstup: [16 bytes IV][32 bytes HMAC][ciphertext]
|
||||
var result = new byte[16 + 32 + ciphertext.Length];
|
||||
Buffer.BlockCopy(aes.IV, 0, result, 0, 16);
|
||||
Buffer.BlockCopy(hmac, 0, result, 16, 32);
|
||||
Buffer.BlockCopy(ciphertext, 0, result, 48, ciphertext.Length);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Dešifruje zprávu a validuje HMAC
|
||||
public byte[]? Decrypt(byte[] encrypted)
|
||||
{
|
||||
if (encrypted.Length < 48) return null; // 16 IV + 32 HMAC + min data
|
||||
|
||||
try
|
||||
{
|
||||
var iv = new byte[16];
|
||||
var receivedHmac = new byte[32];
|
||||
var ciphertext = new byte[encrypted.Length - 48];
|
||||
|
||||
Buffer.BlockCopy(encrypted, 0, iv, 0, 16);
|
||||
Buffer.BlockCopy(encrypted, 16, receivedHmac, 0, 32);
|
||||
Buffer.BlockCopy(encrypted, 48, ciphertext, 0, ciphertext.Length);
|
||||
|
||||
// Ověříme HMAC (používáme AES klíč pro HMAC)
|
||||
byte[] computedHmac;
|
||||
using (var hmacSha = new HMACSHA256(_key))
|
||||
{
|
||||
var dataToVerify = new byte[16 + ciphertext.Length];
|
||||
Buffer.BlockCopy(iv, 0, dataToVerify, 0, 16);
|
||||
Buffer.BlockCopy(ciphertext, 0, dataToVerify, 16, ciphertext.Length);
|
||||
computedHmac = hmacSha.ComputeHash(dataToVerify);
|
||||
}
|
||||
|
||||
// Constant-time porovnání
|
||||
if (!CryptographicOperations.FixedTimeEquals(receivedHmac, computedHmac))
|
||||
{
|
||||
return null; // HMAC mismatch - zpráva byla změněna
|
||||
}
|
||||
|
||||
// Dešifrujeme
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor();
|
||||
return decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Array.Clear(_key, 0, _key.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Klientská strana - generuje session key a šifruje ho RSA public key
|
||||
public static class ClientEncryptionHelper
|
||||
{
|
||||
public static (byte[] Key, byte[] IV) GenerateSessionKey()
|
||||
{
|
||||
var key = RandomNumberGenerator.GetBytes(32); // AES-256
|
||||
var iv = RandomNumberGenerator.GetBytes(16); // AES-CBC IV (16 bytes)
|
||||
return (key, iv);
|
||||
}
|
||||
|
||||
public static (string EncryptedKey, string EncryptedIV) EncryptSessionKey(
|
||||
string rsaPublicKeyPem, byte[] key, byte[] iv)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(rsaPublicKeyPem);
|
||||
|
||||
var encryptedKey = rsa.Encrypt(key, RSAEncryptionPadding.OaepSHA256);
|
||||
var encryptedIv = rsa.Encrypt(iv, RSAEncryptionPadding.OaepSHA256);
|
||||
|
||||
return (Convert.ToBase64String(encryptedKey), Convert.ToBase64String(encryptedIv));
|
||||
}
|
||||
}
|
||||
508
GameLogic.cs
Normal file
508
GameLogic.cs
Normal file
@@ -0,0 +1,508 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading.Channels;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
// Herní logika - kill, meeting, voting, tasks
|
||||
public class GameLogic
|
||||
{
|
||||
private readonly ServerConfig _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly OverpassService? _overpassService;
|
||||
private readonly Random _random = new Random(); // Sdílená instance pro lepší náhodnost
|
||||
|
||||
public GameLogic(ServerConfig config, ILogger logger, OverpassService? overpassService = null)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_overpassService = overpassService;
|
||||
}
|
||||
|
||||
#region Zabíjení
|
||||
|
||||
public (bool Success, string? Error, Body? Body) TryKill(
|
||||
Player killer, Player victim, Dictionary<string, Player> players, List<Body> bodies)
|
||||
{
|
||||
// Validace - killer musí být impostor a alive
|
||||
if (killer.Role != PlayerRole.Impostor)
|
||||
return (false, "Nejsi impostor", null);
|
||||
|
||||
if (killer.State != PlayerState.Alive)
|
||||
return (false, "Jsi mrtvý", null);
|
||||
|
||||
if (victim.State != PlayerState.Alive)
|
||||
return (false, "Oběť je již mrtvá", null);
|
||||
|
||||
// Cooldown kontrola
|
||||
if (killer.LastKillTime.HasValue)
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - killer.LastKillTime.Value).TotalMilliseconds;
|
||||
if (elapsed < _config.KillCooldownMs)
|
||||
{
|
||||
var remaining = (_config.KillCooldownMs - elapsed) / 1000;
|
||||
return (false, $"Cooldown: {remaining:F1}s", null);
|
||||
}
|
||||
}
|
||||
|
||||
// Vzdálenost
|
||||
var distance = killer.Position.DistanceTo(victim.Position);
|
||||
if (distance > _config.KillDistanceM)
|
||||
return (false, $"Příliš daleko ({distance:F1}m)", null);
|
||||
|
||||
// Kill úspěšný
|
||||
victim.State = PlayerState.Dead;
|
||||
killer.LastKillTime = DateTime.UtcNow;
|
||||
|
||||
var body = new Body
|
||||
{
|
||||
BodyId = Guid.NewGuid().ToString("N")[..8],
|
||||
VictimId = victim.ClientUuid,
|
||||
KillerId = killer.ClientUuid,
|
||||
Location = victim.Position,
|
||||
KilledAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
bodies.Add(body);
|
||||
|
||||
_logger.LogInformation("Kill: {Killer} zabil {Victim}, body {BodyId}",
|
||||
killer.ClientUuid, victim.ClientUuid, body.BodyId);
|
||||
|
||||
return (true, null, body);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reporty a meetingy
|
||||
|
||||
public (bool Success, string? Error, Body? Body) TryReportBody(
|
||||
Player reporter, string bodyId, List<Body> bodies)
|
||||
{
|
||||
if (reporter.State != PlayerState.Alive)
|
||||
return (false, "Jsi mrtvý", null);
|
||||
|
||||
var body = bodies.FirstOrDefault(b => b.BodyId == bodyId);
|
||||
if (body == null)
|
||||
return (false, "Tělo neexistuje", null);
|
||||
|
||||
if (body.Reported)
|
||||
return (false, "Tělo již bylo reportnuto", null);
|
||||
|
||||
// Vzdálenost
|
||||
var distance = reporter.Position.DistanceTo(body.Location);
|
||||
if (distance > _config.ReportDistanceM)
|
||||
return (false, $"Příliš daleko ({distance:F1}m)", null);
|
||||
|
||||
body.Reported = true;
|
||||
body.ReportedBy = reporter.ClientUuid;
|
||||
|
||||
_logger.LogInformation("Report: {Reporter} reportnul tělo {BodyId}",
|
||||
reporter.ClientUuid, bodyId);
|
||||
|
||||
return (true, null, body);
|
||||
}
|
||||
|
||||
public (bool Success, string? Error) TryCallEmergencyMeeting(
|
||||
Player caller, int maxMeetings, int cooldownMs)
|
||||
{
|
||||
if (caller.State != PlayerState.Alive)
|
||||
return (false, "Jsi mrtvý");
|
||||
|
||||
// Limit na počet emergency meetings
|
||||
if (caller.EmergencyMeetingsUsed >= maxMeetings)
|
||||
return (false, $"Vyčerpal jsi emergency meetingy ({maxMeetings})");
|
||||
|
||||
// Cooldown
|
||||
if (caller.LastEmergencyMeetingTime.HasValue)
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - caller.LastEmergencyMeetingTime.Value).TotalMilliseconds;
|
||||
if (elapsed < cooldownMs)
|
||||
{
|
||||
var remaining = (cooldownMs - elapsed) / 1000;
|
||||
return (false, $"Cooldown: {remaining:F1}s");
|
||||
}
|
||||
}
|
||||
|
||||
caller.EmergencyMeetingsUsed++;
|
||||
caller.LastEmergencyMeetingTime = DateTime.UtcNow;
|
||||
|
||||
_logger.LogInformation("Emergency meeting: {Caller}", caller.ClientUuid);
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public Meeting StartMeeting(
|
||||
MeetingType type, string callerId, Position meetingLocation,
|
||||
string? reportedBodyId, int arrivalBaseMs, int votingPhaseMs, int discussionPhaseMs)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return new Meeting
|
||||
{
|
||||
MeetingId = Guid.NewGuid().ToString("N")[..8],
|
||||
Type = type,
|
||||
CallerId = callerId,
|
||||
ReportedBodyId = reportedBodyId,
|
||||
MeetingLocation = meetingLocation,
|
||||
StartTime = now,
|
||||
ArrivalDeadline = now.AddMilliseconds(arrivalBaseMs + _config.ArrivalSafetyMarginMs),
|
||||
DiscussionEndTime = discussionPhaseMs > 0 ? now.AddMilliseconds(arrivalBaseMs + discussionPhaseMs) : null,
|
||||
VotingEndTime = now.AddMilliseconds(arrivalBaseMs + discussionPhaseMs + votingPhaseMs)
|
||||
};
|
||||
}
|
||||
|
||||
public bool CheckPlayerArrival(Player player, Meeting meeting)
|
||||
{
|
||||
if (player.State != PlayerState.Alive)
|
||||
return false;
|
||||
|
||||
var distance = player.Position.DistanceTo(meeting.MeetingLocation);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Grace period
|
||||
var deadline = meeting.ArrivalDeadline.AddMilliseconds(_config.AllowedLateMs);
|
||||
|
||||
if (now <= deadline && distance <= _config.MeetingArrivalRadiusM)
|
||||
{
|
||||
meeting.ArrivedPlayers.Add(player.ClientUuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hlasování
|
||||
|
||||
public (bool Success, string? Error) TryCastVote(
|
||||
Player voter, string? targetId, Meeting meeting, Dictionary<string, Player> players)
|
||||
{
|
||||
if (voter.State != PlayerState.Alive)
|
||||
return (false, "Jsi mrtvý");
|
||||
|
||||
if (!meeting.ArrivedPlayers.Contains(voter.ClientUuid))
|
||||
return (false, "Nedorazil jsi včas na meeting");
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Kontrola fáze - musíme být ve voting fázi
|
||||
if (meeting.DiscussionEndTime.HasValue && now < meeting.DiscussionEndTime.Value)
|
||||
return (false, "Ještě probíhá diskuze");
|
||||
|
||||
if (now > meeting.VotingEndTime)
|
||||
return (false, "Hlasování skončilo");
|
||||
|
||||
// Rate limit na změny hlasu
|
||||
if (meeting.Votes.ContainsKey(voter.ClientUuid) && meeting.LastVoteChangeTime.HasValue)
|
||||
{
|
||||
var sinceLastChange = (now - meeting.LastVoteChangeTime.Value).TotalMilliseconds;
|
||||
if (sinceLastChange < 2000)
|
||||
return (false, "Příliš rychlá změna hlasu");
|
||||
}
|
||||
|
||||
// Validace targetu
|
||||
if (targetId != null)
|
||||
{
|
||||
if (!players.TryGetValue(targetId, out var target))
|
||||
return (false, "Neplatný cíl hlasu");
|
||||
if (target.State != PlayerState.Alive)
|
||||
return (false, "Cíl je mrtvý");
|
||||
}
|
||||
|
||||
meeting.Votes[voter.ClientUuid] = targetId;
|
||||
meeting.LastVoteChangeTime = now;
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public (string? EjectedId, bool WasTie, Dictionary<string, int> VoteCounts) ResolveVoting(
|
||||
Meeting meeting, Dictionary<string, Player> players, TiePolicy tiePolicy)
|
||||
{
|
||||
var voteCounts = new Dictionary<string, int>();
|
||||
var skipCount = 0;
|
||||
|
||||
foreach (var (voterId, targetId) in meeting.Votes)
|
||||
{
|
||||
if (targetId == null)
|
||||
{
|
||||
skipCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
voteCounts.TryAdd(targetId, 0);
|
||||
voteCounts[targetId]++;
|
||||
}
|
||||
}
|
||||
|
||||
voteCounts["__SKIP__"] = skipCount;
|
||||
|
||||
// Najdeme maximum
|
||||
var maxVotes = voteCounts.Values.Max();
|
||||
var topVoted = voteCounts.Where(kv => kv.Value == maxVotes).Select(kv => kv.Key).ToList();
|
||||
|
||||
// Kontrola remízy
|
||||
if (topVoted.Count > 1 || topVoted.Contains("__SKIP__"))
|
||||
{
|
||||
// Remíza nebo skip vyhrál
|
||||
switch (tiePolicy)
|
||||
{
|
||||
case TiePolicy.NoEject:
|
||||
return (null, topVoted.Count > 1, voteCounts);
|
||||
|
||||
case TiePolicy.Random:
|
||||
if (topVoted.Contains("__SKIP__"))
|
||||
topVoted.Remove("__SKIP__");
|
||||
if (topVoted.Count > 0)
|
||||
{
|
||||
var random = new Random();
|
||||
var ejected = topVoted[random.Next(topVoted.Count)];
|
||||
return (ejected, true, voteCounts);
|
||||
}
|
||||
return (null, true, voteCounts);
|
||||
|
||||
case TiePolicy.EjectLowestId:
|
||||
if (topVoted.Contains("__SKIP__"))
|
||||
topVoted.Remove("__SKIP__");
|
||||
if (topVoted.Count > 0)
|
||||
{
|
||||
var ejected = topVoted.OrderBy(id => id).First();
|
||||
return (ejected, true, voteCounts);
|
||||
}
|
||||
return (null, true, voteCounts);
|
||||
|
||||
default:
|
||||
return (null, topVoted.Count > 1, voteCounts);
|
||||
}
|
||||
}
|
||||
|
||||
var winner = topVoted[0];
|
||||
if (winner == "__SKIP__")
|
||||
return (null, false, voteCounts);
|
||||
|
||||
return (winner, false, voteCounts);
|
||||
}
|
||||
|
||||
public void EjectPlayer(Player player)
|
||||
{
|
||||
player.State = PlayerState.Dead;
|
||||
_logger.LogInformation("Ejected: {Player}", player.ClientUuid);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tasky
|
||||
|
||||
/// <summary>
|
||||
/// Pokusí se dokončit task. Kontroluje:
|
||||
/// - Hráč je naživu
|
||||
/// - Není impostor
|
||||
/// - Task existuje a patří hráči
|
||||
/// - Task ještě není dokončen
|
||||
/// - Hráč je dostatečně blízko
|
||||
/// Poznámka: Duchové (mrtví hráči) MOHOU plnit úkoly - to je důležité pro crew!
|
||||
/// </summary>
|
||||
public (bool Success, string? Error) TryCompleteTask(Player player, string taskId)
|
||||
{
|
||||
// Duchové mohou plnit úkoly - neblokujeme mrtvé hráče
|
||||
// Ale musí být crew role
|
||||
if (player.Role == PlayerRole.Impostor)
|
||||
return (false, "Impostoři nemohou dělat tasky");
|
||||
|
||||
var task = player.Tasks.FirstOrDefault(t => t.TaskId == taskId);
|
||||
if (task == null)
|
||||
return (false, "Tento task ti nepatří");
|
||||
|
||||
if (player.CompletedTaskIds.Contains(taskId))
|
||||
return (false, "Task již dokončen");
|
||||
|
||||
// Kontrola vzdálenosti
|
||||
var distance = player.Position.DistanceTo(task.Location);
|
||||
if (distance > _config.TaskStartDistanceM)
|
||||
return (false, $"Příliš daleko ({distance:F1}m, max {_config.TaskStartDistanceM}m)");
|
||||
|
||||
// Dokončit task
|
||||
player.CompletedTaskIds.Add(taskId);
|
||||
|
||||
_logger.LogInformation("Task completed: {Player} dokončil {Task} na pozici {Pos} (vzdálenost {Dist:F1}m)",
|
||||
player.ClientUuid, task.Name, task.Location, distance);
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Najde nejbližší nedokončený task hráče
|
||||
/// </summary>
|
||||
public GameTask? FindNearestTask(Player player, double maxDistance)
|
||||
{
|
||||
return player.Tasks
|
||||
.Where(t => !player.CompletedTaskIds.Contains(t.TaskId))
|
||||
.Where(t => player.Position.DistanceTo(t.Location) <= maxDistance)
|
||||
.OrderBy(t => player.Position.DistanceTo(t.Location))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Win conditions
|
||||
|
||||
public (bool GameOver, string? WinningFaction, string? Reason) CheckWinConditions(
|
||||
Dictionary<string, Player> players, List<GameTask> tasks)
|
||||
{
|
||||
var aliveCrew = players.Values.Count(p => p.State == PlayerState.Alive && p.Role == PlayerRole.Crew);
|
||||
var aliveImpostors = players.Values.Count(p => p.State == PlayerState.Alive && p.Role == PlayerRole.Impostor);
|
||||
|
||||
// Impostoři vyhráli - mají většinu nebo rovnost
|
||||
if (aliveImpostors >= aliveCrew && aliveCrew > 0)
|
||||
{
|
||||
return (true, "Impostor", "Impostoři mají převahu");
|
||||
}
|
||||
|
||||
// Všichni impostoři mrtví
|
||||
if (aliveImpostors == 0)
|
||||
{
|
||||
return (true, "Crew", "Všichni impostoři eliminováni");
|
||||
}
|
||||
|
||||
// Všechny tasky hotové (počítáme pouze crew tasky)
|
||||
var crewPlayers = players.Values.Where(p => p.Role == PlayerRole.Crew).ToList();
|
||||
var totalTasks = crewPlayers.Sum(p => p.Tasks.Count);
|
||||
var completedTasks = crewPlayers.Sum(p => p.CompletedTaskIds.Count);
|
||||
|
||||
if (totalTasks > 0 && completedTasks >= totalTasks)
|
||||
{
|
||||
return (true, "Crew", "Všechny tasky dokončeny");
|
||||
}
|
||||
|
||||
return (false, null, null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Role assignment
|
||||
|
||||
public void AssignRoles(List<Player> players, int impostorCount)
|
||||
{
|
||||
if (players.Count < 2)
|
||||
{
|
||||
// Minimálně 2 hráči
|
||||
foreach (var p in players)
|
||||
p.Role = PlayerRole.Crew;
|
||||
return;
|
||||
}
|
||||
|
||||
// Omezení počtu impostorů
|
||||
var maxImpostors = Math.Max(1, players.Count / 3);
|
||||
impostorCount = Math.Min(impostorCount, maxImpostors);
|
||||
impostorCount = Math.Min(impostorCount, players.Count - 1);
|
||||
|
||||
// Náhodně vybereme impostory
|
||||
var shuffled = players.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue)).ToList();
|
||||
|
||||
for (int i = 0; i < players.Count; i++)
|
||||
{
|
||||
shuffled[i].Role = i < impostorCount ? PlayerRole.Impostor : PlayerRole.Crew;
|
||||
shuffled[i].State = PlayerState.Alive;
|
||||
shuffled[i].EmergencyMeetingsUsed = 0;
|
||||
shuffled[i].LastEmergencyMeetingTime = null;
|
||||
shuffled[i].LastKillTime = null;
|
||||
shuffled[i].CompletedTaskIds.Clear();
|
||||
shuffled[i].CurrentTaskId = null;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Roles assigned: {ImpostorCount} impostorů z {TotalPlayers} hráčů",
|
||||
impostorCount, players.Count);
|
||||
}
|
||||
|
||||
public List<GameTask> GenerateTasks(int count, Position center, double radius, MapData? mapData = null, int startIndex = 0)
|
||||
{
|
||||
var tasks = new List<GameTask>();
|
||||
|
||||
var taskNames = new[] {
|
||||
"Opravit kabel", "Zkalibrovat senzor", "Stáhnout data",
|
||||
"Nabít baterii", "Vyčistit filtr", "Nastavit kompas",
|
||||
"Opravit antenu", "Zkontrolovat zásoby", "Otestovat reaktor"
|
||||
};
|
||||
|
||||
// Get task positions - use map data if available
|
||||
List<Position> positions;
|
||||
|
||||
if (mapData != null && _overpassService != null && mapData.ReachablePositions.Count >= count)
|
||||
{
|
||||
positions = _overpassService.GetPOIBasedPositions(mapData, count, _random);
|
||||
_logger.LogInformation("Generated {Count} positions for tasks using map data", positions.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
positions = GenerateRandomPositions(count, center, radius, _random);
|
||||
_logger.LogDebug("Generated {Count} positions for tasks using random fallback", positions.Count);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count && i < positions.Count; i++)
|
||||
{
|
||||
string taskId = $"task_{startIndex + i}";
|
||||
string taskName = taskNames[(startIndex + i) % taskNames.Length];
|
||||
|
||||
tasks.Add(new GameTask
|
||||
{
|
||||
TaskId = taskId,
|
||||
Name = taskName,
|
||||
Location = positions[i],
|
||||
Type = TaskType.Instant
|
||||
});
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private List<Position> GenerateRandomPositions(int count, Position center, double radius, Random random)
|
||||
{
|
||||
var positions = new List<Position>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
// Náhodná pozice v kruhu
|
||||
var angle = random.NextDouble() * 2 * Math.PI;
|
||||
var distance = random.NextDouble() * radius * 0.8; // 80% radius aby nebyly na kraji
|
||||
var lat = center.Lat + (distance / 111000) * Math.Cos(angle);
|
||||
var lon = center.Lon + (distance / (111000 * Math.Cos(center.Lat * Math.PI / 180))) * Math.Sin(angle);
|
||||
positions.Add(new Position(lat, lon));
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate repair station positions using map data for reachability
|
||||
/// </summary>
|
||||
public List<Position> GenerateRepairStationPositions(int count, Position center, double radius, MapData? mapData = null)
|
||||
{
|
||||
if (mapData != null && _overpassService != null && mapData.ReachablePositions.Count >= count)
|
||||
{
|
||||
// Get well-distributed reachable positions
|
||||
var positions = _overpassService.GetDistributedReachablePositions(
|
||||
mapData, count, _random, _config.MinTaskSpacingMeters * 2);
|
||||
|
||||
if (positions.Count >= count)
|
||||
{
|
||||
_logger.LogInformation("Generated {Count} repair station positions using map data", positions.Count);
|
||||
return positions;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: opposite ends of play area
|
||||
var result = new List<Position>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var angle = (2 * Math.PI * i) / count;
|
||||
var dist = radius * 0.7;
|
||||
var lat = center.Lat + (dist * Math.Cos(angle)) / 111000.0;
|
||||
var lon = center.Lon + (dist * Math.Sin(angle)) / (111000.0 * Math.Cos(center.Lat * Math.PI / 180));
|
||||
result.Add(new Position(lat, lon));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
1981
LobbyActor.cs
Normal file
1981
LobbyActor.cs
Normal file
File diff suppressed because it is too large
Load Diff
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>();
|
||||
}
|
||||
}
|
||||
586
OverpassService.cs
Normal file
586
OverpassService.cs
Normal file
@@ -0,0 +1,586 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Service for fetching and processing OpenStreetMap data via Overpass API
|
||||
/// </summary>
|
||||
public class OverpassService
|
||||
{
|
||||
private readonly ServerConfig _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ConcurrentDictionary<string, MapData> _cache = new();
|
||||
|
||||
public OverpassService(ServerConfig config, ILogger logger)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(config.OverpassTimeoutSec)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch map data for a circular area, with caching
|
||||
/// </summary>
|
||||
public async Task<MapData?> GetMapDataAsync(Position center, double radiusMeters)
|
||||
{
|
||||
var cacheKey = $"{center.Lat:F5}_{center.Lon:F5}_{radiusMeters:F0}";
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out var cached))
|
||||
{
|
||||
_logger.LogDebug("Map data cache hit for {CacheKey}", cacheKey);
|
||||
return cached;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mapData = await FetchMapDataAsync(center, radiusMeters);
|
||||
if (mapData != null)
|
||||
{
|
||||
_cache[cacheKey] = mapData;
|
||||
|
||||
// Clean old cache entries if too many
|
||||
if (_cache.Count > _config.OverpassCacheMaxEntries)
|
||||
{
|
||||
var oldest = _cache.Keys.First();
|
||||
_cache.TryRemove(oldest, out _);
|
||||
}
|
||||
}
|
||||
return mapData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch Overpass data for {Center}, radius {Radius}m", center, radiusMeters);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MapData?> FetchMapDataAsync(Position center, double radiusMeters)
|
||||
{
|
||||
// Build Overpass QL query
|
||||
var query = BuildOverpassQuery(center, radiusMeters);
|
||||
|
||||
_logger.LogInformation("Fetching Overpass data for center {Center}, radius {Radius}m", center, radiusMeters);
|
||||
|
||||
var response = await _httpClient.PostAsync(
|
||||
_config.OverpassApiUrl,
|
||||
new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("data", query) })
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("Overpass API returned {StatusCode}: {Content}",
|
||||
response.StatusCode, await response.Content.ReadAsStringAsync());
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var overpassResult = JsonSerializer.Deserialize<OverpassResult>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
if (overpassResult == null)
|
||||
{
|
||||
_logger.LogError("Failed to parse Overpass response");
|
||||
return null;
|
||||
}
|
||||
|
||||
return ProcessOverpassResult(overpassResult, center, radiusMeters);
|
||||
}
|
||||
|
||||
private string BuildOverpassQuery(Position center, double radiusMeters)
|
||||
{
|
||||
// Query for: roads, paths, buildings, amenities
|
||||
// We use "around" to get data within radius of center point
|
||||
return $@"
|
||||
[out:json][timeout:{_config.OverpassTimeoutSec}];
|
||||
(
|
||||
// Walkable ways (roads, paths, sidewalks)
|
||||
way[""highway""~""footway|path|pedestrian|steps|track|residential|tertiary|secondary|primary|service|living_street|cycleway|bridleway""]
|
||||
(around:{radiusMeters},{center.Lat},{center.Lon});
|
||||
|
||||
// Buildings
|
||||
way[""building""]
|
||||
(around:{radiusMeters},{center.Lat},{center.Lon});
|
||||
relation[""building""]
|
||||
(around:{radiusMeters},{center.Lat},{center.Lon});
|
||||
|
||||
// Interesting POIs for task placement
|
||||
node[""amenity""]
|
||||
(around:{radiusMeters},{center.Lat},{center.Lon});
|
||||
way[""amenity""]
|
||||
(around:{radiusMeters},{center.Lat},{center.Lon});
|
||||
|
||||
// Parks and green areas
|
||||
way[""leisure""~""park|playground|garden""]
|
||||
(around:{radiusMeters},{center.Lat},{center.Lon});
|
||||
relation[""leisure""~""park|playground|garden""]
|
||||
(around:{radiusMeters},{center.Lat},{center.Lon});
|
||||
);
|
||||
out body;
|
||||
>;
|
||||
out skel qt;
|
||||
";
|
||||
}
|
||||
|
||||
private MapData ProcessOverpassResult(OverpassResult result, Position center, double radiusMeters)
|
||||
{
|
||||
var mapData = new MapData
|
||||
{
|
||||
Center = center,
|
||||
RadiusMeters = radiusMeters,
|
||||
FetchedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Index nodes by ID for quick lookup
|
||||
var nodesById = new Dictionary<long, OverpassNode>();
|
||||
foreach (var element in result.Elements)
|
||||
{
|
||||
if (element.Type == "node" && element.Lat.HasValue && element.Lon.HasValue)
|
||||
{
|
||||
nodesById[element.Id] = new OverpassNode
|
||||
{
|
||||
Id = element.Id,
|
||||
Lat = element.Lat.Value,
|
||||
Lon = element.Lon.Value,
|
||||
Tags = element.Tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Process ways
|
||||
foreach (var element in result.Elements.Where(e => e.Type == "way"))
|
||||
{
|
||||
if (element.Nodes == null || element.Nodes.Count < 2) continue;
|
||||
|
||||
var wayNodes = new List<Position>();
|
||||
foreach (var nodeId in element.Nodes)
|
||||
{
|
||||
if (nodesById.TryGetValue(nodeId, out var node))
|
||||
{
|
||||
wayNodes.Add(new Position(node.Lat, node.Lon));
|
||||
}
|
||||
}
|
||||
|
||||
if (wayNodes.Count < 2) continue;
|
||||
|
||||
var tags = element.Tags ?? new Dictionary<string, string>();
|
||||
|
||||
// Classify way type
|
||||
if (tags.ContainsKey("building"))
|
||||
{
|
||||
mapData.Buildings.Add(new MapBuilding
|
||||
{
|
||||
Id = $"b_{element.Id}",
|
||||
Outline = wayNodes,
|
||||
BuildingType = tags.GetValueOrDefault("building", "yes"),
|
||||
Name = tags.GetValueOrDefault("name"),
|
||||
Levels = int.TryParse(tags.GetValueOrDefault("building:levels", "1"), out var l) ? l : 1
|
||||
});
|
||||
}
|
||||
else if (tags.ContainsKey("highway"))
|
||||
{
|
||||
var highwayType = tags["highway"];
|
||||
mapData.Pathways.Add(new MapPathway
|
||||
{
|
||||
Id = $"p_{element.Id}",
|
||||
Points = wayNodes,
|
||||
PathType = ClassifyPathType(highwayType),
|
||||
Name = tags.GetValueOrDefault("name"),
|
||||
IsWalkable = IsWalkableHighway(highwayType),
|
||||
Width = EstimatePathWidth(highwayType)
|
||||
});
|
||||
}
|
||||
else if (tags.ContainsKey("leisure"))
|
||||
{
|
||||
mapData.Areas.Add(new MapArea
|
||||
{
|
||||
Id = $"a_{element.Id}",
|
||||
Outline = wayNodes,
|
||||
AreaType = MapAreaType.Park,
|
||||
Name = tags.GetValueOrDefault("name")
|
||||
});
|
||||
}
|
||||
else if (tags.ContainsKey("amenity"))
|
||||
{
|
||||
// Calculate centroid for POI
|
||||
var centroid = CalculateCentroid(wayNodes);
|
||||
mapData.PointsOfInterest.Add(new MapPOI
|
||||
{
|
||||
Id = $"poi_{element.Id}",
|
||||
Position = centroid,
|
||||
PoiType = ClassifyAmenity(tags["amenity"]),
|
||||
Name = tags.GetValueOrDefault("name")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process standalone POI nodes
|
||||
foreach (var element in result.Elements.Where(e => e.Type == "node" && e.Tags != null))
|
||||
{
|
||||
var tags = element.Tags!;
|
||||
if (tags.ContainsKey("amenity") && element.Lat.HasValue && element.Lon.HasValue)
|
||||
{
|
||||
mapData.PointsOfInterest.Add(new MapPOI
|
||||
{
|
||||
Id = $"poi_{element.Id}",
|
||||
Position = new Position(element.Lat.Value, element.Lon.Value),
|
||||
PoiType = ClassifyAmenity(tags["amenity"]),
|
||||
Name = tags.GetValueOrDefault("name")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build reachability graph and compute reachable positions
|
||||
BuildReachabilityData(mapData, center, radiusMeters);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Processed map data: {Buildings} buildings, {Pathways} pathways, {POIs} POIs, {ReachablePoints} reachable points",
|
||||
mapData.Buildings.Count, mapData.Pathways.Count, mapData.PointsOfInterest.Count, mapData.ReachablePositions.Count);
|
||||
|
||||
return mapData;
|
||||
}
|
||||
|
||||
private void BuildReachabilityData(MapData mapData, Position center, double radiusMeters)
|
||||
{
|
||||
// Build a graph of walkable paths and find connected components from center
|
||||
var graph = new PathGraph();
|
||||
|
||||
// Add all walkable pathway segments to graph
|
||||
foreach (var pathway in mapData.Pathways.Where(p => p.IsWalkable))
|
||||
{
|
||||
for (int i = 0; i < pathway.Points.Count - 1; i++)
|
||||
{
|
||||
graph.AddEdge(pathway.Points[i], pathway.Points[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Find all positions reachable from center (or nearest walkable point to center)
|
||||
var startNode = graph.FindNearestNode(center);
|
||||
if (startNode == null)
|
||||
{
|
||||
_logger.LogWarning("No walkable paths found near center, using all pathway points as reachable");
|
||||
// Fallback: all pathway points are considered reachable
|
||||
foreach (var pathway in mapData.Pathways.Where(p => p.IsWalkable))
|
||||
{
|
||||
mapData.ReachablePositions.AddRange(pathway.Points);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// BFS from start node to find all reachable nodes
|
||||
var reachable = graph.GetReachableNodes(startNode.Value, radiusMeters);
|
||||
|
||||
// Filter to only include points within the play area
|
||||
foreach (var pos in reachable)
|
||||
{
|
||||
if (pos.DistanceTo(center) <= radiusMeters)
|
||||
{
|
||||
mapData.ReachablePositions.Add(pos);
|
||||
}
|
||||
}
|
||||
|
||||
// Also mark pathways as reachable or not
|
||||
foreach (var pathway in mapData.Pathways)
|
||||
{
|
||||
var reachablePoints = pathway.Points.Count(p =>
|
||||
mapData.ReachablePositions.Any(rp => rp.DistanceTo(p) < 5));
|
||||
pathway.IsFullyReachable = reachablePoints == pathway.Points.Count;
|
||||
pathway.IsPartiallyReachable = reachablePoints > 0;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Reachability analysis: {Reachable} of {Total} positions are reachable from center",
|
||||
mapData.ReachablePositions.Count,
|
||||
mapData.Pathways.Sum(p => p.Points.Count));
|
||||
}
|
||||
|
||||
private PathType ClassifyPathType(string highway)
|
||||
{
|
||||
return highway switch
|
||||
{
|
||||
"footway" or "pedestrian" or "path" => PathType.Footway,
|
||||
"steps" => PathType.Steps,
|
||||
"cycleway" => PathType.Cycleway,
|
||||
"residential" or "living_street" => PathType.Residential,
|
||||
"service" => PathType.Service,
|
||||
"tertiary" or "secondary" or "primary" => PathType.Road,
|
||||
"track" or "bridleway" => PathType.Track,
|
||||
_ => PathType.Other
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsWalkableHighway(string highway)
|
||||
{
|
||||
// Everything except motorways is considered walkable
|
||||
return highway switch
|
||||
{
|
||||
"motorway" or "motorway_link" or "trunk" or "trunk_link" => false,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private double EstimatePathWidth(string highway)
|
||||
{
|
||||
return highway switch
|
||||
{
|
||||
"footway" or "path" => 1.5,
|
||||
"pedestrian" => 5.0,
|
||||
"steps" => 2.0,
|
||||
"cycleway" => 2.0,
|
||||
"residential" => 6.0,
|
||||
"living_street" => 5.0,
|
||||
"service" => 4.0,
|
||||
"tertiary" => 7.0,
|
||||
"secondary" => 9.0,
|
||||
"primary" => 11.0,
|
||||
_ => 3.0
|
||||
};
|
||||
}
|
||||
|
||||
private MapPOIType ClassifyAmenity(string amenity)
|
||||
{
|
||||
return amenity switch
|
||||
{
|
||||
"cafe" or "restaurant" or "fast_food" or "bar" or "pub" => MapPOIType.FoodDrink,
|
||||
"bank" or "atm" => MapPOIType.Finance,
|
||||
"pharmacy" or "hospital" or "clinic" or "doctors" => MapPOIType.Health,
|
||||
"school" or "university" or "library" => MapPOIType.Education,
|
||||
"fuel" or "parking" => MapPOIType.Transport,
|
||||
"shop" or "supermarket" or "convenience" => MapPOIType.Shop,
|
||||
"toilets" or "bench" or "drinking_water" => MapPOIType.Amenity,
|
||||
_ => MapPOIType.Other
|
||||
};
|
||||
}
|
||||
|
||||
private Position CalculateCentroid(List<Position> points)
|
||||
{
|
||||
if (points.Count == 0) return new Position(0, 0);
|
||||
var lat = points.Average(p => p.Lat);
|
||||
var lon = points.Average(p => p.Lon);
|
||||
return new Position(lat, lon);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a random reachable position suitable for placing a task or repair station
|
||||
/// </summary>
|
||||
public Position? GetRandomReachablePosition(MapData mapData, Random random, double minDistFromCenter = 0)
|
||||
{
|
||||
var candidates = mapData.ReachablePositions
|
||||
.Where(p => p.DistanceTo(mapData.Center) >= minDistFromCenter)
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
// Fallback to any pathway point
|
||||
candidates = mapData.Pathways
|
||||
.Where(p => p.IsWalkable)
|
||||
.SelectMany(p => p.Points)
|
||||
.Where(p => p.DistanceTo(mapData.Center) <= mapData.RadiusMeters)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (candidates.Count == 0)
|
||||
return null;
|
||||
|
||||
return candidates[random.Next(candidates.Count)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get multiple well-distributed reachable positions (e.g., for placing multiple tasks)
|
||||
/// </summary>
|
||||
public List<Position> GetDistributedReachablePositions(MapData mapData, int count, Random random, double minSpacing = 20)
|
||||
{
|
||||
var result = new List<Position>();
|
||||
var available = mapData.ReachablePositions.ToList();
|
||||
|
||||
// Shuffle available positions
|
||||
for (int i = available.Count - 1; i > 0; i--)
|
||||
{
|
||||
int j = random.Next(i + 1);
|
||||
(available[i], available[j]) = (available[j], available[i]);
|
||||
}
|
||||
|
||||
foreach (var pos in available)
|
||||
{
|
||||
if (result.Count >= count) break;
|
||||
|
||||
// Check minimum spacing from already selected positions
|
||||
bool tooClose = result.Any(r => r.DistanceTo(pos) < minSpacing);
|
||||
if (!tooClose)
|
||||
{
|
||||
result.Add(pos);
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't find enough spaced positions, fill with any available
|
||||
if (result.Count < count)
|
||||
{
|
||||
foreach (var pos in available)
|
||||
{
|
||||
if (result.Count >= count) break;
|
||||
if (!result.Contains(pos))
|
||||
{
|
||||
result.Add(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get positions near POIs (better for task placement as they have semantic meaning)
|
||||
/// </summary>
|
||||
public List<Position> GetPOIBasedPositions(MapData mapData, int count, Random random)
|
||||
{
|
||||
var result = new List<Position>();
|
||||
|
||||
// Prefer POIs that are reachable
|
||||
var reachablePOIs = mapData.PointsOfInterest
|
||||
.Where(poi => mapData.ReachablePositions.Any(rp => rp.DistanceTo(poi.Position) < 10))
|
||||
.ToList();
|
||||
|
||||
// Shuffle
|
||||
for (int i = reachablePOIs.Count - 1; i > 0; i--)
|
||||
{
|
||||
int j = random.Next(i + 1);
|
||||
(reachablePOIs[i], reachablePOIs[j]) = (reachablePOIs[j], reachablePOIs[i]);
|
||||
}
|
||||
|
||||
foreach (var poi in reachablePOIs.Take(count))
|
||||
{
|
||||
result.Add(poi.Position);
|
||||
}
|
||||
|
||||
// Fill remaining with distributed positions
|
||||
if (result.Count < count)
|
||||
{
|
||||
var additional = GetDistributedReachablePositions(mapData, count - result.Count, random);
|
||||
result.AddRange(additional);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
#region Overpass API Response Types
|
||||
|
||||
internal class OverpassResult
|
||||
{
|
||||
[JsonPropertyName("elements")]
|
||||
public List<OverpassElement> Elements { get; set; } = new();
|
||||
}
|
||||
|
||||
internal class OverpassElement
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[JsonPropertyName("lat")]
|
||||
public double? Lat { get; set; }
|
||||
|
||||
[JsonPropertyName("lon")]
|
||||
public double? Lon { get; set; }
|
||||
|
||||
[JsonPropertyName("nodes")]
|
||||
public List<long>? Nodes { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
public Dictionary<string, string>? Tags { get; set; }
|
||||
}
|
||||
|
||||
internal class OverpassNode
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public double Lat { get; set; }
|
||||
public double Lon { get; set; }
|
||||
public Dictionary<string, string>? Tags { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Path Graph for Reachability
|
||||
|
||||
internal class PathGraph
|
||||
{
|
||||
private readonly Dictionary<Position, HashSet<Position>> _adjacency = new();
|
||||
private readonly List<Position> _allNodes = new();
|
||||
|
||||
public void AddEdge(Position a, Position b)
|
||||
{
|
||||
if (!_adjacency.ContainsKey(a))
|
||||
{
|
||||
_adjacency[a] = new HashSet<Position>();
|
||||
_allNodes.Add(a);
|
||||
}
|
||||
if (!_adjacency.ContainsKey(b))
|
||||
{
|
||||
_adjacency[b] = new HashSet<Position>();
|
||||
_allNodes.Add(b);
|
||||
}
|
||||
|
||||
_adjacency[a].Add(b);
|
||||
_adjacency[b].Add(a);
|
||||
}
|
||||
|
||||
public Position? FindNearestNode(Position target)
|
||||
{
|
||||
if (_allNodes.Count == 0) return null;
|
||||
|
||||
Position? nearest = null;
|
||||
double minDist = double.MaxValue;
|
||||
|
||||
foreach (var node in _allNodes)
|
||||
{
|
||||
var dist = node.DistanceTo(target);
|
||||
if (dist < minDist)
|
||||
{
|
||||
minDist = dist;
|
||||
nearest = node;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
public HashSet<Position> GetReachableNodes(Position start, double maxDistance)
|
||||
{
|
||||
var visited = new HashSet<Position>();
|
||||
var queue = new Queue<Position>();
|
||||
|
||||
queue.Enqueue(start);
|
||||
visited.Add(start);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
|
||||
if (!_adjacency.TryGetValue(current, out var neighbors))
|
||||
continue;
|
||||
|
||||
foreach (var neighbor in neighbors)
|
||||
{
|
||||
if (!visited.Contains(neighbor) && neighbor.DistanceTo(start) <= maxDistance)
|
||||
{
|
||||
visited.Add(neighbor);
|
||||
queue.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visited;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
245
Persistence.cs
Normal file
245
Persistence.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// Write-Ahead Log a Snapshoty pro persistenci
|
||||
public class Persistence
|
||||
{
|
||||
private readonly ServerConfig _config;
|
||||
private readonly ILogger<Persistence> _logger;
|
||||
private readonly object _walLock = new();
|
||||
|
||||
public Persistence(ServerConfig config, ILogger<Persistence> logger)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
|
||||
// Vytvoříme adresář pro data
|
||||
Directory.CreateDirectory(_config.DataPath);
|
||||
}
|
||||
|
||||
#region WAL
|
||||
|
||||
public void AppendToWal(string lobbyId, GameEvent evt)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
Directory.CreateDirectory(lobbyDir);
|
||||
|
||||
var walPath = GetCurrentWalPath(lobbyDir);
|
||||
var json = JsonSerializer.Serialize(evt, JsonOptions.Default);
|
||||
|
||||
lock (_walLock)
|
||||
{
|
||||
// Kontrola velikosti WAL
|
||||
if (File.Exists(walPath))
|
||||
{
|
||||
var fileInfo = new FileInfo(walPath);
|
||||
if (fileInfo.Length > _config.WalMaxSizeMb * 1024 * 1024)
|
||||
{
|
||||
// Rotace - nový soubor
|
||||
var newWalPath = Path.Combine(lobbyDir, $"wal_{DateTime.UtcNow:yyyyMMddHHmmss}.ndjson");
|
||||
walPath = newWalPath;
|
||||
}
|
||||
}
|
||||
|
||||
using var fs = new FileStream(walPath, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||
using var writer = new StreamWriter(fs, Encoding.UTF8);
|
||||
writer.WriteLine(json);
|
||||
writer.Flush();
|
||||
fs.Flush(true); // fsync
|
||||
}
|
||||
}
|
||||
|
||||
public List<GameEvent> ReadWal(string lobbyId, long fromEventId = 0)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
var events = new List<GameEvent>();
|
||||
|
||||
if (!Directory.Exists(lobbyDir))
|
||||
return events;
|
||||
|
||||
// Načteme všechny WAL soubory seřazené podle času
|
||||
var walFiles = Directory.GetFiles(lobbyDir, "wal_*.ndjson")
|
||||
.OrderBy(f => f)
|
||||
.ToList();
|
||||
|
||||
foreach (var walPath in walFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = File.ReadAllLines(walPath, Encoding.UTF8);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
|
||||
var evt = JsonSerializer.Deserialize<GameEvent>(line, JsonOptions.Default);
|
||||
if (evt != null && evt.EventId > fromEventId)
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Chyba při čtení WAL {Path}", walPath);
|
||||
}
|
||||
}
|
||||
|
||||
return events.OrderBy(e => e.EventId).ToList();
|
||||
}
|
||||
|
||||
private string GetCurrentWalPath(string lobbyDir)
|
||||
{
|
||||
// Najdeme nejnovější WAL nebo vytvoříme nový
|
||||
var walFiles = Directory.GetFiles(lobbyDir, "wal_*.ndjson");
|
||||
if (walFiles.Length == 0)
|
||||
{
|
||||
return Path.Combine(lobbyDir, $"wal_{DateTime.UtcNow:yyyyMMddHHmmss}.ndjson");
|
||||
}
|
||||
return walFiles.OrderByDescending(f => f).First();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Snapshots
|
||||
|
||||
public void SaveSnapshot(string lobbyId, LobbySnapshot snapshot)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
Directory.CreateDirectory(lobbyDir);
|
||||
|
||||
// Serializujeme bez checksumu
|
||||
snapshot.Timestamp = DateTime.UtcNow;
|
||||
var json = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
||||
|
||||
// Spočítáme checksum
|
||||
var checksum = ComputeChecksum(json);
|
||||
snapshot.Checksum = checksum;
|
||||
|
||||
// Serializujeme znovu s checksumem
|
||||
json = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
||||
|
||||
var snapshotPath = Path.Combine(lobbyDir, $"snapshot_{snapshot.LastEventId}.json");
|
||||
var tempPath = snapshotPath + ".tmp";
|
||||
|
||||
// Atomic write - temp + rename
|
||||
File.WriteAllText(tempPath, json, Encoding.UTF8);
|
||||
File.Move(tempPath, snapshotPath, overwrite: true);
|
||||
|
||||
_logger.LogInformation("Snapshot uložen: {Path}, eventId: {EventId}", snapshotPath, snapshot.LastEventId);
|
||||
|
||||
// Čištění starých snapshotů
|
||||
CleanupOldSnapshots(lobbyDir);
|
||||
}
|
||||
|
||||
public LobbySnapshot? LoadLatestSnapshot(string lobbyId)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
|
||||
if (!Directory.Exists(lobbyDir))
|
||||
return null;
|
||||
|
||||
var snapshotFiles = Directory.GetFiles(lobbyDir, "snapshot_*.json")
|
||||
.OrderByDescending(f => f)
|
||||
.ToList();
|
||||
|
||||
foreach (var snapshotPath in snapshotFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(snapshotPath, Encoding.UTF8);
|
||||
var snapshot = JsonSerializer.Deserialize<LobbySnapshot>(json, JsonOptions.Default);
|
||||
|
||||
if (snapshot == null) continue;
|
||||
|
||||
// Validace checksumu
|
||||
var storedChecksum = snapshot.Checksum;
|
||||
snapshot.Checksum = "";
|
||||
var jsonWithoutChecksum = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
||||
var computedChecksum = ComputeChecksum(jsonWithoutChecksum);
|
||||
|
||||
if (storedChecksum != computedChecksum)
|
||||
{
|
||||
_logger.LogWarning("Checksum mismatch pro snapshot {Path}", snapshotPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshot.Checksum = storedChecksum;
|
||||
return snapshot;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Chyba při čtení snapshot {Path}", snapshotPath);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CleanupOldSnapshots(string lobbyDir, int keepCount = 5)
|
||||
{
|
||||
var snapshotFiles = Directory.GetFiles(lobbyDir, "snapshot_*.json")
|
||||
.OrderByDescending(f => f)
|
||||
.Skip(keepCount)
|
||||
.ToList();
|
||||
|
||||
foreach (var oldSnapshot in snapshotFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(oldSnapshot);
|
||||
_logger.LogDebug("Smazán starý snapshot: {Path}", oldSnapshot);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Nelze smazat snapshot {Path}", oldSnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeChecksum(string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Archive
|
||||
|
||||
public void ArchiveLobby(string lobbyId)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
if (!Directory.Exists(lobbyDir))
|
||||
return;
|
||||
|
||||
var archiveDir = Path.Combine(_config.DataPath, "archive");
|
||||
Directory.CreateDirectory(archiveDir);
|
||||
|
||||
var archivePath = Path.Combine(archiveDir, $"{lobbyId}_{DateTime.UtcNow:yyyyMMddHHmmss}");
|
||||
Directory.Move(lobbyDir, archivePath);
|
||||
|
||||
_logger.LogInformation("Lobby archivováno: {LobbyId} -> {ArchivePath}", lobbyId, archivePath);
|
||||
}
|
||||
|
||||
public void DeleteLobbyData(string lobbyId)
|
||||
{
|
||||
var lobbyDir = GetLobbyDir(lobbyId);
|
||||
if (Directory.Exists(lobbyDir))
|
||||
{
|
||||
Directory.Delete(lobbyDir, recursive: true);
|
||||
_logger.LogInformation("Data lobby smazána: {LobbyId}", lobbyId);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string GetLobbyDir(string lobbyId)
|
||||
{
|
||||
return Path.Combine(_config.DataPath, "lobbies", lobbyId);
|
||||
}
|
||||
}
|
||||
769
Program.cs
Normal file
769
Program.cs
Normal file
@@ -0,0 +1,769 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
// Hlavní vstupní bod serveru
|
||||
public class Program
|
||||
{
|
||||
static readonly ConcurrentDictionary<string, ConnectedClient> _clients = new();
|
||||
static readonly DateTime _startTime = DateTime.UtcNow;
|
||||
static ServerConfig _config = null!;
|
||||
static ServerEncryption _encryption = null!;
|
||||
static LobbyManager _lobbyManager = null!;
|
||||
static StatsDb _statsDb = null!;
|
||||
static Persistence _persistence = null!;
|
||||
static OverpassService _overpassService = null!;
|
||||
static AdminPanel _adminPanel = null!;
|
||||
static ILoggerFactory _loggerFactory = null!;
|
||||
static ILogger<Program> _logger = null!;
|
||||
static CancellationTokenSource _cts = new();
|
||||
|
||||
// Public accessor for admin panel
|
||||
public static DateTime StartTime => _startTime;
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
|
||||
// Inicializace
|
||||
_loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Information);
|
||||
});
|
||||
_logger = _loggerFactory.CreateLogger<Program>();
|
||||
|
||||
// Načtení konfigurace
|
||||
_config = ServerConfig.Load("appsettings.json");
|
||||
|
||||
// Uložíme výchozí config pokud neexistuje
|
||||
if (!File.Exists("appsettings.json"))
|
||||
{
|
||||
_config.Save("appsettings.json");
|
||||
_logger.LogInformation("Vytvořen výchozí appsettings.json");
|
||||
}
|
||||
|
||||
// Inicializace komponent
|
||||
_encryption = new ServerEncryption(_config.RsaKeySizeBits);
|
||||
_statsDb = new StatsDb(Path.Combine(_config.DataPath, "stats.db"), _loggerFactory.CreateLogger<StatsDb>());
|
||||
_persistence = new Persistence(_config, _loggerFactory.CreateLogger<Persistence>());
|
||||
_overpassService = new OverpassService(_config, _loggerFactory.CreateLogger<OverpassService>());
|
||||
_lobbyManager = new LobbyManager(_config, _loggerFactory.CreateLogger<LobbyManager>(), _statsDb, _persistence, _overpassService);
|
||||
_adminPanel = new AdminPanel(_loggerFactory.CreateLogger<AdminPanel>(), _config, _lobbyManager, _statsDb);
|
||||
|
||||
_logger.LogInformation("GeoSus Server spouštím...");
|
||||
_logger.LogInformation("TCP port: {TcpPort}, HTTP port: {HttpPort}", _config.TcpPort, _config.HttpPort);
|
||||
|
||||
// Handle shutdown signals - but only on explicit request
|
||||
var shutdownRequested = false;
|
||||
Console.CancelKeyPress += (s, e) =>
|
||||
{
|
||||
if (!shutdownRequested)
|
||||
{
|
||||
shutdownRequested = true;
|
||||
e.Cancel = true;
|
||||
_logger.LogInformation("Shutdown requested (Ctrl+C)... press again to force");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Force shutdown...");
|
||||
_cts.Cancel();
|
||||
}
|
||||
};
|
||||
|
||||
// Spustíme TCP listener a HTTP API paralelně
|
||||
var tcpTask = RunTcpServerAsync();
|
||||
var httpTask = RunHttpServerAsync();
|
||||
var cleanupTask = RunCleanupLoopAsync();
|
||||
var broadcastTask = RunBroadcastLoopAsync();
|
||||
|
||||
_logger.LogInformation("Server běží. Stiskni Ctrl+C dvakrát pro ukončení.");
|
||||
|
||||
// Just wait forever until cancellation - don't monitor individual tasks
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout.Infinite, _cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Shutting down...");
|
||||
}
|
||||
|
||||
_statsDb.Dispose();
|
||||
_encryption.Dispose();
|
||||
|
||||
_logger.LogInformation("Server ukončen");
|
||||
}
|
||||
|
||||
#region TCP Server
|
||||
|
||||
static async Task RunTcpServerAsync()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Any, _config.TcpPort);
|
||||
listener.Start();
|
||||
|
||||
_logger.LogInformation("TCP server naslouchá na portu {Port}", _config.TcpPort);
|
||||
|
||||
try
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tcpClient = await listener.AcceptTcpClientAsync(_cts.Token);
|
||||
_ = HandleClientAsync(tcpClient);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Socket error while accepting client");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("TCP server ukončen (cancelled)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "TCP server fatal error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task HandleClientAsync(TcpClient tcpClient)
|
||||
{
|
||||
var endpoint = tcpClient.Client.RemoteEndPoint?.ToString() ?? "unknown";
|
||||
_logger.LogDebug("Nové spojení z {Endpoint}", endpoint);
|
||||
|
||||
using var stream = tcpClient.GetStream();
|
||||
ConnectedClient? client = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Handshake - plain JSON
|
||||
client = await PerformHandshakeAsync(stream, endpoint);
|
||||
|
||||
if (client == null)
|
||||
{
|
||||
_logger.LogWarning("Handshake selhal pro {Endpoint}", endpoint);
|
||||
return;
|
||||
}
|
||||
|
||||
_clients[client.ClientUuid] = client;
|
||||
_logger.LogInformation("Klient {Uuid} připojen z {Endpoint}", client.ClientUuid, endpoint);
|
||||
|
||||
// Hlavní smyčka zpráv - šifrované
|
||||
await ProcessMessagesAsync(stream, client);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex, "Chyba při zpracování klienta {Endpoint}", endpoint);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (client != null)
|
||||
{
|
||||
_clients.TryRemove(client.ClientUuid, out _);
|
||||
|
||||
// Odebrat z lobby pokud byl přihlášen
|
||||
if (client.Lobby != null)
|
||||
{
|
||||
client.Lobby.UnregisterConnection(client.ClientUuid);
|
||||
await client.Lobby.RemovePlayerAsync(client.ClientUuid, "Disconnected");
|
||||
}
|
||||
|
||||
client.Session?.Dispose();
|
||||
_logger.LogInformation("Klient {Uuid} odpojen", client.ClientUuid);
|
||||
}
|
||||
|
||||
tcpClient.Close();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<ConnectedClient?> PerformHandshakeAsync(NetworkStream stream, string endpoint)
|
||||
{
|
||||
// 1. Čekáme na ClientHello (plain)
|
||||
var helloData = await ReadMessageAsync(stream);
|
||||
if (helloData == null) return null;
|
||||
|
||||
// Debug: log co přichází
|
||||
_logger.LogInformation("Handshake from {Endpoint}: {Length} bytes, first: 0x{First:X2}, content: {Content}",
|
||||
endpoint, helloData.Length, helloData[0],
|
||||
System.Text.Encoding.UTF8.GetString(helloData.AsSpan(0, Math.Min(200, helloData.Length))));
|
||||
|
||||
var hello = MessageSerializer.Deserialize(helloData) as ClientHello;
|
||||
if (hello == null)
|
||||
{
|
||||
_logger.LogWarning("Neplatný ClientHello z {Endpoint}", endpoint);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Posíláme ServerHello s RSA public key (plain)
|
||||
var serverHello = new ServerHello
|
||||
{
|
||||
RsaPublicKeyPem = _encryption.PublicKeyPem,
|
||||
ServerId = Environment.MachineName
|
||||
};
|
||||
await SendMessagePlainAsync(stream, serverHello);
|
||||
|
||||
// 3. Čekáme na KeyExchange (plain - obsahuje RSA šifrovaný session key)
|
||||
var keyExData = await ReadMessageAsync(stream);
|
||||
if (keyExData == null) return null;
|
||||
|
||||
// Debug: hex dump prvních 20 bajtů
|
||||
var hexDump = BitConverter.ToString(keyExData, 0, Math.Min(20, keyExData.Length));
|
||||
_logger.LogInformation("KeyExchange from {Endpoint}: {Length} bytes, hex: {Hex}",
|
||||
endpoint, keyExData.Length, hexDump);
|
||||
|
||||
var keyEx = MessageSerializer.Deserialize(keyExData) as KeyExchange;
|
||||
if (keyEx == null)
|
||||
{
|
||||
_logger.LogWarning("Neplatný KeyExchange z {Endpoint}", endpoint);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. Dešifrujeme session key
|
||||
byte[] sessionKey, sessionIv;
|
||||
try
|
||||
{
|
||||
(sessionKey, sessionIv) = _encryption.DecryptSessionKey(keyEx.EncryptedSessionKey, keyEx.EncryptedIV);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Chyba při dešifrování session key z {Endpoint}", endpoint);
|
||||
return null;
|
||||
}
|
||||
|
||||
var session = new SessionCrypto(sessionKey, sessionIv);
|
||||
|
||||
// 5. Posíláme KeyExchangeAck (šifrovaně)
|
||||
var ack = new KeyExchangeAck { Status = "success" };
|
||||
await SendMessageEncryptedAsync(stream, ack, session);
|
||||
|
||||
return new ConnectedClient
|
||||
{
|
||||
ClientUuid = hello.ClientUuid,
|
||||
DisplayName = hello.DisplayName ?? $"Player_{hello.ClientUuid[..4]}",
|
||||
Endpoint = endpoint,
|
||||
Session = session,
|
||||
Stream = stream
|
||||
};
|
||||
}
|
||||
|
||||
static async Task ProcessMessagesAsync(NetworkStream stream, ConnectedClient client)
|
||||
{
|
||||
int decryptFailures = 0;
|
||||
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
var encrypted = await ReadMessageAsync(stream);
|
||||
if (encrypted == null) break;
|
||||
|
||||
// Dešifrovat
|
||||
var decrypted = client.Session!.Decrypt(encrypted);
|
||||
if (decrypted == null)
|
||||
{
|
||||
decryptFailures++;
|
||||
_logger.LogWarning("Dešifrování selhalo pro {Uuid} ({Count}x)", client.ClientUuid, decryptFailures);
|
||||
|
||||
if (decryptFailures >= 3)
|
||||
{
|
||||
_logger.LogWarning("Příliš mnoho chyb dešifrování, odpojuji {Uuid}", client.ClientUuid);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
decryptFailures = 0;
|
||||
|
||||
var message = MessageSerializer.Deserialize(decrypted);
|
||||
if (message == null)
|
||||
{
|
||||
_logger.LogWarning("Neplatná zpráva od {Uuid}", client.ClientUuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
await HandleMessageAsync(client, message);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task HandleMessageAsync(ConnectedClient client, Message message)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case CreateLobby m:
|
||||
await HandleCreateLobbyAsync(client, m);
|
||||
break;
|
||||
|
||||
case JoinLobby m:
|
||||
await HandleJoinLobbyAsync(client, m);
|
||||
break;
|
||||
|
||||
case Reconnect m:
|
||||
await HandleReconnectAsync(client, m);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Předat do lobby pokud je přihlášen
|
||||
if (client.Lobby != null)
|
||||
{
|
||||
await client.Lobby.HandleMessageAsync(client.ClientUuid, message);
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = new ErrorMessage
|
||||
{
|
||||
ErrorCode = "NOT_IN_LOBBY",
|
||||
ErrorText = "Nejsi v žádném lobby"
|
||||
};
|
||||
await SendToClientAsync(client, error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task HandleCreateLobbyAsync(ConnectedClient client, CreateLobby message)
|
||||
{
|
||||
var (lobby, joinCode, error) = await _lobbyManager.CreateLobbyAsync(
|
||||
client.ClientUuid, client.DisplayName, message);
|
||||
|
||||
var response = new CreateLobbyResponse
|
||||
{
|
||||
ClientSeq = message.ClientSeq,
|
||||
Success = lobby != null,
|
||||
JoinCode = joinCode,
|
||||
LobbyId = lobby?.Settings.LobbyId,
|
||||
Error = error,
|
||||
LobbyState = lobby?.GetLobbyState()
|
||||
};
|
||||
|
||||
if (lobby != null)
|
||||
{
|
||||
client.Lobby = lobby;
|
||||
lobby.RegisterConnection(client.ClientUuid, new ClientConnection
|
||||
{
|
||||
ClientUuid = client.ClientUuid,
|
||||
SendAsync = msg => SendToClientAsync(client, msg)
|
||||
});
|
||||
}
|
||||
|
||||
await SendToClientAsync(client, response);
|
||||
}
|
||||
|
||||
static async Task HandleJoinLobbyAsync(ConnectedClient client, JoinLobby message)
|
||||
{
|
||||
var clientIp = client.Endpoint.Split(':')[0];
|
||||
var (lobby, error) = await _lobbyManager.JoinLobbyAsync(
|
||||
clientIp, client.ClientUuid, client.DisplayName, message.JoinCode, message.Password);
|
||||
|
||||
var response = new JoinLobbyResponse
|
||||
{
|
||||
ClientSeq = message.ClientSeq,
|
||||
Success = lobby != null,
|
||||
LobbyId = lobby?.Settings.LobbyId,
|
||||
Error = error,
|
||||
LobbyState = lobby?.GetLobbyState()
|
||||
};
|
||||
|
||||
if (lobby != null)
|
||||
{
|
||||
client.Lobby = lobby;
|
||||
lobby.RegisterConnection(client.ClientUuid, new ClientConnection
|
||||
{
|
||||
ClientUuid = client.ClientUuid,
|
||||
SendAsync = msg => SendToClientAsync(client, msg)
|
||||
});
|
||||
}
|
||||
|
||||
await SendToClientAsync(client, response);
|
||||
}
|
||||
|
||||
static async Task HandleReconnectAsync(ConnectedClient client, Reconnect message)
|
||||
{
|
||||
var lobby = _lobbyManager.GetLobby(message.LobbyId);
|
||||
|
||||
if (lobby == null)
|
||||
{
|
||||
await SendToClientAsync(client, new ReconnectResponse
|
||||
{
|
||||
Success = false,
|
||||
Error = "Lobby neexistuje"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Načteme snapshot a eventy
|
||||
var snapshot = _persistence.LoadLatestSnapshot(message.LobbyId);
|
||||
var events = _persistence.ReadWal(message.LobbyId, message.LastEventId);
|
||||
|
||||
client.Lobby = lobby;
|
||||
lobby.RegisterConnection(client.ClientUuid, new ClientConnection
|
||||
{
|
||||
ClientUuid = client.ClientUuid,
|
||||
SendAsync = msg => SendToClientAsync(client, msg)
|
||||
});
|
||||
|
||||
await lobby.AddPlayerAsync(client.ClientUuid, client.DisplayName, isOwner: false);
|
||||
|
||||
await SendToClientAsync(client, new ReconnectResponse
|
||||
{
|
||||
Success = true,
|
||||
Snapshot = snapshot,
|
||||
MissedEvents = events
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HTTP Stats API
|
||||
|
||||
static async Task RunHttpServerAsync()
|
||||
{
|
||||
HttpListener? listener = null;
|
||||
bool isDocker = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"
|
||||
|| Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") == "Production";
|
||||
|
||||
// V Dockeru/produkci bindujeme na 0.0.0.0 (všechny interfaces)
|
||||
// Lokálně bindujeme na localhost (nevyžaduje admin práva na Windows)
|
||||
var prefixes = isDocker
|
||||
? new[] { $"http://+:{_config.HttpPort}/", $"http://*:{_config.HttpPort}/" }
|
||||
: new[] { $"http://localhost:{_config.HttpPort}/", $"http://127.0.0.1:{_config.HttpPort}/" };
|
||||
|
||||
foreach (var prefixSet in new[] { prefixes })
|
||||
{
|
||||
try
|
||||
{
|
||||
listener = new HttpListener();
|
||||
foreach (var prefix in prefixSet)
|
||||
listener.Prefixes.Add(prefix);
|
||||
listener.Start();
|
||||
_logger.LogInformation("HTTP Stats API naslouchá na portu {Port} (Docker: {IsDocker})", _config.HttpPort, isDocker);
|
||||
break;
|
||||
}
|
||||
catch (HttpListenerException ex)
|
||||
{
|
||||
_logger.LogWarning("Nelze spustit HTTP Stats API: {Error}", ex.Message);
|
||||
listener?.Close();
|
||||
listener = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (listener == null)
|
||||
{
|
||||
_logger.LogWarning("HTTP Stats API není dostupné - nelze otevřít port {Port}. Server běží bez Stats API.", _config.HttpPort);
|
||||
// Just wait for cancellation instead of crashing
|
||||
try { await Task.Delay(Timeout.Infinite, _cts.Token); } catch (OperationCanceledException) { }
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
var context = await listener.GetContextAsync();
|
||||
_ = HandleHttpRequestAsync(context);
|
||||
}
|
||||
}
|
||||
catch (HttpListenerException) when (_cts.IsCancellationRequested)
|
||||
{
|
||||
// Normal shutdown
|
||||
}
|
||||
catch (ObjectDisposedException) when (_cts.IsCancellationRequested)
|
||||
{
|
||||
// Normal shutdown
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "HTTP listener error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener?.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task HandleHttpRequestAsync(HttpListenerContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
|
||||
// CORS headers
|
||||
response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
|
||||
if (request.HttpMethod == "OPTIONS")
|
||||
{
|
||||
response.StatusCode = 200;
|
||||
response.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = request.Url?.AbsolutePath ?? "/";
|
||||
|
||||
// Admin panel routes
|
||||
if (path.StartsWith("/admin"))
|
||||
{
|
||||
await _adminPanel.HandleRequestAsync(context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.StartsWith("/stats/leaderboard"))
|
||||
{
|
||||
await HandleLeaderboardAsync(request, response);
|
||||
}
|
||||
else if (path.StartsWith("/stats/"))
|
||||
{
|
||||
var clientUuid = path[7..]; // Remove "/stats/"
|
||||
await HandlePlayerStatsAsync(clientUuid, response);
|
||||
}
|
||||
else if (path == "/health")
|
||||
{
|
||||
var uptime = DateTime.UtcNow - _startTime;
|
||||
await WriteJsonResponseAsync(response, new
|
||||
{
|
||||
status = "ok",
|
||||
version = "1.0.0",
|
||||
uptimeSeconds = (long)uptime.TotalSeconds,
|
||||
activeLobbies = _lobbyManager.LobbyCount,
|
||||
connectedPlayers = _lobbyManager.TotalPlayerCount
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonResponseAsync(response, new { error = "Not found" });
|
||||
}
|
||||
|
||||
response.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "HTTP error");
|
||||
try
|
||||
{
|
||||
response.StatusCode = 500;
|
||||
await WriteJsonResponseAsync(response, new { error = "Internal server error" });
|
||||
response.Close();
|
||||
}
|
||||
catch { /* Already closed */ }
|
||||
}
|
||||
}
|
||||
|
||||
static async Task HandlePlayerStatsAsync(string clientUuid, HttpListenerResponse response)
|
||||
{
|
||||
var stats = _statsDb.GetPlayerStats(clientUuid);
|
||||
|
||||
if (stats == null)
|
||||
{
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonResponseAsync(response, new { error = "Player not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
var result = new
|
||||
{
|
||||
clientUuid = stats.ClientUuid,
|
||||
displayName = stats.DisplayName,
|
||||
totalGames = stats.TotalGames,
|
||||
gamesAsCrew = stats.GamesAsCrew,
|
||||
gamesAsImpostor = stats.GamesAsImpostor,
|
||||
crewWins = stats.CrewWins,
|
||||
impostorWins = stats.ImpostorWins,
|
||||
crewWinRate = Math.Round(stats.CrewWinRate, 2),
|
||||
impostorWinRate = Math.Round(stats.ImpostorWinRate, 2),
|
||||
totalKills = stats.TotalKills,
|
||||
totalDeaths = stats.TotalDeaths,
|
||||
killDeathRatio = Math.Round(stats.KillDeathRatio, 2),
|
||||
tasksCompleted = stats.TasksCompleted,
|
||||
averageTasksPerGame = Math.Round(stats.AverageTasksPerGame, 2),
|
||||
bodiesReported = stats.BodiesReported,
|
||||
emergencyMeetingsCalled = stats.EmergencyMeetingsCalled,
|
||||
timesVotedOut = stats.TimesVotedOut,
|
||||
successfulVotes = stats.SuccessfulVotes,
|
||||
totalPlaytimeSeconds = stats.TotalPlaytimeSeconds,
|
||||
cheatIncidents = stats.CheatIncidents,
|
||||
lastSeen = stats.LastSeenUtc
|
||||
};
|
||||
|
||||
await WriteJsonResponseAsync(response, result);
|
||||
}
|
||||
|
||||
static async Task HandleLeaderboardAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
var sortBy = request.QueryString["sortBy"] ?? "total_games";
|
||||
var limitStr = request.QueryString["limit"] ?? "100";
|
||||
var limit = int.TryParse(limitStr, out var l) ? l : 100;
|
||||
limit = Math.Min(limit, 1000);
|
||||
|
||||
var leaderboard = _statsDb.GetLeaderboard(sortBy, limit);
|
||||
|
||||
var result = leaderboard.Select(s => new
|
||||
{
|
||||
clientUuid = s.ClientUuid,
|
||||
displayName = s.DisplayName,
|
||||
totalGames = s.TotalGames,
|
||||
crewWins = s.CrewWins,
|
||||
impostorWins = s.ImpostorWins,
|
||||
totalKills = s.TotalKills,
|
||||
tasksCompleted = s.TasksCompleted
|
||||
});
|
||||
|
||||
await WriteJsonResponseAsync(response, result);
|
||||
}
|
||||
|
||||
static async Task WriteJsonResponseAsync(HttpListenerResponse response, object data)
|
||||
{
|
||||
response.ContentType = "application/json";
|
||||
var json = JsonSerializer.Serialize(data, JsonOptions.Default);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
response.ContentLength64 = bytes.Length;
|
||||
await response.OutputStream.WriteAsync(bytes);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Background tasks
|
||||
|
||||
static async Task RunCleanupLoopAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
|
||||
|
||||
while (await timer.WaitForNextTickAsync(_cts.Token))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _lobbyManager.CleanupIdleLobbiesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Chyba při cleanup");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Normal shutdown
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunBroadcastLoopAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_config.PositionBroadcastRateMs));
|
||||
|
||||
while (await timer.WaitForNextTickAsync(_cts.Token))
|
||||
{
|
||||
foreach (var client in _clients.Values)
|
||||
{
|
||||
client.Lobby?.BroadcastPositions();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Normal shutdown
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network helpers
|
||||
|
||||
static async Task<byte[]?> ReadMessageAsync(NetworkStream stream)
|
||||
{
|
||||
// 4 bytes length prefix (big-endian)
|
||||
var lengthBuffer = new byte[4];
|
||||
var read = await stream.ReadAsync(lengthBuffer, 0, 4);
|
||||
if (read < 4) return null;
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(lengthBuffer);
|
||||
var length = BitConverter.ToInt32(lengthBuffer, 0);
|
||||
|
||||
if (length <= 0 || length > _config.MaxPacketSizeBytes)
|
||||
return null;
|
||||
|
||||
var buffer = new byte[length];
|
||||
var totalRead = 0;
|
||||
while (totalRead < length)
|
||||
{
|
||||
read = await stream.ReadAsync(buffer, totalRead, length - totalRead);
|
||||
if (read == 0) return null;
|
||||
totalRead += read;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static async Task SendMessagePlainAsync(NetworkStream stream, Message message)
|
||||
{
|
||||
var data = MessageSerializer.Serialize(message);
|
||||
await SendDataAsync(stream, data);
|
||||
}
|
||||
|
||||
static async Task SendMessageEncryptedAsync(NetworkStream stream, Message message, SessionCrypto session)
|
||||
{
|
||||
var plain = MessageSerializer.Serialize(message);
|
||||
var encrypted = session.Encrypt(plain);
|
||||
await SendDataAsync(stream, encrypted);
|
||||
}
|
||||
|
||||
static async Task SendDataAsync(NetworkStream stream, byte[] data)
|
||||
{
|
||||
var lengthBuffer = BitConverter.GetBytes(data.Length);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(lengthBuffer);
|
||||
|
||||
await stream.WriteAsync(lengthBuffer);
|
||||
await stream.WriteAsync(data);
|
||||
await stream.FlushAsync();
|
||||
}
|
||||
|
||||
static async Task SendToClientAsync(ConnectedClient client, Message message)
|
||||
{
|
||||
if (client.Session == null || client.Stream == null) return;
|
||||
|
||||
var plain = MessageSerializer.Serialize(message);
|
||||
var encrypted = client.Session.Encrypt(plain);
|
||||
|
||||
lock (client.SendLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
SendDataAsync(client.Stream, encrypted).Wait();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Chyba při odesílání do {Uuid}", client.ClientUuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
// Connected client state
|
||||
class ConnectedClient
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public required string DisplayName { get; set; }
|
||||
public required string Endpoint { get; set; }
|
||||
public SessionCrypto? Session { get; set; }
|
||||
public NetworkStream? Stream { get; set; }
|
||||
public LobbyActor? Lobby { get; set; }
|
||||
public object SendLock { get; } = new();
|
||||
}
|
||||
12
Properties/launchSettings.json
Normal file
12
Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Server": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:9091;http://localhost:9092"
|
||||
}
|
||||
}
|
||||
}
|
||||
948
Protocol.cs
Normal file
948
Protocol.cs
Normal file
@@ -0,0 +1,948 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
#region Základní typy
|
||||
|
||||
public record struct Position(double Lat, double Lon)
|
||||
{
|
||||
// Haversine vzdálenost v metrech
|
||||
public double DistanceTo(Position other)
|
||||
{
|
||||
const double R = 6371000; // Poloměr Země v metrech
|
||||
var lat1 = Lat * Math.PI / 180;
|
||||
var lat2 = other.Lat * Math.PI / 180;
|
||||
var dLat = (other.Lat - Lat) * Math.PI / 180;
|
||||
var dLon = (other.Lon - Lon) * Math.PI / 180;
|
||||
|
||||
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||
Math.Cos(lat1) * Math.Cos(lat2) *
|
||||
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PlayerRole { Crew, Impostor }
|
||||
public enum PlayerState { Alive, Dead }
|
||||
public enum CheatStatus { Ok, Warn, Restrict, Kicked }
|
||||
public enum GamePhase { Lobby, Loading, Playing, Meeting, Voting, Ended }
|
||||
public enum TaskType { Instant }
|
||||
public enum MeetingType { BodyReport, Emergency }
|
||||
|
||||
// Sabotage system
|
||||
public enum SabotageType
|
||||
{
|
||||
/// <summary>Comms Blackout - Cannot report bodies or call emergency meetings</summary>
|
||||
CommsBlackout,
|
||||
/// <summary>Critical Meltdown - Must be repaired in time or impostors win (requires 2 simultaneous repairs)</summary>
|
||||
CriticalMeltdown
|
||||
}
|
||||
|
||||
public enum SabotageState
|
||||
{
|
||||
Inactive,
|
||||
Active,
|
||||
Repaired
|
||||
}
|
||||
|
||||
// Map data types
|
||||
public enum PathType
|
||||
{
|
||||
Footway,
|
||||
Steps,
|
||||
Cycleway,
|
||||
Residential,
|
||||
Service,
|
||||
Road,
|
||||
Track,
|
||||
Other
|
||||
}
|
||||
|
||||
public enum MapAreaType
|
||||
{
|
||||
Park,
|
||||
Playground,
|
||||
Garden,
|
||||
Water,
|
||||
Other
|
||||
}
|
||||
|
||||
public enum MapPOIType
|
||||
{
|
||||
FoodDrink,
|
||||
Finance,
|
||||
Health,
|
||||
Education,
|
||||
Transport,
|
||||
Shop,
|
||||
Amenity,
|
||||
Other
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Herní entity
|
||||
|
||||
public class Player
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public required string DisplayName { get; set; }
|
||||
public Position Position { get; set; }
|
||||
public PlayerRole Role { get; set; } = PlayerRole.Crew;
|
||||
public PlayerState State { get; set; } = PlayerState.Alive;
|
||||
public CheatStatus CheatStatus { get; set; } = CheatStatus.Ok;
|
||||
public int CheatScore { get; set; }
|
||||
public DateTime ConnectedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime LastPositionUpdate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? LastKillTime { get; set; }
|
||||
public DateTime? LastEmergencyMeetingTime { get; set; }
|
||||
public int EmergencyMeetingsUsed { get; set; }
|
||||
public List<string> CompletedTaskIds { get; set; } = new();
|
||||
public List<GameTask> Tasks { get; set; } = new(); // Per-player tasks
|
||||
public string? CurrentTaskId { get; set; }
|
||||
public DateTime? TaskStartTime { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
public bool IsReady { get; set; }
|
||||
public long LastEventId { get; set; }
|
||||
public int Ping { get; set; }
|
||||
|
||||
// Historie pozic pro anti-cheat
|
||||
public Queue<(Position Pos, DateTime Time)> PositionHistory { get; set; } = new();
|
||||
}
|
||||
|
||||
public class Body
|
||||
{
|
||||
public required string BodyId { get; set; }
|
||||
public required string VictimId { get; set; }
|
||||
public required string KillerId { get; set; }
|
||||
public Position Location { get; set; }
|
||||
public DateTime KilledAt { get; set; } = DateTime.UtcNow;
|
||||
public bool Reported { get; set; }
|
||||
public string? ReportedBy { get; set; }
|
||||
}
|
||||
|
||||
public class GameTask
|
||||
{
|
||||
public required string TaskId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public Position Location { get; set; }
|
||||
public TaskType Type { get; set; } = TaskType.Instant;
|
||||
}
|
||||
|
||||
public class Meeting
|
||||
{
|
||||
public required string MeetingId { get; set; }
|
||||
public MeetingType Type { get; set; }
|
||||
public string? ReportedBodyId { get; set; }
|
||||
public required string CallerId { get; set; }
|
||||
public Position MeetingLocation { get; set; }
|
||||
public DateTime StartTime { get; set; } = DateTime.UtcNow;
|
||||
public DateTime ArrivalDeadline { get; set; }
|
||||
public DateTime? DiscussionEndTime { get; set; }
|
||||
public DateTime VotingEndTime { get; set; }
|
||||
public HashSet<string> ArrivedPlayers { get; set; } = new();
|
||||
public Dictionary<string, string?> Votes { get; set; } = new(); // Voter -> Target (null = skip)
|
||||
public DateTime? LastVoteChangeTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Active sabotage instance
|
||||
/// </summary>
|
||||
public class Sabotage
|
||||
{
|
||||
public required string SabotageId { get; set; }
|
||||
public SabotageType Type { get; set; }
|
||||
public SabotageState State { get; set; } = SabotageState.Active;
|
||||
public required string InitiatorId { get; set; }
|
||||
public DateTime StartTime { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? Deadline { get; set; } // For critical meltdown
|
||||
public DateTime? RepairedAt { get; set; }
|
||||
public string? RepairedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Repair stations for this sabotage.
|
||||
/// CommsBlackout: 1 station
|
||||
/// CriticalMeltdown: 2 stations that need simultaneous activation
|
||||
/// </summary>
|
||||
public List<RepairStation> RepairStations { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// How many stations need to be simultaneously active to repair.
|
||||
/// CommsBlackout: 1, CriticalMeltdown: 2
|
||||
/// </summary>
|
||||
public int RequiredSimultaneousRepairs { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repair station for fixing sabotages
|
||||
/// </summary>
|
||||
public class RepairStation
|
||||
{
|
||||
public required string StationId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public Position Location { get; set; }
|
||||
public bool IsBeingRepaired { get; set; }
|
||||
public string? RepairingPlayerId { get; set; }
|
||||
public DateTime? RepairStartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has this station been successfully repaired
|
||||
/// </summary>
|
||||
public bool IsRepaired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How long player must hold to complete repair at this station
|
||||
/// </summary>
|
||||
public int RepairDurationMs { get; set; } = 3000;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Map Data
|
||||
|
||||
/// <summary>
|
||||
/// Complete map data for a play area - sent to clients for rendering
|
||||
/// </summary>
|
||||
public class MapData
|
||||
{
|
||||
public Position Center { get; set; }
|
||||
public double RadiusMeters { get; set; }
|
||||
public DateTime FetchedAt { get; set; }
|
||||
|
||||
/// <summary>Buildings to render (polygons)</summary>
|
||||
public List<MapBuilding> Buildings { get; set; } = new();
|
||||
|
||||
/// <summary>Walkable pathways (lines)</summary>
|
||||
public List<MapPathway> Pathways { get; set; } = new();
|
||||
|
||||
/// <summary>Parks, green areas (polygons)</summary>
|
||||
public List<MapArea> Areas { get; set; } = new();
|
||||
|
||||
/// <summary>Points of interest (single points)</summary>
|
||||
public List<MapPOI> PointsOfInterest { get; set; } = new();
|
||||
|
||||
/// <summary>All positions reachable from center via walkable paths (within play area)</summary>
|
||||
[JsonIgnore] // Too large for network, computed server-side only
|
||||
public List<Position> ReachablePositions { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Building outline for rendering</summary>
|
||||
public class MapBuilding
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public List<Position> Outline { get; set; } = new();
|
||||
public string BuildingType { get; set; } = "yes";
|
||||
public string? Name { get; set; }
|
||||
public int Levels { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>Walkable pathway for rendering and reachability</summary>
|
||||
public class MapPathway
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public List<Position> Points { get; set; } = new();
|
||||
public PathType PathType { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public bool IsWalkable { get; set; } = true;
|
||||
public double Width { get; set; } = 2.0;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsFullyReachable { get; set; }
|
||||
[JsonIgnore]
|
||||
public bool IsPartiallyReachable { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Area like park or garden</summary>
|
||||
public class MapArea
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public List<Position> Outline { get; set; } = new();
|
||||
public MapAreaType AreaType { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Point of interest for task placement hints</summary>
|
||||
public class MapPOI
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public Position Position { get; set; }
|
||||
public MapPOIType PoiType { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Simplified map data sent to clients (smaller payload)</summary>
|
||||
public class MapDataPayload
|
||||
{
|
||||
public Position Center { get; set; }
|
||||
public double RadiusMeters { get; set; }
|
||||
|
||||
/// <summary>Buildings: [[lat,lon,lat,lon,...], ...]</summary>
|
||||
public List<double[]> Buildings { get; set; } = new();
|
||||
|
||||
/// <summary>Building types: ["residential", "commercial", ...]</summary>
|
||||
public List<string> BuildingTypes { get; set; } = new();
|
||||
|
||||
/// <summary>Pathways: [[lat,lon,lat,lon,...], ...]</summary>
|
||||
public List<double[]> Pathways { get; set; } = new();
|
||||
|
||||
/// <summary>Pathway types: [0=footway, 1=steps, ...]</summary>
|
||||
public List<int> PathwayTypes { get; set; } = new();
|
||||
|
||||
/// <summary>Areas (parks): [[lat,lon,lat,lon,...], ...]</summary>
|
||||
public List<double[]> Areas { get; set; } = new();
|
||||
|
||||
/// <summary>Area types</summary>
|
||||
public List<int> AreaTypes { get; set; } = new();
|
||||
|
||||
/// <summary>POIs: [lat, lon, type, lat, lon, type, ...]</summary>
|
||||
public List<double> POIs { get; set; } = new();
|
||||
|
||||
/// <summary>Convert from full MapData to compact payload</summary>
|
||||
public static MapDataPayload FromMapData(MapData data)
|
||||
{
|
||||
var payload = new MapDataPayload
|
||||
{
|
||||
Center = data.Center,
|
||||
RadiusMeters = data.RadiusMeters
|
||||
};
|
||||
|
||||
foreach (var b in data.Buildings)
|
||||
{
|
||||
var coords = new double[b.Outline.Count * 2];
|
||||
for (int i = 0; i < b.Outline.Count; i++)
|
||||
{
|
||||
coords[i * 2] = b.Outline[i].Lat;
|
||||
coords[i * 2 + 1] = b.Outline[i].Lon;
|
||||
}
|
||||
payload.Buildings.Add(coords);
|
||||
payload.BuildingTypes.Add(b.BuildingType);
|
||||
}
|
||||
|
||||
foreach (var p in data.Pathways)
|
||||
{
|
||||
var coords = new double[p.Points.Count * 2];
|
||||
for (int i = 0; i < p.Points.Count; i++)
|
||||
{
|
||||
coords[i * 2] = p.Points[i].Lat;
|
||||
coords[i * 2 + 1] = p.Points[i].Lon;
|
||||
}
|
||||
payload.Pathways.Add(coords);
|
||||
payload.PathwayTypes.Add((int)p.PathType);
|
||||
}
|
||||
|
||||
foreach (var a in data.Areas)
|
||||
{
|
||||
var coords = new double[a.Outline.Count * 2];
|
||||
for (int i = 0; i < a.Outline.Count; i++)
|
||||
{
|
||||
coords[i * 2] = a.Outline[i].Lat;
|
||||
coords[i * 2 + 1] = a.Outline[i].Lon;
|
||||
}
|
||||
payload.Areas.Add(coords);
|
||||
payload.AreaTypes.Add((int)a.AreaType);
|
||||
}
|
||||
|
||||
foreach (var poi in data.PointsOfInterest)
|
||||
{
|
||||
payload.POIs.Add(poi.Position.Lat);
|
||||
payload.POIs.Add(poi.Position.Lon);
|
||||
payload.POIs.Add((int)poi.PoiType);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Protokol - Zprávy
|
||||
|
||||
public abstract class Message
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public abstract string Type { get; }
|
||||
|
||||
[JsonPropertyName("clientSeq")]
|
||||
public int ClientSeq { get; set; }
|
||||
|
||||
[JsonPropertyName("actionId")]
|
||||
public string? ActionId { get; set; }
|
||||
}
|
||||
|
||||
// Handshake
|
||||
public class ClientHello : Message
|
||||
{
|
||||
public override string Type => "ClientHello";
|
||||
public string ProtocolVersion { get; set; } = "1.0";
|
||||
public required string ClientUuid { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
|
||||
public class ServerHello : Message
|
||||
{
|
||||
public override string Type => "ServerHello";
|
||||
public required string RsaPublicKeyPem { get; set; }
|
||||
public required string ServerId { get; set; }
|
||||
}
|
||||
|
||||
public class KeyExchange : Message
|
||||
{
|
||||
public override string Type => "KeyExchange";
|
||||
public required string EncryptedSessionKey { get; set; }
|
||||
public required string EncryptedIV { get; set; }
|
||||
}
|
||||
|
||||
public class KeyExchangeAck : Message
|
||||
{
|
||||
public override string Type => "KeyExchangeAck";
|
||||
public string Status { get; set; } = "success";
|
||||
}
|
||||
|
||||
// Lobby
|
||||
public class CreateLobby : Message
|
||||
{
|
||||
public override string Type => "CreateLobby";
|
||||
public string? Password { get; set; }
|
||||
public Position? PlayAreaCenter { get; set; }
|
||||
public double PlayAreaRadius { get; set; } = 500; // metry
|
||||
public int ImpostorCount { get; set; } = 1;
|
||||
public int TaskCount { get; set; } = 5;
|
||||
}
|
||||
|
||||
public class CreateLobbyResponse : Message
|
||||
{
|
||||
public override string Type => "CreateLobbyResponse";
|
||||
public bool Success { get; set; }
|
||||
public string? JoinCode { get; set; }
|
||||
public string? LobbyId { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public LobbyState? LobbyState { get; set; }
|
||||
}
|
||||
|
||||
public class JoinLobby : Message
|
||||
{
|
||||
public override string Type => "JoinLobby";
|
||||
public required string JoinCode { get; set; }
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
|
||||
public class JoinLobbyResponse : Message
|
||||
{
|
||||
public override string Type => "JoinLobbyResponse";
|
||||
public bool Success { get; set; }
|
||||
public string? LobbyId { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public LobbyState? LobbyState { get; set; }
|
||||
}
|
||||
|
||||
public class LeaveLobby : Message
|
||||
{
|
||||
public override string Type => "LeaveLobby";
|
||||
}
|
||||
|
||||
public class KickPlayer : Message
|
||||
{
|
||||
public override string Type => "KickPlayer";
|
||||
public required string TargetClientUuid { get; set; }
|
||||
}
|
||||
|
||||
public class StartGame : Message
|
||||
{
|
||||
public override string Type => "StartGame";
|
||||
}
|
||||
|
||||
public class ReturnToLobby : Message
|
||||
{
|
||||
public override string Type => "ReturnToLobby";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client confirms it received map data and is ready to play
|
||||
/// </summary>
|
||||
public class MapDataReceived : Message
|
||||
{
|
||||
public override string Type => "MapDataReceived";
|
||||
}
|
||||
|
||||
// Hra
|
||||
public class UpdatePosition : Message
|
||||
{
|
||||
public override string Type => "UpdatePosition";
|
||||
public Position Position { get; set; }
|
||||
}
|
||||
|
||||
public class PositionBroadcast : Message
|
||||
{
|
||||
public override string Type => "PositionBroadcast";
|
||||
public List<PlayerPositionInfo> Players { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PlayerPositionInfo
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public Position Position { get; set; }
|
||||
public PlayerState State { get; set; }
|
||||
}
|
||||
|
||||
public class KillAttempt : Message
|
||||
{
|
||||
public override string Type => "KillAttempt";
|
||||
public required string TargetClientUuid { get; set; }
|
||||
}
|
||||
|
||||
public class ReportBody : Message
|
||||
{
|
||||
public override string Type => "ReportBody";
|
||||
public required string BodyId { get; set; }
|
||||
}
|
||||
|
||||
public class CallEmergencyMeeting : Message
|
||||
{
|
||||
public override string Type => "CallEmergencyMeeting";
|
||||
}
|
||||
|
||||
public class CastVote : Message
|
||||
{
|
||||
public override string Type => "CastVote";
|
||||
public string? TargetClientUuid { get; set; } // null = skip
|
||||
}
|
||||
|
||||
public class TaskStart : Message
|
||||
{
|
||||
public override string Type => "TaskStart";
|
||||
public required string TaskId { get; set; }
|
||||
}
|
||||
|
||||
public class TaskProgress : Message
|
||||
{
|
||||
public override string Type => "TaskProgress";
|
||||
public required string TaskId { get; set; }
|
||||
public int Step { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class TaskComplete : Message
|
||||
{
|
||||
public override string Type => "TaskComplete";
|
||||
public required string TaskId { get; set; }
|
||||
}
|
||||
|
||||
// Ping/Heartbeat
|
||||
public class Ping : Message
|
||||
{
|
||||
public override string Type => "Ping";
|
||||
public long ClientTime { get; set; }
|
||||
}
|
||||
|
||||
public class Pong : Message
|
||||
{
|
||||
public override string Type => "Pong";
|
||||
public long ClientTime { get; set; }
|
||||
public long ServerTime { get; set; }
|
||||
}
|
||||
|
||||
// Reconnect
|
||||
public class Reconnect : Message
|
||||
{
|
||||
public override string Type => "Reconnect";
|
||||
public required string LobbyId { get; set; }
|
||||
public long LastEventId { get; set; }
|
||||
}
|
||||
|
||||
public class ReconnectResponse : Message
|
||||
{
|
||||
public override string Type => "ReconnectResponse";
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public LobbySnapshot? Snapshot { get; set; }
|
||||
public List<GameEvent>? MissedEvents { get; set; }
|
||||
}
|
||||
|
||||
// Ack
|
||||
public class Ack : Message
|
||||
{
|
||||
public override string Type => "Ack";
|
||||
public int AckedSeq { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
// Error
|
||||
public class ErrorMessage : Message
|
||||
{
|
||||
public override string Type => "Error";
|
||||
public required string ErrorCode { get; set; }
|
||||
public required string ErrorText { get; set; }
|
||||
}
|
||||
|
||||
// Sabotage messages
|
||||
public class StartSabotage : Message
|
||||
{
|
||||
public override string Type => "StartSabotage";
|
||||
public SabotageType SabotageType { get; set; }
|
||||
public Position? TargetLocation { get; set; } // For zone lockdown
|
||||
}
|
||||
|
||||
public class ActivateRepairStation : Message
|
||||
{
|
||||
public override string Type => "ActivateRepairStation";
|
||||
public required string StationId { get; set; }
|
||||
}
|
||||
|
||||
public class DeactivateRepairStation : Message
|
||||
{
|
||||
public override string Type => "DeactivateRepairStation";
|
||||
public required string StationId { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Eventy
|
||||
|
||||
public class GameEvent : Message
|
||||
{
|
||||
public override string Type => "GameEvent";
|
||||
public long EventId { get; set; }
|
||||
public long ServerSeq { get; set; }
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
public string? Actor { get; set; }
|
||||
public required string EventType { get; set; }
|
||||
public object? Payload { get; set; }
|
||||
}
|
||||
|
||||
// Payload typy pro eventy
|
||||
public class LobbyCreatedPayload
|
||||
{
|
||||
public required string LobbyId { get; set; }
|
||||
public required string JoinCode { get; set; }
|
||||
public required string OwnerId { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerJoinedPayload
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public required string DisplayName { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerLeftPayload
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent when owner clicks StartGame - clients should show loading screen
|
||||
/// </summary>
|
||||
public class GameStartingPayload
|
||||
{
|
||||
public string? InitiatorId { get; set; }
|
||||
public string Message { get; set; } = "Loading map data...";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent when map data is ready and distributed to all clients
|
||||
/// </summary>
|
||||
public class MapDataReadyPayload
|
||||
{
|
||||
public MapDataPayload? MapData { get; set; }
|
||||
public Position PlayAreaCenter { get; set; }
|
||||
public double PlayAreaRadius { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent when a client confirms they received map data
|
||||
/// </summary>
|
||||
public class PlayerMapDataReceivedPayload
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public string DisplayName { get; set; } = "";
|
||||
public int PlayersReady { get; set; }
|
||||
public int TotalPlayers { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent when all clients have confirmed map data and game actually starts
|
||||
/// </summary>
|
||||
public class GameStartedPayload
|
||||
{
|
||||
public int ImpostorCount { get; set; }
|
||||
public int TaskCount { get; set; }
|
||||
}
|
||||
|
||||
public class RoleAssignedPayload
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public PlayerRole Role { get; set; }
|
||||
public List<GameTask>? Tasks { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerKilledPayload
|
||||
{
|
||||
public required string VictimId { get; set; }
|
||||
public required string KillerId { get; set; }
|
||||
public required string BodyId { get; set; }
|
||||
public Position Location { get; set; }
|
||||
}
|
||||
|
||||
public class BodyReportedPayload
|
||||
{
|
||||
public required string ReporterId { get; set; }
|
||||
public required string BodyId { get; set; }
|
||||
public required string VictimId { get; set; }
|
||||
}
|
||||
|
||||
public class EmergencyMeetingCalledPayload
|
||||
{
|
||||
public required string CallerId { get; set; }
|
||||
}
|
||||
|
||||
public class MeetingStartedPayload
|
||||
{
|
||||
public required string MeetingId { get; set; }
|
||||
public MeetingType Type { get; set; }
|
||||
public Position MeetingLocation { get; set; }
|
||||
public DateTime ArrivalDeadline { get; set; }
|
||||
public DateTime? DiscussionEndTime { get; set; }
|
||||
public DateTime VotingEndTime { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerArrivedAtMeetingPayload
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public required string MeetingId { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerVotedPayload
|
||||
{
|
||||
public required string VoterId { get; set; }
|
||||
public string? TargetId { get; set; } // null = skip
|
||||
}
|
||||
|
||||
public class VotingClosedPayload
|
||||
{
|
||||
public Dictionary<string, int> VoteCounts { get; set; } = new();
|
||||
public string? EjectedPlayerId { get; set; }
|
||||
public bool WasTie { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerEjectedPayload
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public PlayerRole Role { get; set; }
|
||||
}
|
||||
|
||||
public class TaskCompletedPayload
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public required string TaskId { get; set; }
|
||||
public int TotalCompleted { get; set; }
|
||||
public int TotalTasks { get; set; }
|
||||
}
|
||||
|
||||
public class GameEndedPayload
|
||||
{
|
||||
public required string WinningFaction { get; set; } // "Crew" nebo "Impostor"
|
||||
public required string Reason { get; set; }
|
||||
public List<string> Winners { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ReturnedToLobbyPayload
|
||||
{
|
||||
public string Message { get; set; } = "";
|
||||
}
|
||||
|
||||
public class HostChangedPayload
|
||||
{
|
||||
public required string NewHostId { get; set; }
|
||||
public required string PreviousHostId { get; set; }
|
||||
}
|
||||
|
||||
public class CheatDetectedPayload
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public required string Violation { get; set; }
|
||||
public int NewCheatScore { get; set; }
|
||||
public CheatStatus NewStatus { get; set; }
|
||||
}
|
||||
|
||||
// Sabotage event payloads
|
||||
public class SabotageStartedPayload
|
||||
{
|
||||
public required string SabotageId { get; set; }
|
||||
public SabotageType Type { get; set; }
|
||||
public required string InitiatorId { get; set; }
|
||||
public DateTime? Deadline { get; set; } // For critical meltdown
|
||||
public List<RepairStationInfo> RepairStations { get; set; } = new();
|
||||
public int RequiredSimultaneousRepairs { get; set; }
|
||||
}
|
||||
|
||||
public class RepairStationInfo
|
||||
{
|
||||
public required string StationId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public Position Location { get; set; }
|
||||
public int RepairDurationMs { get; set; }
|
||||
}
|
||||
|
||||
public class RepairStartedPayload
|
||||
{
|
||||
public required string SabotageId { get; set; }
|
||||
public required string StationId { get; set; }
|
||||
public required string PlayerId { get; set; }
|
||||
}
|
||||
|
||||
public class RepairStoppedPayload
|
||||
{
|
||||
public required string SabotageId { get; set; }
|
||||
public required string StationId { get; set; }
|
||||
public required string PlayerId { get; set; }
|
||||
}
|
||||
|
||||
public class RepairProgressPayload
|
||||
{
|
||||
public required string SabotageId { get; set; }
|
||||
public required string StationId { get; set; }
|
||||
public required string PlayerId { get; set; }
|
||||
public int ProgressMs { get; set; }
|
||||
public int RequiredMs { get; set; }
|
||||
}
|
||||
|
||||
public class SabotageRepairedPayload
|
||||
{
|
||||
public required string SabotageId { get; set; }
|
||||
public SabotageType Type { get; set; }
|
||||
public List<string> RepairerIds { get; set; } = new(); // Players who helped repair
|
||||
}
|
||||
|
||||
public class SabotageMeltdownPayload
|
||||
{
|
||||
public required string SabotageId { get; set; }
|
||||
// Impostor wins - game ends
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
public class LobbyState
|
||||
{
|
||||
public required string LobbyId { get; set; }
|
||||
public required string JoinCode { get; set; }
|
||||
public string? OwnerId { get; set; }
|
||||
public GamePhase Phase { get; set; } = GamePhase.Lobby;
|
||||
public List<PlayerInfo> Players { get; set; } = new();
|
||||
public Position PlayAreaCenter { get; set; }
|
||||
public double PlayAreaRadius { get; set; }
|
||||
public int ImpostorCount { get; set; }
|
||||
public bool HasPassword { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>Map data for client rendering (null if Overpass disabled or failed)</summary>
|
||||
public MapDataPayload? MapData { get; set; }
|
||||
|
||||
/// <summary>True if map data has been loaded (or Overpass is disabled)</summary>
|
||||
public bool MapDataReady { get; set; } = true;
|
||||
}
|
||||
|
||||
public class PlayerInfo
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public required string DisplayName { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
public bool IsReady { get; set; }
|
||||
public PlayerState State { get; set; }
|
||||
// Role je viditelná pouze pro daného hráče, ne v broadcast
|
||||
}
|
||||
|
||||
public class LobbySnapshot
|
||||
{
|
||||
public required string LobbyId { get; set; }
|
||||
public long LastEventId { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public required string Checksum { get; set; }
|
||||
public GamePhase Phase { get; set; }
|
||||
public List<Player> Players { get; set; } = new();
|
||||
public List<Body> Bodies { get; set; } = new();
|
||||
public List<GameTask> Tasks { get; set; } = new();
|
||||
public Meeting? CurrentMeeting { get; set; }
|
||||
public Position PlayAreaCenter { get; set; }
|
||||
public double PlayAreaRadius { get; set; }
|
||||
public int ImpostorCount { get; set; }
|
||||
public TiePolicy TiePolicy { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serializace
|
||||
|
||||
public static class MessageSerializer
|
||||
{
|
||||
private static readonly Dictionary<string, Type> MessageTypes = new()
|
||||
{
|
||||
["ClientHello"] = typeof(ClientHello),
|
||||
["ServerHello"] = typeof(ServerHello),
|
||||
["KeyExchange"] = typeof(KeyExchange),
|
||||
["KeyExchangeAck"] = typeof(KeyExchangeAck),
|
||||
["CreateLobby"] = typeof(CreateLobby),
|
||||
["CreateLobbyResponse"] = typeof(CreateLobbyResponse),
|
||||
["JoinLobby"] = typeof(JoinLobby),
|
||||
["JoinLobbyResponse"] = typeof(JoinLobbyResponse),
|
||||
["LeaveLobby"] = typeof(LeaveLobby),
|
||||
["KickPlayer"] = typeof(KickPlayer),
|
||||
["StartGame"] = typeof(StartGame),
|
||||
["ReturnToLobby"] = typeof(ReturnToLobby),
|
||||
["MapDataReceived"] = typeof(MapDataReceived),
|
||||
["UpdatePosition"] = typeof(UpdatePosition),
|
||||
["PositionBroadcast"] = typeof(PositionBroadcast),
|
||||
["KillAttempt"] = typeof(KillAttempt),
|
||||
["ReportBody"] = typeof(ReportBody),
|
||||
["CallEmergencyMeeting"] = typeof(CallEmergencyMeeting),
|
||||
["CastVote"] = typeof(CastVote),
|
||||
["TaskStart"] = typeof(TaskStart),
|
||||
["TaskProgress"] = typeof(TaskProgress),
|
||||
["TaskComplete"] = typeof(TaskComplete),
|
||||
["Ping"] = typeof(Ping),
|
||||
["Pong"] = typeof(Pong),
|
||||
["Reconnect"] = typeof(Reconnect),
|
||||
["ReconnectResponse"] = typeof(ReconnectResponse),
|
||||
["Ack"] = typeof(Ack),
|
||||
["Error"] = typeof(ErrorMessage),
|
||||
["GameEvent"] = typeof(GameEvent),
|
||||
["StartSabotage"] = typeof(StartSabotage),
|
||||
["ActivateRepairStation"] = typeof(ActivateRepairStation),
|
||||
["DeactivateRepairStation"] = typeof(DeactivateRepairStation)
|
||||
};
|
||||
|
||||
public static byte[] Serialize(Message msg)
|
||||
{
|
||||
return JsonSerializer.SerializeToUtf8Bytes(msg, msg.GetType(), JsonOptions.Default);
|
||||
}
|
||||
|
||||
public static byte[] Serialize(GameEvent evt)
|
||||
{
|
||||
return JsonSerializer.SerializeToUtf8Bytes(evt, JsonOptions.Default);
|
||||
}
|
||||
|
||||
public static Message? Deserialize(ReadOnlySpan<byte> data)
|
||||
{
|
||||
// Nejdřív zjistíme typ
|
||||
using var doc = JsonDocument.Parse(data.ToArray());
|
||||
if (!doc.RootElement.TryGetProperty("type", out var typeProp))
|
||||
return null;
|
||||
|
||||
var typeName = typeProp.GetString();
|
||||
if (typeName == null || !MessageTypes.TryGetValue(typeName, out var type))
|
||||
return null;
|
||||
|
||||
return (Message?)JsonSerializer.Deserialize(data, type, JsonOptions.Default);
|
||||
}
|
||||
|
||||
public static GameEvent? DeserializeEvent(ReadOnlySpan<byte> data)
|
||||
{
|
||||
return JsonSerializer.Deserialize<GameEvent>(data, JsonOptions.Default);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
17
Server.csproj
Normal file
17
Server.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>GeoSus.Server</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
327
StatsDb.cs
Normal file
327
StatsDb.cs
Normal file
@@ -0,0 +1,327 @@
|
||||
namespace GeoSus.Server;
|
||||
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// SQLite databáze pro statistiky hráčů
|
||||
public class StatsDb : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly ILogger<StatsDb> _logger;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public StatsDb(string dbPath, ILogger<StatsDb> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
var directory = Path.GetDirectoryName(dbPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
_connection = new SqliteConnection($"Data Source={dbPath}");
|
||||
_connection.Open();
|
||||
|
||||
InitializeSchema();
|
||||
}
|
||||
|
||||
private void InitializeSchema()
|
||||
{
|
||||
var sql = @"
|
||||
CREATE TABLE IF NOT EXISTS player_stats (
|
||||
client_uuid TEXT PRIMARY KEY,
|
||||
display_name TEXT,
|
||||
total_games INTEGER DEFAULT 0,
|
||||
games_as_crew INTEGER DEFAULT 0,
|
||||
games_as_impostor INTEGER DEFAULT 0,
|
||||
crew_wins INTEGER DEFAULT 0,
|
||||
impostor_wins INTEGER DEFAULT 0,
|
||||
total_kills INTEGER DEFAULT 0,
|
||||
total_deaths INTEGER DEFAULT 0,
|
||||
tasks_completed INTEGER DEFAULT 0,
|
||||
bodies_reported INTEGER DEFAULT 0,
|
||||
emergency_meetings_called INTEGER DEFAULT 0,
|
||||
times_voted_out INTEGER DEFAULT 0,
|
||||
successful_votes INTEGER DEFAULT 0,
|
||||
total_playtime_seconds INTEGER DEFAULT 0,
|
||||
cheat_incidents INTEGER DEFAULT 0,
|
||||
last_seen_utc TEXT,
|
||||
created_at_utc TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_crew_wins ON player_stats(crew_wins DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_impostor_wins ON player_stats(impostor_wins DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_total_games ON player_stats(total_games DESC);
|
||||
";
|
||||
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
_logger.LogInformation("StatsDb inicializována");
|
||||
}
|
||||
|
||||
#region CRUD operace
|
||||
|
||||
public void EnsurePlayerExists(string clientUuid, string displayName)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var sql = @"
|
||||
INSERT INTO player_stats (client_uuid, display_name, created_at_utc, last_seen_utc)
|
||||
VALUES (@uuid, @name, @now, @now)
|
||||
ON CONFLICT(client_uuid) DO UPDATE SET
|
||||
display_name = @name,
|
||||
last_seen_utc = @now
|
||||
";
|
||||
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.AddWithValue("@uuid", clientUuid);
|
||||
cmd.Parameters.AddWithValue("@name", displayName);
|
||||
cmd.Parameters.AddWithValue("@now", DateTime.UtcNow.ToString("o"));
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerStats? GetPlayerStats(string clientUuid)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var sql = "SELECT * FROM player_stats WHERE client_uuid = @uuid";
|
||||
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.AddWithValue("@uuid", clientUuid);
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
if (reader.Read())
|
||||
{
|
||||
return ReadPlayerStats(reader);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<PlayerStats> GetLeaderboard(string sortBy, int limit = 100)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var validColumns = new[] {
|
||||
"crew_wins", "impostor_wins", "total_games",
|
||||
"total_kills", "tasks_completed"
|
||||
};
|
||||
|
||||
if (!validColumns.Contains(sortBy))
|
||||
sortBy = "total_games";
|
||||
|
||||
var sql = $"SELECT * FROM player_stats ORDER BY {sortBy} DESC LIMIT @limit";
|
||||
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.AddWithValue("@limit", limit);
|
||||
|
||||
var results = new List<PlayerStats>();
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
results.Add(ReadPlayerStats(reader));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Inkrementální aktualizace
|
||||
|
||||
public void IncrementKills(string clientUuid)
|
||||
{
|
||||
IncrementColumn(clientUuid, "total_kills");
|
||||
}
|
||||
|
||||
public void IncrementDeaths(string clientUuid)
|
||||
{
|
||||
IncrementColumn(clientUuid, "total_deaths");
|
||||
}
|
||||
|
||||
public void IncrementTasksCompleted(string clientUuid)
|
||||
{
|
||||
IncrementColumn(clientUuid, "tasks_completed");
|
||||
}
|
||||
|
||||
public void IncrementBodiesReported(string clientUuid)
|
||||
{
|
||||
IncrementColumn(clientUuid, "bodies_reported");
|
||||
}
|
||||
|
||||
public void IncrementEmergencyMeetings(string clientUuid)
|
||||
{
|
||||
IncrementColumn(clientUuid, "emergency_meetings_called");
|
||||
}
|
||||
|
||||
public void IncrementTimesVotedOut(string clientUuid)
|
||||
{
|
||||
IncrementColumn(clientUuid, "times_voted_out");
|
||||
}
|
||||
|
||||
public void IncrementSuccessfulVotes(string clientUuid)
|
||||
{
|
||||
IncrementColumn(clientUuid, "successful_votes");
|
||||
}
|
||||
|
||||
public void IncrementCheatIncidents(string clientUuid)
|
||||
{
|
||||
IncrementColumn(clientUuid, "cheat_incidents");
|
||||
}
|
||||
|
||||
private void IncrementColumn(string clientUuid, string column)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var sql = $@"
|
||||
UPDATE player_stats
|
||||
SET {column} = {column} + 1, last_seen_utc = @now
|
||||
WHERE client_uuid = @uuid
|
||||
";
|
||||
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.AddWithValue("@uuid", clientUuid);
|
||||
cmd.Parameters.AddWithValue("@now", DateTime.UtcNow.ToString("o"));
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Game end aktualizace
|
||||
|
||||
public void RecordGameEnd(GameEndStats stats)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
using var transaction = _connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var player in stats.Players)
|
||||
{
|
||||
var sql = @"
|
||||
UPDATE player_stats SET
|
||||
total_games = total_games + 1,
|
||||
games_as_crew = games_as_crew + @asCrew,
|
||||
games_as_impostor = games_as_impostor + @asImpostor,
|
||||
crew_wins = crew_wins + @crewWin,
|
||||
impostor_wins = impostor_wins + @impostorWin,
|
||||
total_playtime_seconds = total_playtime_seconds + @playtime,
|
||||
last_seen_utc = @now
|
||||
WHERE client_uuid = @uuid
|
||||
";
|
||||
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.AddWithValue("@uuid", player.ClientUuid);
|
||||
cmd.Parameters.AddWithValue("@asCrew", player.WasCrew ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("@asImpostor", player.WasImpostor ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("@crewWin", player.WasCrew && stats.CrewWon ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("@impostorWin", player.WasImpostor && !stats.CrewWon ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("@playtime", player.PlaytimeSeconds);
|
||||
cmd.Parameters.AddWithValue("@now", DateTime.UtcNow.ToString("o"));
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Game stats uloženy pro {Count} hráčů", stats.Players.Count);
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private PlayerStats ReadPlayerStats(SqliteDataReader reader)
|
||||
{
|
||||
return new PlayerStats
|
||||
{
|
||||
ClientUuid = reader.GetString(reader.GetOrdinal("client_uuid")),
|
||||
DisplayName = reader.IsDBNull(reader.GetOrdinal("display_name"))
|
||||
? null : reader.GetString(reader.GetOrdinal("display_name")),
|
||||
TotalGames = reader.GetInt32(reader.GetOrdinal("total_games")),
|
||||
GamesAsCrew = reader.GetInt32(reader.GetOrdinal("games_as_crew")),
|
||||
GamesAsImpostor = reader.GetInt32(reader.GetOrdinal("games_as_impostor")),
|
||||
CrewWins = reader.GetInt32(reader.GetOrdinal("crew_wins")),
|
||||
ImpostorWins = reader.GetInt32(reader.GetOrdinal("impostor_wins")),
|
||||
TotalKills = reader.GetInt32(reader.GetOrdinal("total_kills")),
|
||||
TotalDeaths = reader.GetInt32(reader.GetOrdinal("total_deaths")),
|
||||
TasksCompleted = reader.GetInt32(reader.GetOrdinal("tasks_completed")),
|
||||
BodiesReported = reader.GetInt32(reader.GetOrdinal("bodies_reported")),
|
||||
EmergencyMeetingsCalled = reader.GetInt32(reader.GetOrdinal("emergency_meetings_called")),
|
||||
TimesVotedOut = reader.GetInt32(reader.GetOrdinal("times_voted_out")),
|
||||
SuccessfulVotes = reader.GetInt32(reader.GetOrdinal("successful_votes")),
|
||||
TotalPlaytimeSeconds = reader.GetInt32(reader.GetOrdinal("total_playtime_seconds")),
|
||||
CheatIncidents = reader.GetInt32(reader.GetOrdinal("cheat_incidents")),
|
||||
LastSeenUtc = reader.IsDBNull(reader.GetOrdinal("last_seen_utc"))
|
||||
? null : reader.GetString(reader.GetOrdinal("last_seen_utc")),
|
||||
CreatedAtUtc = reader.IsDBNull(reader.GetOrdinal("created_at_utc"))
|
||||
? null : reader.GetString(reader.GetOrdinal("created_at_utc"))
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connection.Close();
|
||||
_connection.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#region DTO
|
||||
|
||||
public class PlayerStats
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public int TotalGames { get; set; }
|
||||
public int GamesAsCrew { get; set; }
|
||||
public int GamesAsImpostor { get; set; }
|
||||
public int CrewWins { get; set; }
|
||||
public int ImpostorWins { get; set; }
|
||||
public int TotalKills { get; set; }
|
||||
public int TotalDeaths { get; set; }
|
||||
public int TasksCompleted { get; set; }
|
||||
public int BodiesReported { get; set; }
|
||||
public int EmergencyMeetingsCalled { get; set; }
|
||||
public int TimesVotedOut { get; set; }
|
||||
public int SuccessfulVotes { get; set; }
|
||||
public int TotalPlaytimeSeconds { get; set; }
|
||||
public int CheatIncidents { get; set; }
|
||||
public string? LastSeenUtc { get; set; }
|
||||
public string? CreatedAtUtc { get; set; }
|
||||
|
||||
// Computed properties pro API
|
||||
public double CrewWinRate => GamesAsCrew > 0 ? (double)CrewWins / GamesAsCrew : 0;
|
||||
public double ImpostorWinRate => GamesAsImpostor > 0 ? (double)ImpostorWins / GamesAsImpostor : 0;
|
||||
public double KillDeathRatio => TotalDeaths > 0 ? (double)TotalKills / TotalDeaths : TotalKills;
|
||||
public double AverageTasksPerGame => TotalGames > 0 ? (double)TasksCompleted / TotalGames : 0;
|
||||
}
|
||||
|
||||
public class GameEndStats
|
||||
{
|
||||
public bool CrewWon { get; set; }
|
||||
public List<PlayerGameStats> Players { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PlayerGameStats
|
||||
{
|
||||
public required string ClientUuid { get; set; }
|
||||
public bool WasCrew { get; set; }
|
||||
public bool WasImpostor { get; set; }
|
||||
public int PlaytimeSeconds { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
42
apache-vhost.conf
Normal file
42
apache-vhost.conf
Normal file
@@ -0,0 +1,42 @@
|
||||
# GeoSus Admin Panel - Apache Virtual Host
|
||||
#
|
||||
# Required modules:
|
||||
# sudo a2enmod proxy proxy_http proxy_wstunnel rewrite headers ssl
|
||||
#
|
||||
# Installation:
|
||||
# 1. sudo cp apache-vhost.conf /etc/apache2/sites-available/geosus.conf
|
||||
# 2. sudo a2ensite geosus && sudo systemctl reload apache2
|
||||
# 3. sudo certbot --apache -d geosus.honzuvkod.dev
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName geosus.honzuvkod.dev
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/geosus_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/geosus_access.log combined
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyRequests Off
|
||||
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/admin/ws/(.*)$ ws://127.0.0.1:8088/admin/ws/$1 [P,L]
|
||||
|
||||
ProxyPass /admin http://127.0.0.1:8088/admin
|
||||
ProxyPassReverse /admin http://127.0.0.1:8088/admin
|
||||
|
||||
ProxyPass /api http://127.0.0.1:8088/api
|
||||
ProxyPassReverse /api http://127.0.0.1:8088/api
|
||||
|
||||
ProxyPass /health http://127.0.0.1:8088/health
|
||||
ProxyPassReverse /health http://127.0.0.1:8088/health
|
||||
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
ProxyTimeout 3600
|
||||
SetEnv proxy-nokeepalive 0
|
||||
SetEnv proxy-initial-not-pooled 1
|
||||
</VirtualHost>
|
||||
45
appsettings.json
Normal file
45
appsettings.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"tcpPort": 7777,
|
||||
"httpPort": 8088,
|
||||
"maxPacketSizeBytes": 1048576,
|
||||
"tickMs": 200,
|
||||
"positionBroadcastRateMs": 1000,
|
||||
"maxSpeedMps": 12.0,
|
||||
"movementValidationWindowSec": 5.0,
|
||||
"teleportThresholdMeters": 50.0,
|
||||
"cheatScoreWarnThreshold": 10,
|
||||
"cheatScoreRestrictThreshold": 25,
|
||||
"cheatScoreKickThreshold": 50,
|
||||
"killDistanceM": 10.0,
|
||||
"killCooldownMs": 5000,
|
||||
"meetingArrivalRadiusM": 15.0,
|
||||
"arrivalBaseMs": 20000,
|
||||
"arrivalSafetyMarginMs": 3000,
|
||||
"allowedLateMs": 5000,
|
||||
"discussionPhaseMs": 15000,
|
||||
"votingPhaseMs": 30000,
|
||||
"emergencyMeetingCooldownMs": 30000,
|
||||
"maxEmergencyMeetingsPerPlayer": 2,
|
||||
"emergencyMeetingCallRadiusM": 15.0,
|
||||
"reportDistanceM": 10.0,
|
||||
"taskStartDistanceM": 5.0,
|
||||
"taskLeaveDebounceMs": 2000,
|
||||
"taskProgressKeepaliveMs": 5000,
|
||||
"snapshotEvents": 200,
|
||||
"snapshotIntervalMs": 300000,
|
||||
"walMaxSizeMb": 10,
|
||||
"dataPath": "data",
|
||||
"hostTimeoutMs": 15000,
|
||||
"reconnectWindowMs": 60000,
|
||||
"idleLobbyTtlMs": 3600000,
|
||||
"joinCodeTtlMs": 86400000,
|
||||
"maxPlayersPerLobby": 15,
|
||||
"joinRateLimitPerMinute": 10,
|
||||
"sessionKeySizeBytes": 32,
|
||||
"rsaKeySizeBits": 2048,
|
||||
"statsApiRateLimit": 100,
|
||||
"statsApiKey": null,
|
||||
"defaultImpostorCount": 1,
|
||||
"defaultTaskCount": 5,
|
||||
"defaultTiePolicy": "NoEject"
|
||||
}
|
||||
BIN
bin/Debug/net8.0/Microsoft.Data.Sqlite.dll
Normal file
BIN
bin/Debug/net8.0/Microsoft.Data.Sqlite.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/SQLitePCLRaw.batteries_v2.dll
Normal file
BIN
bin/Debug/net8.0/SQLitePCLRaw.batteries_v2.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/SQLitePCLRaw.core.dll
Normal file
BIN
bin/Debug/net8.0/SQLitePCLRaw.core.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll
Normal file
BIN
bin/Debug/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll
Normal file
Binary file not shown.
220
bin/Debug/net8.0/Server.deps.json
Normal file
220
bin/Debug/net8.0/Server.deps.json
Normal file
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"Server/1.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite": "8.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Server.dll": {}
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite/8.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "8.0.0",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.6"
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core/8.0.0": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.Data.Sqlite.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.23.53103"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.6",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/SQLitePCLRaw.batteries_v2.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core/2.1.6": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/SQLitePCLRaw.core.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
|
||||
"runtimeTargets": {
|
||||
"runtimes/browser-wasm/nativeassets/net8.0/e_sqlite3.a": {
|
||||
"rid": "browser-wasm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-arm/native/libe_sqlite3.so": {
|
||||
"rid": "linux-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-arm64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-armel/native/libe_sqlite3.so": {
|
||||
"rid": "linux-armel",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-mips64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-mips64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-arm/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-arm64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-x64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-ppc64le/native/libe_sqlite3.so": {
|
||||
"rid": "linux-ppc64le",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-s390x/native/libe_sqlite3.so": {
|
||||
"rid": "linux-s390x",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-x64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-x86/native/libe_sqlite3.so": {
|
||||
"rid": "linux-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/maccatalyst-arm64/native/libe_sqlite3.dylib": {
|
||||
"rid": "maccatalyst-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/maccatalyst-x64/native/libe_sqlite3.dylib": {
|
||||
"rid": "maccatalyst-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-arm64/native/libe_sqlite3.dylib": {
|
||||
"rid": "osx-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-x64/native/libe_sqlite3.dylib": {
|
||||
"rid": "osx-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-arm/native/e_sqlite3.dll": {
|
||||
"rid": "win-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/e_sqlite3.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x64/native/e_sqlite3.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x86/native/e_sqlite3.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/SQLitePCLRaw.provider.e_sqlite3.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Server/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.Data.Sqlite/8.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-H+iC5IvkCCKSNHXzL3JARvDn7VpkvuJM91KVB89sKjeTF/KX/BocNNh93ZJtX5MCQKb/z4yVKgkU2sVIq+xKfg==",
|
||||
"path": "microsoft.data.sqlite/8.0.0",
|
||||
"hashPath": "microsoft.data.sqlite.8.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core/8.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-pujbzfszX7jAl7oTbHhqx7pxd9jibeyHHl8zy1gd55XMaKWjDtc5XhhNYwQnrwWYCInNdVoArbaaAvLgW7TwuA==",
|
||||
"path": "microsoft.data.sqlite.core/8.0.0",
|
||||
"hashPath": "microsoft.data.sqlite.core.8.0.0.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==",
|
||||
"path": "sqlitepclraw.bundle_e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.bundle_e_sqlite3.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.core/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==",
|
||||
"path": "sqlitepclraw.core/2.1.6",
|
||||
"hashPath": "sqlitepclraw.core.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==",
|
||||
"path": "sqlitepclraw.lib.e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.lib.e_sqlite3.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==",
|
||||
"path": "sqlitepclraw.provider.e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.provider.e_sqlite3.2.1.6.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
bin/Debug/net8.0/Server.dll
Normal file
BIN
bin/Debug/net8.0/Server.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/Server.exe
Normal file
BIN
bin/Debug/net8.0/Server.exe
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/Server.pdb
Normal file
BIN
bin/Debug/net8.0/Server.pdb
Normal file
Binary file not shown.
19
bin/Debug/net8.0/Server.runtimeconfig.json
Normal file
19
bin/Debug/net8.0/Server.runtimeconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.GC.Server": true,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
1
bin/Debug/net8.0/Server.staticwebassets.endpoints.json
Normal file
1
bin/Debug/net8.0/Server.staticwebassets.endpoints.json
Normal file
@@ -0,0 +1 @@
|
||||
{"Version":1,"ManifestType":"Build","Endpoints":[]}
|
||||
44
bin/Debug/net8.0/appsettings.json
Normal file
44
bin/Debug/net8.0/appsettings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"tcpPort": 7777,
|
||||
"httpPort": 8080,
|
||||
"maxPacketSizeBytes": 1048576,
|
||||
"tickMs": 200,
|
||||
"positionBroadcastRateMs": 1000,
|
||||
"maxSpeedMps": 12.0,
|
||||
"movementValidationWindowSec": 5.0,
|
||||
"teleportThresholdMeters": 50.0,
|
||||
"cheatScoreWarnThreshold": 10,
|
||||
"cheatScoreRestrictThreshold": 25,
|
||||
"cheatScoreKickThreshold": 50,
|
||||
"killDistanceM": 10.0,
|
||||
"killCooldownMs": 30000,
|
||||
"meetingArrivalRadiusM": 15.0,
|
||||
"arrivalBaseMs": 30000,
|
||||
"arrivalSafetyMarginMs": 500,
|
||||
"allowedLateMs": 2000,
|
||||
"discussionPhaseMs": 30000,
|
||||
"votingPhaseMs": 45000,
|
||||
"emergencyMeetingCooldownMs": 60000,
|
||||
"maxEmergencyMeetingsPerPlayer": 1,
|
||||
"reportDistanceM": 5.0,
|
||||
"taskStartDistanceM": 3.0,
|
||||
"taskLeaveDebounceMs": 2000,
|
||||
"taskProgressKeepaliveMs": 5000,
|
||||
"snapshotEvents": 200,
|
||||
"snapshotIntervalMs": 300000,
|
||||
"walMaxSizeMb": 10,
|
||||
"dataPath": "data",
|
||||
"hostTimeoutMs": 15000,
|
||||
"reconnectWindowMs": 60000,
|
||||
"idleLobbyTtlMs": 3600000,
|
||||
"joinCodeTtlMs": 86400000,
|
||||
"maxPlayersPerLobby": 15,
|
||||
"joinRateLimitPerMinute": 10,
|
||||
"sessionKeySizeBytes": 32,
|
||||
"rsaKeySizeBits": 2048,
|
||||
"statsApiRateLimit": 100,
|
||||
"statsApiKey": null,
|
||||
"defaultImpostorCount": 1,
|
||||
"defaultTaskCount": 5,
|
||||
"defaultTiePolicy": "NoEject"
|
||||
}
|
||||
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-arm/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-arm/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-arm64/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-arm64/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-armel/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-armel/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-mips64/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-mips64/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-musl-arm/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-musl-arm/native/libe_sqlite3.so
Normal file
Binary file not shown.
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-musl-x64/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-musl-x64/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-ppc64le/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-ppc64le/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-s390x/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-s390x/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-x64/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-x64/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/linux-x86/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net8.0/runtimes/linux-x86/native/libe_sqlite3.so
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/osx-arm64/native/libe_sqlite3.dylib
Normal file
BIN
bin/Debug/net8.0/runtimes/osx-arm64/native/libe_sqlite3.dylib
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/osx-x64/native/libe_sqlite3.dylib
Normal file
BIN
bin/Debug/net8.0/runtimes/osx-x64/native/libe_sqlite3.dylib
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/win-arm/native/e_sqlite3.dll
Normal file
BIN
bin/Debug/net8.0/runtimes/win-arm/native/e_sqlite3.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/win-arm64/native/e_sqlite3.dll
Normal file
BIN
bin/Debug/net8.0/runtimes/win-arm64/native/e_sqlite3.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/win-x64/native/e_sqlite3.dll
Normal file
BIN
bin/Debug/net8.0/runtimes/win-x64/native/e_sqlite3.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net8.0/runtimes/win-x86/native/e_sqlite3.dll
Normal file
BIN
bin/Debug/net8.0/runtimes/win-x86/native/e_sqlite3.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net9.0/Microsoft.Data.Sqlite.dll
Normal file
BIN
bin/Debug/net9.0/Microsoft.Data.Sqlite.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net9.0/SQLitePCLRaw.batteries_v2.dll
Normal file
BIN
bin/Debug/net9.0/SQLitePCLRaw.batteries_v2.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net9.0/SQLitePCLRaw.core.dll
Normal file
BIN
bin/Debug/net9.0/SQLitePCLRaw.core.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net9.0/SQLitePCLRaw.provider.e_sqlite3.dll
Normal file
BIN
bin/Debug/net9.0/SQLitePCLRaw.provider.e_sqlite3.dll
Normal file
Binary file not shown.
226
bin/Debug/net9.0/Server.deps.json
Normal file
226
bin/Debug/net9.0/Server.deps.json
Normal file
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v9.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v9.0": {
|
||||
"Server/1.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite": "9.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Server.dll": {}
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite/9.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "9.0.0",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.10",
|
||||
"SQLitePCLRaw.core": "2.1.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core/9.0.0": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.Data.Sqlite.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.24.52902"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.10": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.10",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/SQLitePCLRaw.batteries_v2.dll": {
|
||||
"assemblyVersion": "2.1.10.2445",
|
||||
"fileVersion": "2.1.10.2445"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core/2.1.10": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/SQLitePCLRaw.core.dll": {
|
||||
"assemblyVersion": "2.1.10.2445",
|
||||
"fileVersion": "2.1.10.2445"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3/2.1.10": {
|
||||
"runtimeTargets": {
|
||||
"runtimes/browser-wasm/nativeassets/net9.0/e_sqlite3.a": {
|
||||
"rid": "browser-wasm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-arm/native/libe_sqlite3.so": {
|
||||
"rid": "linux-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-arm64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-armel/native/libe_sqlite3.so": {
|
||||
"rid": "linux-armel",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-mips64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-mips64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-arm/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-arm64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-s390x/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-s390x",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-x64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-ppc64le/native/libe_sqlite3.so": {
|
||||
"rid": "linux-ppc64le",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-s390x/native/libe_sqlite3.so": {
|
||||
"rid": "linux-s390x",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-x64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-x86/native/libe_sqlite3.so": {
|
||||
"rid": "linux-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/maccatalyst-arm64/native/libe_sqlite3.dylib": {
|
||||
"rid": "maccatalyst-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/maccatalyst-x64/native/libe_sqlite3.dylib": {
|
||||
"rid": "maccatalyst-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-arm64/native/libe_sqlite3.dylib": {
|
||||
"rid": "osx-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-x64/native/libe_sqlite3.dylib": {
|
||||
"rid": "osx-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-arm/native/e_sqlite3.dll": {
|
||||
"rid": "win-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/e_sqlite3.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x64/native/e_sqlite3.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x86/native/e_sqlite3.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3/2.1.10": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/SQLitePCLRaw.provider.e_sqlite3.dll": {
|
||||
"assemblyVersion": "2.1.10.2445",
|
||||
"fileVersion": "2.1.10.2445"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Server/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.Data.Sqlite/9.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-lw6wthgXGx3r/U775k1UkUAWIn0kAT0wj4ZRq0WlhPx4WAOiBsIjgDKgWkXcNTGT0KfHiClkM+tyPVFDvxeObw==",
|
||||
"path": "microsoft.data.sqlite/9.0.0",
|
||||
"hashPath": "microsoft.data.sqlite.9.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core/9.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-cFfZjFL+tqzGYw9lB31EkV1IWF5xRQNk2k+MQd+Cf86Gl6zTeAoiZIFw5sRB1Z8OxpEC7nu+nTDsLSjieBAPTw==",
|
||||
"path": "microsoft.data.sqlite.core/9.0.0",
|
||||
"hashPath": "microsoft.data.sqlite.core.9.0.0.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-UxWuisvZ3uVcVOLJQv7urM/JiQH+v3TmaJc1BLKl5Dxfm/nTzTUrqswCqg/INiYLi61AXnHo1M1JPmPqqLnAdg==",
|
||||
"path": "sqlitepclraw.bundle_e_sqlite3/2.1.10",
|
||||
"hashPath": "sqlitepclraw.bundle_e_sqlite3.2.1.10.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.core/2.1.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Ii8JCbC7oiVclaE/mbDEK000EFIJ+ShRPwAvvV89GOZhQ+ZLtlnSWl6ksCNMKu/VGXA4Nfi2B7LhN/QFN9oBcw==",
|
||||
"path": "sqlitepclraw.core/2.1.10",
|
||||
"hashPath": "sqlitepclraw.core.2.1.10.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3/2.1.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==",
|
||||
"path": "sqlitepclraw.lib.e_sqlite3/2.1.10",
|
||||
"hashPath": "sqlitepclraw.lib.e_sqlite3.2.1.10.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3/2.1.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-uZVTi02C1SxqzgT0HqTWatIbWGb40iIkfc3FpFCpE/r7g6K0PqzDUeefL6P6HPhDtc6BacN3yQysfzP7ks+wSQ==",
|
||||
"path": "sqlitepclraw.provider.e_sqlite3/2.1.10",
|
||||
"hashPath": "sqlitepclraw.provider.e_sqlite3.2.1.10.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
bin/Debug/net9.0/Server.dll
Normal file
BIN
bin/Debug/net9.0/Server.dll
Normal file
Binary file not shown.
BIN
bin/Debug/net9.0/Server.exe
Normal file
BIN
bin/Debug/net9.0/Server.exe
Normal file
Binary file not shown.
BIN
bin/Debug/net9.0/Server.pdb
Normal file
BIN
bin/Debug/net9.0/Server.pdb
Normal file
Binary file not shown.
19
bin/Debug/net9.0/Server.runtimeconfig.json
Normal file
19
bin/Debug/net9.0/Server.runtimeconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net9.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "9.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App",
|
||||
"version": "9.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.GC.Server": true,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
1
bin/Debug/net9.0/Server.staticwebassets.endpoints.json
Normal file
1
bin/Debug/net9.0/Server.staticwebassets.endpoints.json
Normal file
@@ -0,0 +1 @@
|
||||
{"Version":1,"ManifestType":"Build","Endpoints":[]}
|
||||
45
bin/Debug/net9.0/appsettings.json
Normal file
45
bin/Debug/net9.0/appsettings.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"tcpPort": 7777,
|
||||
"httpPort": 8088,
|
||||
"maxPacketSizeBytes": 1048576,
|
||||
"tickMs": 200,
|
||||
"positionBroadcastRateMs": 1000,
|
||||
"maxSpeedMps": 12.0,
|
||||
"movementValidationWindowSec": 5.0,
|
||||
"teleportThresholdMeters": 50.0,
|
||||
"cheatScoreWarnThreshold": 10,
|
||||
"cheatScoreRestrictThreshold": 25,
|
||||
"cheatScoreKickThreshold": 50,
|
||||
"killDistanceM": 10.0,
|
||||
"killCooldownMs": 5000,
|
||||
"meetingArrivalRadiusM": 15.0,
|
||||
"arrivalBaseMs": 20000,
|
||||
"arrivalSafetyMarginMs": 3000,
|
||||
"allowedLateMs": 5000,
|
||||
"discussionPhaseMs": 15000,
|
||||
"votingPhaseMs": 30000,
|
||||
"emergencyMeetingCooldownMs": 30000,
|
||||
"maxEmergencyMeetingsPerPlayer": 2,
|
||||
"emergencyMeetingCallRadiusM": 15.0,
|
||||
"reportDistanceM": 10.0,
|
||||
"taskStartDistanceM": 5.0,
|
||||
"taskLeaveDebounceMs": 2000,
|
||||
"taskProgressKeepaliveMs": 5000,
|
||||
"snapshotEvents": 200,
|
||||
"snapshotIntervalMs": 300000,
|
||||
"walMaxSizeMb": 10,
|
||||
"dataPath": "data",
|
||||
"hostTimeoutMs": 15000,
|
||||
"reconnectWindowMs": 60000,
|
||||
"idleLobbyTtlMs": 3600000,
|
||||
"joinCodeTtlMs": 86400000,
|
||||
"maxPlayersPerLobby": 15,
|
||||
"joinRateLimitPerMinute": 10,
|
||||
"sessionKeySizeBytes": 32,
|
||||
"rsaKeySizeBits": 2048,
|
||||
"statsApiRateLimit": 100,
|
||||
"statsApiKey": null,
|
||||
"defaultImpostorCount": 1,
|
||||
"defaultTaskCount": 5,
|
||||
"defaultTiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"lobbyId": "0ef46de0570a4be6",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T13:41:27.5170822Z",
|
||||
"checksum": "eaeb904e566968a23b9fad8defde721f0a958b5173f671f96b67463cd102e5b8",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.0868023,
|
||||
"lon": 14.4203088
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7851,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.0855253,
|
||||
"lon": 14.4216649
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.087640820000004,
|
||||
"lon": 14.42321536
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9934,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.0862119,
|
||||
"lon": 14.4239779
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7287,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.08971575714286,
|
||||
"lon": 14.420300657142858
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lobbyId": "27685b18526d4ce5",
|
||||
"lastEventId": 7,
|
||||
"timestamp": "2026-01-27T13:36:27.5323111Z",
|
||||
"checksum": "cc270b3bd115ec20db477f4b58e987b0b4a0bede6b35d5fb8786f1a3ad5a89bf",
|
||||
"phase": "Lobby",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"lobbyId": "34d8737b80434782",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T13:36:27.5253055Z",
|
||||
"checksum": "ec7a8cc5ca949945cac59b72af28e810b5db1ed59683d8840aeb5293c21154e6",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.0894098,
|
||||
"lon": 14.4226833
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.08879728000001,
|
||||
"lon": 14.41972866
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.0879498,
|
||||
"lon": 14.4233898
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.0865062,
|
||||
"lon": 14.4206464
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5593,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.0883246,
|
||||
"lon": 14.4238715
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"lobbyId": "5d096533c3094eaa",
|
||||
"lastEventId": 51,
|
||||
"timestamp": "2026-01-27T13:31:27.5287743Z",
|
||||
"checksum": "c3eb831a207d599f3fbf61d89359c0c36b3d787fdb339fb36766c4cbf1f52462",
|
||||
"phase": "Ended",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 49.9999798,
|
||||
"lon": 14.0026563
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 49.9996681,
|
||||
"lon": 14.0003986
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8093,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.0031177,
|
||||
"lon": 13.998514216666669
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50,
|
||||
"lon": 14
|
||||
},
|
||||
"playAreaRadius": 500,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lobbyId": "6772d80dd8a44352",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T11:37:11.3093918Z",
|
||||
"checksum": "2c421294fe51e01d08fe09e59f84c8a0c1e73120cb2764eb503b6c24cfb94d8e",
|
||||
"phase": "Lobby",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50,
|
||||
"lon": 14
|
||||
},
|
||||
"playAreaRadius": 500,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"lobbyId": "6af233ecf68b4434",
|
||||
"lastEventId": 4,
|
||||
"timestamp": "2026-01-27T13:41:27.5355764Z",
|
||||
"checksum": "a14cbc3f62b3a9d629f3852bc29e2d6e5460d7c7c5db39eae124537a56bc9fd5",
|
||||
"phase": "Lobby",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "4cda16e3",
|
||||
"displayName": "SabPlayer",
|
||||
"position": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T12:38:01.2482361Z",
|
||||
"lastPositionUpdate": "2026-01-27T12:38:01.2482361Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": []
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"lobbyId": "6dba8c0edbe7490d",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T13:41:27.5240463Z",
|
||||
"checksum": "e006a9e9b5a9473600b72662bb8ee30ed1d8f5896538ca22976917c77ecd88e2",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.0877076,
|
||||
"lon": 14.4241423
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6709,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.089346660000004,
|
||||
"lon": 14.42227354
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.087235,
|
||||
"lon": 14.4177676
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.0872447,
|
||||
"lon": 14.4252718
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.08679,
|
||||
"lon": 14.4209933
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lobbyId": "74771a6153544040",
|
||||
"lastEventId": 7,
|
||||
"timestamp": "2026-01-27T13:41:27.5390519Z",
|
||||
"checksum": "c4133709107e0aa5b7b36b09e2275c68306c8b0ba9f149dd2a7acc8af4841d72",
|
||||
"phase": "Lobby",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lobbyId": "7da1c15116fc4fbc",
|
||||
"lastEventId": 7,
|
||||
"timestamp": "2026-01-27T13:36:27.528593Z",
|
||||
"checksum": "4536b2249e71f0a79258b5a3861a0e4868ad592170b159404031f315105966a5",
|
||||
"phase": "Lobby",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"lobbyId": "7f234630f0d3470c",
|
||||
"lastEventId": 51,
|
||||
"timestamp": "2026-01-27T13:41:27.5227014Z",
|
||||
"checksum": "f52e66c8c122229b9ed6b67b4a965540971b421294a9adf54f2295ea6206da80",
|
||||
"phase": "Ended",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.0004706,
|
||||
"lon": 14.0066172
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5753,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.0030234,
|
||||
"lon": 13.9993778
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.0030134625,
|
||||
"lon": 13.999101749999998
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50,
|
||||
"lon": 14
|
||||
},
|
||||
"playAreaRadius": 500,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lobbyId": "8da0b3e582444daf",
|
||||
"lastEventId": 7,
|
||||
"timestamp": "2026-01-27T13:41:27.5200488Z",
|
||||
"checksum": "926ef187cbeca5bbb412bbd70e86494ae9ae0bad25bb322cf2e8798fb46156ee",
|
||||
"phase": "Lobby",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lobbyId": "9f07a2d91db949ad",
|
||||
"lastEventId": 7,
|
||||
"timestamp": "2026-01-27T13:31:27.538973Z",
|
||||
"checksum": "d2834a20c3fbf35631ac19aa4cc881dc4ea22da294f378c9ea175f79def2e5b1",
|
||||
"phase": "Lobby",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"lobbyId": "a16bac8d99e74830",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T13:41:27.5257123Z",
|
||||
"checksum": "ce1d6aec022b9dfcd40896dc2d137c997eb55b180fcb565d259dd9d3fba5efe2",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.0858413,
|
||||
"lon": 14.4205435
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.08849002857143,
|
||||
"lon": 14.421553200000002
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.0875007,
|
||||
"lon": 14.4179042
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5708,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.08828,
|
||||
"lon": 14.4177757
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.0863048,
|
||||
"lon": 14.4194325
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9104,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"lobbyId": "b9cffa33b9264559",
|
||||
"lastEventId": 11,
|
||||
"timestamp": "2026-01-27T13:36:27.5272024Z",
|
||||
"checksum": "eb35b9829659a173f4ff3ff4bffae656b8f7fd54d7d0b77addc9e65dc7e95d47",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.0880837,
|
||||
"lon": 14.4241303
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.0868455,
|
||||
"lon": 14.4251958
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.0881001,
|
||||
"lon": 14.4251481
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5959,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.0868548,
|
||||
"lon": 14.4252407
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8586,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.0889995,
|
||||
"lon": 14.4222151
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lobbyId": "cf3be038c5a3472e",
|
||||
"lastEventId": 7,
|
||||
"timestamp": "2026-01-27T13:41:27.521301Z",
|
||||
"checksum": "b8c94dca9682f3f2308d847b765fd5d473149dcb87f84a9c6dff93f403e2d622",
|
||||
"phase": "Lobby",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lobbyId": "d3e8a657109144ce",
|
||||
"lastEventId": 7,
|
||||
"timestamp": "2026-01-27T13:36:27.5310617Z",
|
||||
"checksum": "0b23ea5bf52ae1200a729d7db310feb1c8a9307f014bbc58d2e532468d112a91",
|
||||
"phase": "Lobby",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"lobbyId": "dc41c9d596db4946",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T13:41:27.518834Z",
|
||||
"checksum": "6cf9b25105fdf12ec96a8dedb18dd85aa5fef345c2c08a50045e7a53df078ffc",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.085561139999996,
|
||||
"lon": 14.42309244
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.085837,
|
||||
"lon": 14.4188593
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7152,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.0859669,
|
||||
"lon": 14.4188964
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.0860902,
|
||||
"lon": 14.4235956
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5893,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.09006194,
|
||||
"lon": 14.421070419999998
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"lobbyId": "df5878e2914a4104",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T13:36:27.5297309Z",
|
||||
"checksum": "33e3631ebc9785562d42cb7d2a9ac0778daa8fbccf56496c6938f1da4305fe0b",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.0870176,
|
||||
"lon": 14.4213745
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.08900948,
|
||||
"lon": 14.418448039999998
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.0856234,
|
||||
"lon": 14.4229967
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5157,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.0876224,
|
||||
"lon": 14.4200385
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6963,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.0873335,
|
||||
"lon": 14.4229653
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"lobbyId": "e52ea7e70f9c4475",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T13:31:27.5486405Z",
|
||||
"checksum": "b974ca483616d916c79e6661a25e155a34d9905bf1e8bf044f958e3332ddecbe",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.0883326,
|
||||
"lon": 14.4243654
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9830,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.0888976,
|
||||
"lon": 14.4235413
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.087982700000005,
|
||||
"lon": 14.424510520000002
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.08682402000001,
|
||||
"lon": 14.41780478
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.0879393,
|
||||
"lon": 14.4195066
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.0875,
|
||||
"lon": 14.4213
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
254
bin/Debug/net9.0/data/lobbies/2648919f382c41c5/snapshot_59.json
Normal file
254
bin/Debug/net9.0/data/lobbies/2648919f382c41c5/snapshot_59.json
Normal file
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"lobbyId": "2648919f382c41c5",
|
||||
"lastEventId": 59,
|
||||
"timestamp": "2026-01-27T18:59:03.2825318Z",
|
||||
"checksum": "1a2ad5f4cbb6cce28242186be6f40b40eec4324c03c7f86a5240dd90a9dd2177",
|
||||
"phase": "Lobby",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "8994ac32",
|
||||
"displayName": "Hr\u00E1\u010D415",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:53:06.3002728Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:57:32.9413685Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": "2026-01-27T18:55:14.0260895Z",
|
||||
"emergencyMeetingsUsed": 1,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "0edf1729",
|
||||
"displayName": "Hr\u00E1\u010D807",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:53:14.049105Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:56:19.557754Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "71890d7c",
|
||||
"displayName": "Hr\u00E1\u010D727",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:53:16.7573899Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:57:32.6978551Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "c9700c3e",
|
||||
"displayName": "Hr\u00E1\u010D811",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:53:18.3315357Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:57:32.6887401Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "8d4ca20b",
|
||||
"displayName": "Hr\u00E1\u010D45",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:53:20.2071786Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:57:32.6947537Z",
|
||||
"lastKillTime": "2026-01-27T18:56:19.060694Z",
|
||||
"lastEmergencyMeetingTime": "2026-01-27T18:53:50.6568196Z",
|
||||
"emergencyMeetingsUsed": 1,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 104,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"lobbyId": "2c50d55476084b56",
|
||||
"lastEventId": 12,
|
||||
"timestamp": "2026-01-27T16:28:29.5775449Z",
|
||||
"checksum": "f350e9f787de7222da92560a4faf764440917922ee2f6bc477a61b0b2597c047",
|
||||
"phase": "Playing",
|
||||
"players": [],
|
||||
"bodies": [
|
||||
{
|
||||
"bodyId": "93c535cb",
|
||||
"victimId": "3a7b7d51",
|
||||
"killerId": "4776b4fd",
|
||||
"location": {
|
||||
"lat": 50.773528230993556,
|
||||
"lon": 15.072186665335956
|
||||
},
|
||||
"killedAt": "2026-01-27T16:07:05.4091568Z",
|
||||
"reported": false,
|
||||
"reportedBy": null
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8268,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7748682,
|
||||
"lon": 15.0725437
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7741888,
|
||||
"lon": 15.0705734
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6077,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7738016,
|
||||
"lon": 15.0702315
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7746238,
|
||||
"lon": 15.0716847
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5791,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 175,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
545
bin/Debug/net9.0/data/lobbies/2e22bb042f2f4bbe/snapshot_22.json
Normal file
545
bin/Debug/net9.0/data/lobbies/2e22bb042f2f4bbe/snapshot_22.json
Normal file
@@ -0,0 +1,545 @@
|
||||
{
|
||||
"lobbyId": "2e22bb042f2f4bbe",
|
||||
"lastEventId": 22,
|
||||
"timestamp": "2026-01-27T15:51:57.8920069Z",
|
||||
"checksum": "9d1b249ae84e5d6285b305b86c43d02c035c58d7639d26b444131c7d6e548953",
|
||||
"phase": "Playing",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "f950d5da",
|
||||
"displayName": "Hr\u00E1\u010D807",
|
||||
"position": {
|
||||
"lat": 50.77313331657089,
|
||||
"lon": 15.072188416646162
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Dead",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:41:57.4124996Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:43:03.3779377Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7149,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7727517,
|
||||
"lon": 15.0713078
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7743857,
|
||||
"lon": 15.0723828
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7738718,
|
||||
"lon": 15.0726246
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6496,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7730823,
|
||||
"lon": 15.071947
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "c5654f74",
|
||||
"displayName": "Hr\u00E1\u010D437",
|
||||
"position": {
|
||||
"lat": 50.77278259768372,
|
||||
"lon": 15.07222070338361
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:42:07.6931815Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:51:57.5706389Z",
|
||||
"lastKillTime": "2026-01-27T15:45:05.8370242Z",
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "03a78181",
|
||||
"displayName": "Hr\u00E1\u010D551",
|
||||
"position": {
|
||||
"lat": 50.77398539640443,
|
||||
"lon": 15.072124569466894
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:42:10.6142359Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:51:57.7340338Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": "2026-01-27T15:44:49.798083Z",
|
||||
"emergencyMeetingsUsed": 1,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7731933,
|
||||
"lon": 15.0726196
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.772732,
|
||||
"lon": 15.0728076
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9946,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7743766,
|
||||
"lon": 15.0722922
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7738298,
|
||||
"lon": 15.0716222
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "ecd89d1a",
|
||||
"displayName": "Hr\u00E1\u010D18",
|
||||
"position": {
|
||||
"lat": 50.77368486134701,
|
||||
"lon": 15.071483856973888
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:42:12.192649Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:51:57.8914614Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_10",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_11",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7730142,
|
||||
"lon": 15.0715442
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8933,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_12",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7738298,
|
||||
"lon": 15.0716222
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8475,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_13",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.772732,
|
||||
"lon": 15.0728076
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_14",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7732768,
|
||||
"lon": 15.0725856
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7149,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7727517,
|
||||
"lon": 15.0713078
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7743857,
|
||||
"lon": 15.0723828
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7738718,
|
||||
"lon": 15.0726246
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6496,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7730823,
|
||||
"lon": 15.071947
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7731933,
|
||||
"lon": 15.0726196
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.772732,
|
||||
"lon": 15.0728076
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9946,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7743766,
|
||||
"lon": 15.0722922
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7738298,
|
||||
"lon": 15.0716222
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_10",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_11",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7730142,
|
||||
"lon": 15.0715442
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8933,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_12",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7738298,
|
||||
"lon": 15.0716222
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8475,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_13",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.772732,
|
||||
"lon": 15.0728076
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_14",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7732768,
|
||||
"lon": 15.0725856
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 112,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
163
bin/Debug/net9.0/data/lobbies/525fa8b4d76a42ed/snapshot_18.json
Normal file
163
bin/Debug/net9.0/data/lobbies/525fa8b4d76a42ed/snapshot_18.json
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"lobbyId": "525fa8b4d76a42ed",
|
||||
"lastEventId": 18,
|
||||
"timestamp": "2026-01-27T16:41:21.5644571Z",
|
||||
"checksum": "f00d06dc1b92bc8cfdebcf3fc4ea33925e0fb24ca9ffc130f838f042c7d0fec7",
|
||||
"phase": "Lobby",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "a4b084ae",
|
||||
"displayName": "Hr\u00E1\u010D49",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T16:35:21.4117891Z",
|
||||
"lastPositionUpdate": "2026-01-27T16:40:04.8976091Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "e7432841",
|
||||
"displayName": "Hr\u00E1\u010D952",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T16:35:34.1694493Z",
|
||||
"lastPositionUpdate": "2026-01-27T16:40:04.8708739Z",
|
||||
"lastKillTime": "2026-01-27T16:40:04.944272Z",
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "a529c9bf",
|
||||
"displayName": "Hr\u00E1\u010D887",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T16:35:34.6701344Z",
|
||||
"lastPositionUpdate": "2026-01-27T16:40:04.8646587Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": "2026-01-27T16:38:42.7706296Z",
|
||||
"emergencyMeetingsUsed": 1,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 100,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"lobbyId": "578a2e6234924b7e",
|
||||
"lastEventId": 8,
|
||||
"timestamp": "2026-01-27T13:58:30.9653651Z",
|
||||
"checksum": "903a6c6bed851b55cb00fdf3578463e6541556145374f6211fd60adbbdbfad13",
|
||||
"phase": "Lobby",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "39cc9c4a",
|
||||
"displayName": "Hr\u00E1\u010D880",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T13:53:36.8705417Z",
|
||||
"lastPositionUpdate": "2026-01-27T13:53:36.8705418Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": []
|
||||
},
|
||||
{
|
||||
"clientUuid": "e76cb534",
|
||||
"displayName": "Hr\u00E1\u010D839",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T13:53:38.3550591Z",
|
||||
"lastPositionUpdate": "2026-01-27T13:53:38.3550591Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": []
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
207
bin/Debug/net9.0/data/lobbies/5e6dc2f6267b4a23/snapshot_49.json
Normal file
207
bin/Debug/net9.0/data/lobbies/5e6dc2f6267b4a23/snapshot_49.json
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"lobbyId": "5e6dc2f6267b4a23",
|
||||
"lastEventId": 49,
|
||||
"timestamp": "2026-01-27T17:23:51.9496141Z",
|
||||
"checksum": "a076881d1a0cea6a2b39fdfeb1fb2041927e6b0a4351c1871422900729eac9f6",
|
||||
"phase": "Lobby",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "ac74a0a6",
|
||||
"displayName": "Hr\u00E1\u010D229",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:18:51.5675805Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:23:44.7477804Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "beb07c52",
|
||||
"displayName": "Hr\u00E1\u010D623",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:19:28.7165473Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:23:49.0892842Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "8505d1ed",
|
||||
"displayName": "Hr\u00E1\u010D976",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:19:32.3214433Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:23:49.0984457Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "55d92428",
|
||||
"displayName": "Hr\u00E1\u010D172",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:19:35.2884502Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:23:49.2130304Z",
|
||||
"lastKillTime": "2026-01-27T17:23:49.4098068Z",
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 116,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
518
bin/Debug/net9.0/data/lobbies/5e6dc2f6267b4a23/snapshot_97.json
Normal file
518
bin/Debug/net9.0/data/lobbies/5e6dc2f6267b4a23/snapshot_97.json
Normal file
@@ -0,0 +1,518 @@
|
||||
{
|
||||
"lobbyId": "5e6dc2f6267b4a23",
|
||||
"lastEventId": 97,
|
||||
"timestamp": "2026-01-27T17:28:52.5626512Z",
|
||||
"checksum": "f67d29a0cbd24da5e340bdb8d725233366ff81b97e13488d09de13477f5291ce",
|
||||
"phase": "Meeting",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "ac74a0a6",
|
||||
"displayName": "Hr\u00E1\u010D229",
|
||||
"position": {
|
||||
"lat": 50.77361640940255,
|
||||
"lon": 15.072599356266377
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:18:51.5675805Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:28:17.854293Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7735493,
|
||||
"lon": 15.0730608
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7738298,
|
||||
"lon": 15.0716222
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7738661,
|
||||
"lon": 15.0722328
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"type": "Instant"
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "beb07c52",
|
||||
"displayName": "Hr\u00E1\u010D623",
|
||||
"position": {
|
||||
"lat": 50.77365433872294,
|
||||
"lon": 15.07261560623589
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:19:28.7165473Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:28:17.8661244Z",
|
||||
"lastKillTime": "2026-01-27T17:27:45.615597Z",
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "8505d1ed",
|
||||
"displayName": "Hr\u00E1\u010D976",
|
||||
"position": {
|
||||
"lat": 50.77364810754672,
|
||||
"lon": 15.072627464770637
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Dead",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:19:32.3214433Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:27:45.7426596Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7739109,
|
||||
"lon": 15.0728539
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7734516,
|
||||
"lon": 15.0723155
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7729336,
|
||||
"lon": 15.0713619
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.773807,
|
||||
"lon": 15.071876
|
||||
},
|
||||
"type": "Instant"
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "55d92428",
|
||||
"displayName": "Hr\u00E1\u010D172",
|
||||
"position": {
|
||||
"lat": 50.77365944629486,
|
||||
"lon": 15.07263119340305
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:19:35.2884502Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:28:17.7087384Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_10",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_11",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7732768,
|
||||
"lon": 15.0725856
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_12",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.772732,
|
||||
"lon": 15.0728076
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_13",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_14",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7728733,
|
||||
"lon": 15.0713562
|
||||
},
|
||||
"type": "Instant"
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [
|
||||
{
|
||||
"bodyId": "a0d004c6",
|
||||
"victimId": "8505d1ed",
|
||||
"killerId": "beb07c52",
|
||||
"location": {
|
||||
"lat": 50.77364810754672,
|
||||
"lon": 15.072627464770637
|
||||
},
|
||||
"killedAt": "2026-01-27T17:27:45.6156674Z",
|
||||
"reported": true,
|
||||
"reportedBy": "beb07c52"
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7735493,
|
||||
"lon": 15.0730608
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7738298,
|
||||
"lon": 15.0716222
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7738661,
|
||||
"lon": 15.0722328
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7739109,
|
||||
"lon": 15.0728539
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7734516,
|
||||
"lon": 15.0723155
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7729336,
|
||||
"lon": 15.0713619
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.773807,
|
||||
"lon": 15.071876
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_10",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_11",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7732768,
|
||||
"lon": 15.0725856
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_12",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.772732,
|
||||
"lon": 15.0728076
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_13",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_14",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7728733,
|
||||
"lon": 15.0713562
|
||||
},
|
||||
"type": "Instant"
|
||||
}
|
||||
],
|
||||
"currentMeeting": {
|
||||
"meetingId": "0b48d4b8",
|
||||
"type": "BodyReport",
|
||||
"reportedBodyId": "a0d004c6",
|
||||
"callerId": "beb07c52",
|
||||
"meetingLocation": {
|
||||
"lat": 50.77364810754672,
|
||||
"lon": 15.072627464770637
|
||||
},
|
||||
"startTime": "2026-01-27T17:27:54.9995201Z",
|
||||
"arrivalDeadline": "2026-01-27T17:28:17.9995201Z",
|
||||
"discussionEndTime": "2026-01-27T17:28:29.9995201Z",
|
||||
"votingEndTime": "2026-01-27T17:28:59.9995201Z",
|
||||
"arrivedPlayers": [
|
||||
"beb07c52",
|
||||
"55d92428",
|
||||
"ac74a0a6"
|
||||
],
|
||||
"votes": {
|
||||
"55d92428": "beb07c52",
|
||||
"ac74a0a6": "beb07c52",
|
||||
"beb07c52": "55d92428"
|
||||
},
|
||||
"lastVoteChangeTime": "2026-01-27T17:28:52.5435833Z"
|
||||
},
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 116,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
843
bin/Debug/net9.0/data/lobbies/6dc0701887224f6c/snapshot_29.json
Normal file
843
bin/Debug/net9.0/data/lobbies/6dc0701887224f6c/snapshot_29.json
Normal file
@@ -0,0 +1,843 @@
|
||||
{
|
||||
"lobbyId": "6dc0701887224f6c",
|
||||
"lastEventId": 29,
|
||||
"timestamp": "2026-01-27T17:08:04.9544682Z",
|
||||
"checksum": "35e22951c680f0a9dc2e4dcf31e203e75e9bc3601ff3d2654d925857ca3d9e11",
|
||||
"phase": "Playing",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "0cabd196",
|
||||
"displayName": "Hr\u00E1\u010D308",
|
||||
"position": {
|
||||
"lat": 50.77360619739959,
|
||||
"lon": 15.072054196830372
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:03:04.9405525Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:08:04.8139149Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"currentWaypointIndex": 1,
|
||||
"taskStartTime": "2026-01-27T17:06:22.6362862Z",
|
||||
"lastTaskProgressTime": "2026-01-27T17:06:22.636287Z",
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "70d9764e",
|
||||
"displayName": "Hr\u00E1\u010D467",
|
||||
"position": {
|
||||
"lat": 50.773608029754094,
|
||||
"lon": 15.072135796821454
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:03:12.3441902Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:08:04.9533762Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_0_wp0",
|
||||
"name": "Opravit kabel - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_0_wp1",
|
||||
"name": "Opravit kabel - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7738727,
|
||||
"lon": 15.0724211
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_0_wp2",
|
||||
"name": "Opravit kabel - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.7735894,
|
||||
"lon": 15.0717216
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_1_wp0",
|
||||
"name": "Zkalibrovat senzor - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_1_wp1",
|
||||
"name": "Zkalibrovat senzor - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7729336,
|
||||
"lon": 15.0713619
|
||||
},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7735493,
|
||||
"lon": 15.0730608
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_2_wp0",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7735493,
|
||||
"lon": 15.0730608
|
||||
},
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7736218,
|
||||
"lon": 15.0734419
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_3_wp0",
|
||||
"name": "Nab\u00EDt baterii - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7736218,
|
||||
"lon": 15.0734419
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_3_wp1",
|
||||
"name": "Nab\u00EDt baterii - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7730823,
|
||||
"lon": 15.071947
|
||||
},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_4_wp0",
|
||||
"name": "Vy\u010Distit filtr - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_4_wp1",
|
||||
"name": "Vy\u010Distit filtr - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7735484,
|
||||
"lon": 15.0720508
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_4_wp2",
|
||||
"name": "Vy\u010Distit filtr - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.7741669,
|
||||
"lon": 15.0718846
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"currentWaypointIndex": 0,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "47c5062e",
|
||||
"displayName": "Hr\u00E1\u010D283",
|
||||
"position": {
|
||||
"lat": 50.77362533873519,
|
||||
"lon": 15.072083787211199
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T17:03:20.2284988Z",
|
||||
"lastPositionUpdate": "2026-01-27T17:08:04.7240844Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_5_wp0",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7738661,
|
||||
"lon": 15.0722328
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_6_wp0",
|
||||
"name": "Opravit antenu - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7738661,
|
||||
"lon": 15.0722328
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_6_wp1",
|
||||
"name": "Opravit antenu - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7730823,
|
||||
"lon": 15.071947
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_6_wp2",
|
||||
"name": "Opravit antenu - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.773902,
|
||||
"lon": 15.0727669
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7739852,
|
||||
"lon": 15.072001
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_7_wp0",
|
||||
"name": "Zkontrolovat z\u00E1soby - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7739852,
|
||||
"lon": 15.072001
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_7_wp1",
|
||||
"name": "Zkontrolovat z\u00E1soby - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7735939,
|
||||
"lon": 15.0732437
|
||||
},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7735004,
|
||||
"lon": 15.0728728
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_8_wp0",
|
||||
"name": "Otestovat reaktor - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7735004,
|
||||
"lon": 15.0728728
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_8_wp1",
|
||||
"name": "Otestovat reaktor - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7729336,
|
||||
"lon": 15.0713619
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_8_wp2",
|
||||
"name": "Otestovat reaktor - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.7735894,
|
||||
"lon": 15.0717216
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.773807,
|
||||
"lon": 15.071876
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_9_wp0",
|
||||
"name": "Opravit kabel - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.773807,
|
||||
"lon": 15.071876
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_9_wp1",
|
||||
"name": "Opravit kabel - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_9_wp2",
|
||||
"name": "Opravit kabel - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.7734588,
|
||||
"lon": 15.0719916
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"currentWaypointIndex": 1,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": "2026-01-27T17:06:50.8371719Z",
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_0_wp0",
|
||||
"name": "Opravit kabel - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_0_wp1",
|
||||
"name": "Opravit kabel - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7738727,
|
||||
"lon": 15.0724211
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_0_wp2",
|
||||
"name": "Opravit kabel - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.7735894,
|
||||
"lon": 15.0717216
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_1_wp0",
|
||||
"name": "Zkalibrovat senzor - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_1_wp1",
|
||||
"name": "Zkalibrovat senzor - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7729336,
|
||||
"lon": 15.0713619
|
||||
},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7735493,
|
||||
"lon": 15.0730608
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_2_wp0",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7735493,
|
||||
"lon": 15.0730608
|
||||
},
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7736218,
|
||||
"lon": 15.0734419
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_3_wp0",
|
||||
"name": "Nab\u00EDt baterii - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7736218,
|
||||
"lon": 15.0734419
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_3_wp1",
|
||||
"name": "Nab\u00EDt baterii - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7730823,
|
||||
"lon": 15.071947
|
||||
},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_4_wp0",
|
||||
"name": "Vy\u010Distit filtr - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_4_wp1",
|
||||
"name": "Vy\u010Distit filtr - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7735484,
|
||||
"lon": 15.0720508
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_4_wp2",
|
||||
"name": "Vy\u010Distit filtr - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.7741669,
|
||||
"lon": 15.0718846
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_5_wp0",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7738661,
|
||||
"lon": 15.0722328
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_6_wp0",
|
||||
"name": "Opravit antenu - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7738661,
|
||||
"lon": 15.0722328
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_6_wp1",
|
||||
"name": "Opravit antenu - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7730823,
|
||||
"lon": 15.071947
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_6_wp2",
|
||||
"name": "Opravit antenu - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.773902,
|
||||
"lon": 15.0727669
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7739852,
|
||||
"lon": 15.072001
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_7_wp0",
|
||||
"name": "Zkontrolovat z\u00E1soby - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7739852,
|
||||
"lon": 15.072001
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_7_wp1",
|
||||
"name": "Zkontrolovat z\u00E1soby - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7735939,
|
||||
"lon": 15.0732437
|
||||
},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7735004,
|
||||
"lon": 15.0728728
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_8_wp0",
|
||||
"name": "Otestovat reaktor - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.7735004,
|
||||
"lon": 15.0728728
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_8_wp1",
|
||||
"name": "Otestovat reaktor - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7729336,
|
||||
"lon": 15.0713619
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_8_wp2",
|
||||
"name": "Otestovat reaktor - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.7735894,
|
||||
"lon": 15.0717216
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.773807,
|
||||
"lon": 15.071876
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"waypointId": "task_9_wp0",
|
||||
"name": "Opravit kabel - P\u0159ij\u00EDt sem",
|
||||
"location": {
|
||||
"lat": 50.773807,
|
||||
"lon": 15.071876
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"waypointId": "task_9_wp1",
|
||||
"name": "Opravit kabel - Aktivovat termin\u00E1l",
|
||||
"location": {
|
||||
"lat": 50.7733695,
|
||||
"lon": 15.072243
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"waypointId": "task_9_wp2",
|
||||
"name": "Opravit kabel - Potvrdit",
|
||||
"location": {
|
||||
"lat": 50.7734588,
|
||||
"lon": 15.0719916
|
||||
},
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 100,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
263
bin/Debug/net9.0/data/lobbies/79b0a3695df748e9/snapshot_15.json
Normal file
263
bin/Debug/net9.0/data/lobbies/79b0a3695df748e9/snapshot_15.json
Normal file
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"lobbyId": "79b0a3695df748e9",
|
||||
"lastEventId": 15,
|
||||
"timestamp": "2026-01-27T14:09:32.6436672Z",
|
||||
"checksum": "c5ed7252657435f2394fb45ea57a0a9a55580f67ce1fa47c8d3d57133b8b4ec3",
|
||||
"phase": "Playing",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "0a2becd5",
|
||||
"displayName": "Hr\u00E1\u010D663",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T14:04:32.409501Z",
|
||||
"lastPositionUpdate": "2026-01-27T14:09:32.251747Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "7ddc8c89",
|
||||
"displayName": "Hr\u00E1\u010D774",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T14:04:49.0930502Z",
|
||||
"lastPositionUpdate": "2026-01-27T14:09:32.1675785Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "7f1e49f6",
|
||||
"displayName": "Hr\u00E1\u010D409",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T14:04:50.9443623Z",
|
||||
"lastPositionUpdate": "2026-01-27T14:09:32.6433769Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "1c0c235a",
|
||||
"displayName": "Hr\u00E1\u010D643",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T14:04:52.5855743Z",
|
||||
"lastPositionUpdate": "2026-01-27T14:09:32.3222919Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.773738,
|
||||
"lon": 15.0696539
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7376,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7742687,
|
||||
"lon": 15.0684062
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7741888,
|
||||
"lon": 15.0705734
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7739087,
|
||||
"lon": 15.0680313
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
390
bin/Debug/net9.0/data/lobbies/833e71c1c6c64572/snapshot_41.json
Normal file
390
bin/Debug/net9.0/data/lobbies/833e71c1c6c64572/snapshot_41.json
Normal file
@@ -0,0 +1,390 @@
|
||||
{
|
||||
"lobbyId": "833e71c1c6c64572",
|
||||
"lastEventId": 41,
|
||||
"timestamp": "2026-01-27T19:49:33.8228142Z",
|
||||
"checksum": "947203265976dbb0c3abb28163f1ad9f4a95832ae6af7af2db9b6cb1a7d35a1f",
|
||||
"phase": "Playing",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "42abae11",
|
||||
"displayName": "Hr\u00E1\u010D71",
|
||||
"position": {
|
||||
"lat": 50.77368411905394,
|
||||
"lon": 15.072021509115855
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T19:44:33.6038667Z",
|
||||
"lastPositionUpdate": "2026-01-27T19:49:33.5853267Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "e43245ed",
|
||||
"displayName": "Hr\u00E1\u010D656",
|
||||
"position": {
|
||||
"lat": 50.773616604900646,
|
||||
"lon": 15.07330046468691
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T19:46:15.7160727Z",
|
||||
"lastPositionUpdate": "2026-01-27T19:49:33.5497451Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [
|
||||
"task_9"
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7738714,
|
||||
"lon": 15.0698715
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.774702,
|
||||
"lon": 15.0718182
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7738016,
|
||||
"lon": 15.0702315
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant"
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "ac9c6e85",
|
||||
"displayName": "Hr\u00E1\u010D454",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T19:46:21.0904587Z",
|
||||
"lastPositionUpdate": "2026-01-27T19:49:33.4002447Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_10",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.774702,
|
||||
"lon": 15.0718182
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_11",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7738016,
|
||||
"lon": 15.0702315
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_12",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_13",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7739942,
|
||||
"lon": 15.0706985
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_14",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7740297,
|
||||
"lon": 15.0744936
|
||||
},
|
||||
"type": "Instant"
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7738016,
|
||||
"lon": 15.0702315
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7739942,
|
||||
"lon": 15.0706985
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7746238,
|
||||
"lon": 15.0716847
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7738714,
|
||||
"lon": 15.0698715
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.774702,
|
||||
"lon": 15.0718182
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7738016,
|
||||
"lon": 15.0702315
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_10",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.774702,
|
||||
"lon": 15.0718182
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_11",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7738016,
|
||||
"lon": 15.0702315
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_12",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_13",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7739942,
|
||||
"lon": 15.0706985
|
||||
},
|
||||
"type": "Instant"
|
||||
},
|
||||
{
|
||||
"taskId": "task_14",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7740297,
|
||||
"lon": 15.0744936
|
||||
},
|
||||
"type": "Instant"
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 207,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
442
bin/Debug/net9.0/data/lobbies/8d4e9da599f54808/snapshot_24.json
Normal file
442
bin/Debug/net9.0/data/lobbies/8d4e9da599f54808/snapshot_24.json
Normal file
@@ -0,0 +1,442 @@
|
||||
{
|
||||
"lobbyId": "8d4e9da599f54808",
|
||||
"lastEventId": 24,
|
||||
"timestamp": "2026-01-27T15:18:37.7070312Z",
|
||||
"checksum": "f58ec73bd590e27a43011ac40020f25a033ec754c77741ad3520b6a846a25d0f",
|
||||
"phase": "Playing",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "3cf2ce1c",
|
||||
"displayName": "Hr\u00E1\u010D136",
|
||||
"position": {
|
||||
"lat": 50.77379320364965,
|
||||
"lon": 15.070223610918289
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:13:37.5682167Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:18:37.4568323Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7741888,
|
||||
"lon": 15.0705734
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9550,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6835,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7738016,
|
||||
"lon": 15.0702315
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7748682,
|
||||
"lon": 15.0725437
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7744712,
|
||||
"lon": 15.0716784
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "fc0eec4c",
|
||||
"displayName": "Hr\u00E1\u010D652",
|
||||
"position": {
|
||||
"lat": 50.7745196638094,
|
||||
"lon": 15.071680304081935
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:13:59.4594629Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:18:37.3824595Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7744712,
|
||||
"lon": 15.0716784
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9304,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7741494,
|
||||
"lon": 15.0703058
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9090,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "4485888a",
|
||||
"displayName": "Hr\u00E1\u010D445",
|
||||
"position": {
|
||||
"lat": 50.77362353479194,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:14:09.4702052Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:18:37.7046693Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.774702,
|
||||
"lon": 15.0718182
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7741888,
|
||||
"lon": 15.0705734
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7553,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7741888,
|
||||
"lon": 15.0705734
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9550,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6835,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7738016,
|
||||
"lon": 15.0702315
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7748682,
|
||||
"lon": 15.0725437
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7744712,
|
||||
"lon": 15.0716784
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7744712,
|
||||
"lon": 15.0716784
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9304,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7741494,
|
||||
"lon": 15.0703058
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9090,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 155,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
442
bin/Debug/net9.0/data/lobbies/8d4e9da599f54808/snapshot_38.json
Normal file
442
bin/Debug/net9.0/data/lobbies/8d4e9da599f54808/snapshot_38.json
Normal file
@@ -0,0 +1,442 @@
|
||||
{
|
||||
"lobbyId": "8d4e9da599f54808",
|
||||
"lastEventId": 38,
|
||||
"timestamp": "2026-01-27T15:33:38.0063974Z",
|
||||
"checksum": "961ac66fd07f021b7e58dfbb84defa1a985c143994a1d4de652d5e2060c510e3",
|
||||
"phase": "Playing",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "3cf2ce1c",
|
||||
"displayName": "Hr\u00E1\u010D136",
|
||||
"position": {
|
||||
"lat": 50.773291841983664,
|
||||
"lon": 15.07256331093216
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:13:37.5682167Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:33:37.716566Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7741888,
|
||||
"lon": 15.0705734
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7744712,
|
||||
"lon": 15.0716784
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7741494,
|
||||
"lon": 15.0703058
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "fc0eec4c",
|
||||
"displayName": "Hr\u00E1\u010D652",
|
||||
"position": {
|
||||
"lat": 50.77317868182666,
|
||||
"lon": 15.072231622732726
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:13:59.4594629Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:33:38.0059926Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7744712,
|
||||
"lon": 15.0716784
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9304,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7741494,
|
||||
"lon": 15.0703058
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9090,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "4485888a",
|
||||
"displayName": "Hr\u00E1\u010D445",
|
||||
"position": {
|
||||
"lat": 50.77364163912988,
|
||||
"lon": 15.072117688520137
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:14:09.4702052Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:33:37.8006425Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7748514,
|
||||
"lon": 15.0724958
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7746238,
|
||||
"lon": 15.0716847
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5107,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9759,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 3
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7741888,
|
||||
"lon": 15.0705734
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7744712,
|
||||
"lon": 15.0716784
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7741494,
|
||||
"lon": 15.0703058
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77303,
|
||||
"lon": 15.07395
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.7748514,
|
||||
"lon": 15.0724958
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7741443,
|
||||
"lon": 15.0702437
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7746238,
|
||||
"lon": 15.0716847
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5107,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.774201,
|
||||
"lon": 15.0712028
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 9759,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 155,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
276
bin/Debug/net9.0/data/lobbies/92a59e40e47f46a4/snapshot_24.json
Normal file
276
bin/Debug/net9.0/data/lobbies/92a59e40e47f46a4/snapshot_24.json
Normal file
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"lobbyId": "92a59e40e47f46a4",
|
||||
"lastEventId": 24,
|
||||
"timestamp": "2026-01-27T14:19:25.6511339Z",
|
||||
"checksum": "34a5e98d571d2b1417dcd8170b95ea7a4e54c25cdcbb43b0c6442527a0619d58",
|
||||
"phase": "Playing",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "d5b6a42a",
|
||||
"displayName": "Hr\u00E1\u010D329",
|
||||
"position": {
|
||||
"lat": 50.77360212891568,
|
||||
"lon": 15.072066545144711
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T14:14:25.4748393Z",
|
||||
"lastPositionUpdate": "2026-01-27T14:19:25.4482291Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": "2026-01-27T14:17:23.8607905Z",
|
||||
"emergencyMeetingsUsed": 2,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "cb379618",
|
||||
"displayName": "Hr\u00E1\u010D723",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Dead",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T14:14:45.3520713Z",
|
||||
"lastPositionUpdate": "2026-01-27T14:18:48.3375132Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "1545672a",
|
||||
"displayName": "Hr\u00E1\u010D473",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T14:14:47.2696299Z",
|
||||
"lastPositionUpdate": "2026-01-27T14:19:25.3302047Z",
|
||||
"lastKillTime": "2026-01-27T14:18:47.9069569Z",
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "69e62b18",
|
||||
"displayName": "Hr\u00E1\u010D276",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T14:14:49.3924189Z",
|
||||
"lastPositionUpdate": "2026-01-27T14:19:25.6462515Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [
|
||||
{
|
||||
"bodyId": "096a1f19",
|
||||
"victimId": "cb379618",
|
||||
"killerId": "1545672a",
|
||||
"location": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"killedAt": "2026-01-27T14:18:47.9069664Z",
|
||||
"reported": false,
|
||||
"reportedBy": null
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7744712,
|
||||
"lon": 15.0716784
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5468,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7741888,
|
||||
"lon": 15.0705734
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7743404,
|
||||
"lon": 15.0686409
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6822,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7740297,
|
||||
"lon": 15.0744936
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8038,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7739087,
|
||||
"lon": 15.0680313
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8266,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 300,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
212
bin/Debug/net9.0/data/lobbies/c20295d1ca0241b3/snapshot_39.json
Normal file
212
bin/Debug/net9.0/data/lobbies/c20295d1ca0241b3/snapshot_39.json
Normal file
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"lobbyId": "c20295d1ca0241b3",
|
||||
"lastEventId": 39,
|
||||
"timestamp": "2026-01-27T18:46:44.0153464Z",
|
||||
"checksum": "c869a0099601f13a5f3e7ef86940bb965849277cd84a2f7f3e88ecbdcb03f320",
|
||||
"phase": "Lobby",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "e8888b3e",
|
||||
"displayName": "Hr\u00E1\u010D229",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:41:34.084531Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:45:44.9225573Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "7614bb77",
|
||||
"displayName": "Hr\u00E1\u010D423",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:41:47.9066426Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:45:44.9999768Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "a482eaf8",
|
||||
"displayName": "Hr\u00E1\u010D755",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:41:49.9957639Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:45:45.0762014Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "b0172c9f",
|
||||
"displayName": "Hr\u00E1\u010D382",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:46:12.1783916Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:46:12.1783917Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": []
|
||||
},
|
||||
{
|
||||
"clientUuid": "0d634cf6",
|
||||
"displayName": "Hr\u00E1\u010D27",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T18:46:43.9875318Z",
|
||||
"lastPositionUpdate": "2026-01-27T18:46:43.987532Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": []
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [],
|
||||
"currentMeeting": null,
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 248,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
565
bin/Debug/net9.0/data/lobbies/ce407ba2a282475a/snapshot_23.json
Normal file
565
bin/Debug/net9.0/data/lobbies/ce407ba2a282475a/snapshot_23.json
Normal file
@@ -0,0 +1,565 @@
|
||||
{
|
||||
"lobbyId": "ce407ba2a282475a",
|
||||
"lastEventId": 23,
|
||||
"timestamp": "2026-01-27T16:01:32.7927329Z",
|
||||
"checksum": "094b8a24f68546bf3d88658d7bb01dc7621ae1087f5aa2b0f865e08008e2fdde",
|
||||
"phase": "Meeting",
|
||||
"players": [
|
||||
{
|
||||
"clientUuid": "481bcb45",
|
||||
"displayName": "Hr\u00E1\u010D402",
|
||||
"position": {
|
||||
"lat": 50.77312730016415,
|
||||
"lon": 15.072042156480837
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Dead",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:56:20.9156815Z",
|
||||
"lastPositionUpdate": "2026-01-27T15:58:53.9908881Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7125,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7738727,
|
||||
"lon": 15.0724211
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5835,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7732806,
|
||||
"lon": 15.0725075
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7731719,
|
||||
"lon": 15.0719627
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": true,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "154a8e55",
|
||||
"displayName": "Hr\u00E1\u010D949",
|
||||
"position": {
|
||||
"lat": 50.773134818907785,
|
||||
"lon": 15.07198899309649
|
||||
},
|
||||
"role": "Impostor",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:56:32.367088Z",
|
||||
"lastPositionUpdate": "2026-01-27T16:00:53.790974Z",
|
||||
"lastKillTime": "2026-01-27T16:00:02.4620235Z",
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "b8c1c6dc",
|
||||
"displayName": "Hr\u00E1\u010D590",
|
||||
"position": {
|
||||
"lat": 50.77358763220549,
|
||||
"lon": 15.072158011186659
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:56:44.2222077Z",
|
||||
"lastPositionUpdate": "2026-01-27T16:00:53.7269354Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": "2026-01-27T16:00:53.7404921Z",
|
||||
"emergencyMeetingsUsed": 1,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5791,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.773851,
|
||||
"lon": 15.073075
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7731933,
|
||||
"lon": 15.0726196
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7737495,
|
||||
"lon": 15.0715106
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7350,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientUuid": "104d74ed",
|
||||
"displayName": "Hr\u00E1\u010D381",
|
||||
"position": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"role": "Crew",
|
||||
"state": "Alive",
|
||||
"cheatStatus": "Ok",
|
||||
"cheatScore": 0,
|
||||
"connectedAt": "2026-01-27T15:56:56.1455962Z",
|
||||
"lastPositionUpdate": "2026-01-27T16:00:53.8024205Z",
|
||||
"lastKillTime": null,
|
||||
"lastEmergencyMeetingTime": null,
|
||||
"emergencyMeetingsUsed": 0,
|
||||
"completedTaskIds": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_10",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6764,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_11",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7734516,
|
||||
"lon": 15.0723155
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_12",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7741669,
|
||||
"lon": 15.0718846
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7958,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_13",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.773646,
|
||||
"lon": 15.0719148
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8269,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_14",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7902,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentTaskId": null,
|
||||
"taskStartTime": null,
|
||||
"lastTaskProgressTime": null,
|
||||
"isOwner": false,
|
||||
"isReady": false,
|
||||
"lastEventId": 0,
|
||||
"ping": 0,
|
||||
"positionHistory": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
],
|
||||
"bodies": [],
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task_0",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7125,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_1",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.7738727,
|
||||
"lon": 15.0724211
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_2",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5835,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_3",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7732806,
|
||||
"lon": 15.0725075
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 2
|
||||
},
|
||||
{
|
||||
"taskId": "task_4",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.7731719,
|
||||
"lon": 15.0719627
|
||||
},
|
||||
"type": "MultiStep",
|
||||
"durationMs": 0,
|
||||
"steps": 4
|
||||
},
|
||||
{
|
||||
"taskId": "task_5",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 5791,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_6",
|
||||
"name": "Opravit antenu",
|
||||
"location": {
|
||||
"lat": 50.773851,
|
||||
"lon": 15.073075
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_7",
|
||||
"name": "Zkontrolovat z\u00E1soby",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_8",
|
||||
"name": "Otestovat reaktor",
|
||||
"location": {
|
||||
"lat": 50.7731933,
|
||||
"lon": 15.0726196
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_9",
|
||||
"name": "Opravit kabel",
|
||||
"location": {
|
||||
"lat": 50.7737495,
|
||||
"lon": 15.0715106
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7350,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_10",
|
||||
"name": "Zkalibrovat senzor",
|
||||
"location": {
|
||||
"lat": 50.77358,
|
||||
"lon": 15.07339
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 6764,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_11",
|
||||
"name": "St\u00E1hnout data",
|
||||
"location": {
|
||||
"lat": 50.7734516,
|
||||
"lon": 15.0723155
|
||||
},
|
||||
"type": "Instant",
|
||||
"durationMs": 0,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_12",
|
||||
"name": "Nab\u00EDt baterii",
|
||||
"location": {
|
||||
"lat": 50.7741669,
|
||||
"lon": 15.0718846
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7958,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_13",
|
||||
"name": "Vy\u010Distit filtr",
|
||||
"location": {
|
||||
"lat": 50.773646,
|
||||
"lon": 15.0719148
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 8269,
|
||||
"steps": 1
|
||||
},
|
||||
{
|
||||
"taskId": "task_14",
|
||||
"name": "Nastavit kompas",
|
||||
"location": {
|
||||
"lat": 50.7739981,
|
||||
"lon": 15.0714832
|
||||
},
|
||||
"type": "Progress",
|
||||
"durationMs": 7902,
|
||||
"steps": 1
|
||||
}
|
||||
],
|
||||
"currentMeeting": {
|
||||
"meetingId": "8897a8fb",
|
||||
"type": "Emergency",
|
||||
"reportedBodyId": null,
|
||||
"callerId": "b8c1c6dc",
|
||||
"meetingLocation": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"startTime": "2026-01-27T16:00:53.7788042Z",
|
||||
"arrivalDeadline": "2026-01-27T16:01:16.7788042Z",
|
||||
"discussionEndTime": "2026-01-27T16:01:28.7788042Z",
|
||||
"votingEndTime": "2026-01-27T16:01:58.7788042Z",
|
||||
"arrivedPlayers": [
|
||||
"104d74ed"
|
||||
],
|
||||
"votes": {
|
||||
"104d74ed": "154a8e55"
|
||||
},
|
||||
"lastVoteChangeTime": "2026-01-27T16:01:32.7852692Z"
|
||||
},
|
||||
"playAreaCenter": {
|
||||
"lat": 50.7735892,
|
||||
"lon": 15.0721653
|
||||
},
|
||||
"playAreaRadius": 100,
|
||||
"impostorCount": 1,
|
||||
"tiePolicy": "NoEject"
|
||||
}
|
||||
BIN
bin/Debug/net9.0/data/stats.db
Normal file
BIN
bin/Debug/net9.0/data/stats.db
Normal file
Binary file not shown.
Binary file not shown.
BIN
bin/Debug/net9.0/runtimes/linux-arm/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net9.0/runtimes/linux-arm/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net9.0/runtimes/linux-arm64/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net9.0/runtimes/linux-arm64/native/libe_sqlite3.so
Normal file
Binary file not shown.
BIN
bin/Debug/net9.0/runtimes/linux-armel/native/libe_sqlite3.so
Normal file
BIN
bin/Debug/net9.0/runtimes/linux-armel/native/libe_sqlite3.so
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user