fixes 2
This commit is contained in:
229
LobbyActor.cs
229
LobbyActor.cs
@@ -244,12 +244,72 @@ public class LobbyActor : IDisposable
|
||||
{
|
||||
if (!_players.ContainsKey(playerId))
|
||||
return false;
|
||||
|
||||
|
||||
// Odstraníme hráče asynchronně
|
||||
_ = RemovePlayerAsync(playerId, reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── P12: Admin lobby manipulation ────────────────────────────────────────
|
||||
// These methods are the building blocks for the new admin-panel endpoints
|
||||
// (force-end, force-phase, edit-settings). Each enqueues an action via
|
||||
// the existing actor channel so mutation stays serialized with the rest
|
||||
// of the lobby's state machine - no race against tick handlers.
|
||||
|
||||
/// <summary>
|
||||
/// Force-end the current game immediately. No winner is declared (the
|
||||
/// admin is interrupting outside normal win conditions); the lobby falls
|
||||
/// back to the lobby phase ready for a new round. Safe to call in any
|
||||
/// phase - the actor will no-op gracefully if there's nothing to end.
|
||||
/// </summary>
|
||||
public async Task<bool> TryForceEndGameAsync(string adminReason = "ended by admin")
|
||||
{
|
||||
var done = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
await EnqueueAsync(new AdminForceEndGameAction
|
||||
{
|
||||
Reason = adminReason,
|
||||
Result = done
|
||||
});
|
||||
return await done.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force the lobby into a specific phase. Used for testing / unsticking
|
||||
/// games where the natural phase-transition didn't fire. Accepts
|
||||
/// "Lobby", "Loading", "Playing", "Meeting", "Ended" (string for the
|
||||
/// JSON wire); "Voting" intentionally rejected because Voting is a
|
||||
/// sub-window of Meeting on the wire (see Phase 1 dead-code comment).
|
||||
/// </summary>
|
||||
public async Task<bool> TryAdminSetPhaseAsync(string targetPhase)
|
||||
{
|
||||
if (!System.Enum.TryParse<GamePhase>(targetPhase, ignoreCase: true, out var p))
|
||||
return false;
|
||||
if (p == GamePhase.Voting) return false; // dead value
|
||||
|
||||
var done = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
await EnqueueAsync(new AdminSetPhaseAction { TargetPhase = p, Result = done });
|
||||
return await done.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update mutable lobby settings. Only takes effect during Lobby phase -
|
||||
/// changing radius/impostor count mid-game would be silly. Returns false
|
||||
/// if any required field is invalid OR the lobby is past the lobby phase.
|
||||
/// </summary>
|
||||
public async Task<bool> TryUpdateLobbySettingsAsync(double? radius, int? impostorCount, int? taskCount, string? tiePolicy)
|
||||
{
|
||||
var done = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
await EnqueueAsync(new AdminUpdateSettingsAction
|
||||
{
|
||||
NewRadius = radius,
|
||||
NewImpostorCount = impostorCount,
|
||||
NewTaskCount = taskCount,
|
||||
NewTiePolicy = tiePolicy,
|
||||
Result = done
|
||||
});
|
||||
return await done.Task;
|
||||
}
|
||||
|
||||
public async Task ArchiveAndCloseAsync()
|
||||
{
|
||||
_cts.Cancel();
|
||||
@@ -329,6 +389,15 @@ public class LobbyActor : IDisposable
|
||||
case SabotageExpireAction a:
|
||||
ProcessSabotageExpire(a);
|
||||
break;
|
||||
case AdminForceEndGameAction a:
|
||||
ProcessAdminForceEndGame(a);
|
||||
break;
|
||||
case AdminSetPhaseAction a:
|
||||
ProcessAdminSetPhase(a);
|
||||
break;
|
||||
case AdminUpdateSettingsAction a:
|
||||
ProcessAdminUpdateSettings(a);
|
||||
break;
|
||||
}
|
||||
|
||||
// Signalizace dokončení pro akce, které na to čekají
|
||||
@@ -1889,9 +1958,136 @@ public class LobbyActor : IDisposable
|
||||
/// Get current sabotage info (for state sync)
|
||||
/// </summary>
|
||||
public Sabotage? GetCurrentSabotage() => _currentSabotage;
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region P12 Admin actions
|
||||
|
||||
/// <summary>
|
||||
/// Admin force-end-game implementation. Tears down active meeting/sabotage
|
||||
/// state, sets phase to Ended, and broadcasts a GameEnded event with
|
||||
/// "admin" as the winning faction so spectators see the interruption
|
||||
/// reason. The lobby actor stays alive for a return-to-lobby loop.
|
||||
/// </summary>
|
||||
private void ProcessAdminForceEndGame(AdminForceEndGameAction action)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_phase == GamePhase.Lobby || _phase == GamePhase.Ended)
|
||||
{
|
||||
action.Result.TrySetResult(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_currentMeeting = null;
|
||||
_currentSabotage = null;
|
||||
_bodies.Clear();
|
||||
_phase = GamePhase.Ended;
|
||||
|
||||
var evt = CreateEvent("GameEnded", "admin", new GameEndedPayload
|
||||
{
|
||||
WinningFaction = "Admin",
|
||||
Reason = action.Reason
|
||||
});
|
||||
PersistAndBroadcast(evt);
|
||||
|
||||
_logger.LogWarning("[Admin] Force-ended game in lobby {Id}: {Reason}", _lobbyId, action.Reason);
|
||||
action.Result.TrySetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[Admin] ForceEndGame failed");
|
||||
action.Result.TrySetResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Admin set-phase implementation. Skips most of the natural phase-
|
||||
/// transition side effects (role assignment, map fetch etc.) - it's an
|
||||
/// override, not a full transition. Used to unstick games or jump to
|
||||
/// Ended for cleanup. The phase change is broadcast so spectators see
|
||||
/// the new state on their next push.
|
||||
/// </summary>
|
||||
private void ProcessAdminSetPhase(AdminSetPhaseAction action)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prev = _phase;
|
||||
_phase = action.TargetPhase;
|
||||
|
||||
var evt = CreateEvent("PhaseChanged", "admin", new PhaseChangedPayload
|
||||
{
|
||||
Phase = action.TargetPhase
|
||||
});
|
||||
PersistAndBroadcast(evt);
|
||||
|
||||
_logger.LogWarning("[Admin] Force-set phase {Prev} -> {Next} in lobby {Id}", prev, action.TargetPhase, _lobbyId);
|
||||
action.Result.TrySetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[Admin] SetPhase failed");
|
||||
action.Result.TrySetResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Admin update-settings implementation. Only takes effect during the
|
||||
/// Lobby phase (changing impostor count mid-game would be incoherent;
|
||||
/// changing radius requires a re-fetch of map data which we don't try
|
||||
/// to coordinate from here). Each field is individually optional - only
|
||||
/// non-null values are applied.
|
||||
/// </summary>
|
||||
private void ProcessAdminUpdateSettings(AdminUpdateSettingsAction action)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_phase != GamePhase.Lobby)
|
||||
{
|
||||
action.Result.TrySetResult(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.NewRadius.HasValue && action.NewRadius.Value > 0)
|
||||
Settings.PlayAreaRadius = action.NewRadius.Value;
|
||||
|
||||
if (action.NewImpostorCount.HasValue && action.NewImpostorCount.Value >= 1 && action.NewImpostorCount.Value <= 8)
|
||||
Settings.ImpostorCount = action.NewImpostorCount.Value;
|
||||
|
||||
if (action.NewTaskCount.HasValue && action.NewTaskCount.Value >= 1 && action.NewTaskCount.Value <= 30)
|
||||
Settings.TaskCount = action.NewTaskCount.Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(action.NewTiePolicy) &&
|
||||
System.Enum.TryParse<TiePolicy>(action.NewTiePolicy, ignoreCase: true, out var tp))
|
||||
Settings.TiePolicy = tp;
|
||||
|
||||
// Broadcast a settings-changed event so all clients refresh their
|
||||
// lobby UI with the new values. Reuses the existing PhaseChanged
|
||||
// payload shape with current phase as a no-op-style notification;
|
||||
// a dedicated SettingsChanged payload would be cleaner but
|
||||
// requires touching all three Protocol.cs files.
|
||||
var evt = CreateEvent("LobbySettingsChanged", "admin", new LobbySettingsChangedPayload
|
||||
{
|
||||
Radius = Settings.PlayAreaRadius,
|
||||
ImpostorCount = Settings.ImpostorCount,
|
||||
TaskCount = Settings.TaskCount,
|
||||
TiePolicy = Settings.TiePolicy.ToString()
|
||||
});
|
||||
PersistAndBroadcast(evt);
|
||||
|
||||
_logger.LogInformation("[Admin] Updated settings in lobby {Id}: radius={R} impostors={I} tasks={T} tie={Tp}",
|
||||
_lobbyId, Settings.PlayAreaRadius, Settings.ImpostorCount, Settings.TaskCount, Settings.TiePolicy);
|
||||
action.Result.TrySetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[Admin] UpdateSettings failed");
|
||||
action.Result.TrySetResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Snapshots
|
||||
|
||||
private void CheckSnapshot()
|
||||
@@ -1990,6 +2186,33 @@ class SabotageExpireAction : LobbyAction
|
||||
public required string SabotageId { get; set; }
|
||||
}
|
||||
|
||||
// ── P12: Admin lobby manipulation actions ────────────────────────────────────
|
||||
// Each carries a TaskCompletionSource<bool> so the caller can await the
|
||||
// outcome (true = applied, false = rejected by validation / phase gate).
|
||||
// The completion is signaled inside the per-action Process* implementation,
|
||||
// not by the generic ProcessActionAsync trySetResult hook.
|
||||
|
||||
class AdminForceEndGameAction : LobbyAction
|
||||
{
|
||||
public required string Reason { get; set; }
|
||||
public required TaskCompletionSource<bool> Result { get; set; }
|
||||
}
|
||||
|
||||
class AdminSetPhaseAction : LobbyAction
|
||||
{
|
||||
public required GamePhase TargetPhase { get; set; }
|
||||
public required TaskCompletionSource<bool> Result { get; set; }
|
||||
}
|
||||
|
||||
class AdminUpdateSettingsAction : LobbyAction
|
||||
{
|
||||
public double? NewRadius { get; set; }
|
||||
public int? NewImpostorCount { get; set; }
|
||||
public int? NewTaskCount { get; set; }
|
||||
public string? NewTiePolicy { get; set; }
|
||||
public required TaskCompletionSource<bool> Result { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Stub pro ClientConnection - bude implementováno v Program.cs
|
||||
|
||||
Reference in New Issue
Block a user