fixes 2
This commit is contained in:
178
AdminPanel.cs
178
AdminPanel.cs
@@ -180,7 +180,20 @@ public class AdminPanel
|
||||
case "/admin/api/broadcast":
|
||||
await HandleBroadcastAsync(request, response);
|
||||
break;
|
||||
|
||||
|
||||
// ── P12: lobby manipulation endpoints ────────────────────
|
||||
case "/admin/api/lobby/end":
|
||||
await HandleLobbyForceEndAsync(request, response);
|
||||
break;
|
||||
|
||||
case "/admin/api/lobby/phase":
|
||||
await HandleLobbySetPhaseAsync(request, response);
|
||||
break;
|
||||
|
||||
case "/admin/api/lobby/settings":
|
||||
await HandleLobbyUpdateSettingsAsync(request, response);
|
||||
break;
|
||||
|
||||
case "/admin/api/logout":
|
||||
_sessions.TryRemove(sessionId, out _);
|
||||
response.SetCookie(new Cookie("geosus_admin_session", "", "/admin") { Expired = true });
|
||||
@@ -412,10 +425,10 @@ public class AdminPanel
|
||||
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
|
||||
@@ -430,10 +443,115 @@ public class AdminPanel
|
||||
var lobby = _lobbyManager.GetLobby(lobbyId);
|
||||
lobby?.BroadcastSystemMessage(message!);
|
||||
}
|
||||
|
||||
|
||||
_logger.LogInformation("Admin broadcast: {Message}", message);
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
}
|
||||
|
||||
// ── P12: lobby manipulation endpoint handlers ────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Force-end a game in a specific lobby. POST { "lobbyId": "...", "reason": "optional" }.
|
||||
/// Returns { success: true } on application, 404 if lobby missing, or
|
||||
/// 409 if the lobby was already in Lobby/Ended phase (nothing to end).
|
||||
/// </summary>
|
||||
private async Task HandleLobbyForceEndAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
using var reader = new StreamReader(request.InputStream);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
var lobbyId = data.GetProperty("lobbyId").GetString();
|
||||
var reason = data.TryGetProperty("reason", out var r) ? (r.GetString() ?? "ended by admin") : "ended by admin";
|
||||
|
||||
var lobby = _lobbyManager.GetLobby(lobbyId!);
|
||||
if (lobby == null)
|
||||
{
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonAsync(response, new { error = "Lobby not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
var ok = await lobby.TryForceEndGameAsync(reason);
|
||||
if (!ok)
|
||||
{
|
||||
response.StatusCode = 409;
|
||||
await WriteJsonAsync(response, new { error = "Cannot force-end - lobby not in an active game phase" });
|
||||
return;
|
||||
}
|
||||
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force a lobby into a specific phase. POST { "lobbyId": "...", "phase": "Playing|Meeting|Ended|..." }.
|
||||
/// Voting is rejected (it's not a real wire phase - lives inside Meeting).
|
||||
/// </summary>
|
||||
private async Task HandleLobbySetPhaseAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
using var reader = new StreamReader(request.InputStream);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
var lobbyId = data.GetProperty("lobbyId").GetString();
|
||||
var phase = data.GetProperty("phase").GetString();
|
||||
|
||||
var lobby = _lobbyManager.GetLobby(lobbyId!);
|
||||
if (lobby == null)
|
||||
{
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonAsync(response, new { error = "Lobby not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
var ok = await lobby.TryAdminSetPhaseAsync(phase!);
|
||||
if (!ok)
|
||||
{
|
||||
response.StatusCode = 400;
|
||||
await WriteJsonAsync(response, new { error = "Invalid phase (rejected: 'Voting' is not a wire phase)" });
|
||||
return;
|
||||
}
|
||||
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update lobby settings. POST { "lobbyId": "...", "radius": ..., "impostorCount": ..., "taskCount": ..., "tiePolicy": "..." }.
|
||||
/// All settings fields optional - only those present in the body are
|
||||
/// applied. Lobby must be in Lobby phase; mid-game settings changes are
|
||||
/// rejected to keep gameplay consistent.
|
||||
/// </summary>
|
||||
private async Task HandleLobbyUpdateSettingsAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
using var reader = new StreamReader(request.InputStream);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
var lobbyId = data.GetProperty("lobbyId").GetString();
|
||||
|
||||
double? radius = data.TryGetProperty("radius", out var rd) && rd.ValueKind == JsonValueKind.Number ? rd.GetDouble() : (double?)null;
|
||||
int? impostors = data.TryGetProperty("impostorCount", out var ic) && ic.ValueKind == JsonValueKind.Number ? ic.GetInt32() : (int?)null;
|
||||
int? tasks = data.TryGetProperty("taskCount", out var tc) && tc.ValueKind == JsonValueKind.Number ? tc.GetInt32() : (int?)null;
|
||||
string? tiePolicy = data.TryGetProperty("tiePolicy", out var tp) ? tp.GetString() : null;
|
||||
|
||||
var lobby = _lobbyManager.GetLobby(lobbyId!);
|
||||
if (lobby == null)
|
||||
{
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonAsync(response, new { error = "Lobby not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
var ok = await lobby.TryUpdateLobbySettingsAsync(radius, impostors, tasks, tiePolicy);
|
||||
if (!ok)
|
||||
{
|
||||
response.StatusCode = 409;
|
||||
await WriteJsonAsync(response, new { error = "Cannot update settings - lobby is past the Lobby phase" });
|
||||
return;
|
||||
}
|
||||
|
||||
await WriteJsonAsync(response, new { success = true });
|
||||
}
|
||||
|
||||
#region ═══════════════════════════════════════════════════════════════════
|
||||
// WEBSOCKET - SPECTATE
|
||||
@@ -469,8 +587,11 @@ public class AdminPanel
|
||||
|
||||
try
|
||||
{
|
||||
// Pošli initial state
|
||||
await SendLobbyStateAsync(ws, lobby);
|
||||
// Pošli initial state - include the heavy mapData payload exactly
|
||||
// once per spectator session. The push loop below sends just the
|
||||
// dynamic fields (positions/votes/sabotage/etc.) at 5Hz.
|
||||
await SendLobbyStateAsync(ws, lobby, includeMapData: true);
|
||||
connection.MapDataSent = true;
|
||||
|
||||
var buffer = new byte[4096];
|
||||
var updateInterval = TimeSpan.FromMilliseconds(200);
|
||||
@@ -508,7 +629,10 @@ public class AdminPanel
|
||||
lobby = _lobbyManager.GetLobby(lobbyId);
|
||||
if (lobby != null)
|
||||
{
|
||||
await SendLobbyStateAsync(ws, lobby);
|
||||
// Subsequent ticks: skip mapData (heavy + immutable during
|
||||
// a game). Spectator's frontend cached it from the initial
|
||||
// push.
|
||||
await SendLobbyStateAsync(ws, lobby, includeMapData: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -549,7 +673,15 @@ public class AdminPanel
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendLobbyStateAsync(WebSocket ws, LobbyActor lobby)
|
||||
/// <summary>
|
||||
/// Push the lobby state to a spectator's WebSocket. When `includeMapData`
|
||||
/// is false (the default for the periodic 5Hz push loop) the heavy
|
||||
/// Overpass payload is omitted - the frontend keeps the copy it received
|
||||
/// on the initial push. This was the dominant cost in the "admin panel
|
||||
/// slow to show spectating" complaint: a typical lobby's mapData is tens
|
||||
/// of KB and serializing it 5x per second per spectator added up fast.
|
||||
/// </summary>
|
||||
private async Task SendLobbyStateAsync(WebSocket ws, LobbyActor lobby, bool includeMapData = true)
|
||||
{
|
||||
var state = new
|
||||
{
|
||||
@@ -560,7 +692,7 @@ public class AdminPanel
|
||||
joinCode = lobby.JoinCode,
|
||||
phase = lobby.Phase.ToString(),
|
||||
phaseEndTime = lobby.PhaseEndTime?.ToString("o"),
|
||||
|
||||
|
||||
players = lobby.GetPlayers().Select(p => new
|
||||
{
|
||||
id = p.PlayerId,
|
||||
@@ -574,7 +706,7 @@ public class AdminPanel
|
||||
killCooldownEnd = p.KillCooldownEnd?.ToString("o"),
|
||||
votedFor = p.VotedFor
|
||||
}),
|
||||
|
||||
|
||||
tasks = lobby.GetTasks().Select(t => new
|
||||
{
|
||||
id = t.TaskId,
|
||||
@@ -583,7 +715,7 @@ public class AdminPanel
|
||||
lng = t.Location.Lon,
|
||||
completedBy = t.CompletedBy.ToList()
|
||||
}),
|
||||
|
||||
|
||||
bodies = lobby.GetBodies().Select(b => new
|
||||
{
|
||||
victimId = b.VictimId,
|
||||
@@ -591,7 +723,7 @@ public class AdminPanel
|
||||
lng = b.Position.Lon,
|
||||
reportedAt = b.ReportedAt?.ToString("o")
|
||||
}),
|
||||
|
||||
|
||||
sabotage = lobby.ActiveSabotage != null ? new
|
||||
{
|
||||
type = lobby.ActiveSabotage.Type.ToString(),
|
||||
@@ -604,9 +736,9 @@ public class AdminPanel
|
||||
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 },
|
||||
@@ -614,15 +746,16 @@ public class AdminPanel
|
||||
impostorCount = lobby.Settings.ImpostorCount,
|
||||
taskCount = lobby.Settings.TaskCount
|
||||
},
|
||||
|
||||
mapData = lobby.MapData // Overpass data pro vykreslení mapy
|
||||
|
||||
// mapData on first push only - frontend caches it client-side.
|
||||
mapData = includeMapData ? lobby.MapData : null
|
||||
},
|
||||
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);
|
||||
@@ -792,4 +925,13 @@ public class SpectatorConnection
|
||||
public required WebSocket WebSocket { get; set; }
|
||||
public required string LobbyId { get; set; }
|
||||
public required string SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// P12: per-spectator flag to ensure we serialize the heavy `mapData`
|
||||
/// payload only once at session start. The previous push loop included
|
||||
/// it on every 200ms tick, which dominated CPU + bandwidth and made the
|
||||
/// admin panel feel laggy ("slow to show spectating") even though the
|
||||
/// position/vote/role updates were already arriving promptly.
|
||||
/// </summary>
|
||||
public bool MapDataSent { get; set; } = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user