fix(online): green-felt freeze — replay initial state to late subscriber
Root cause: the server sends matchFound then immediately broadcasts the first state, but the client only subscribes to state inside enterServerMatch, which runs a React effect later — so the ordered "state" message is dispatched while there are no subscribers and is dropped. The server then waits for the human hakem's trump choice that can never come → permanent freeze on the green felt. - signalr-service: cache lastState; replay it to a late onState subscriber on a microtask (after enterServerMatch resets its store); clear the cache on every fresh-match entry (startMatchmaking / createRoom / acceptInvite) so a finished game's final state is never replayed into a new match. - safety net: if no state lands within 2.5s of matchFound, the client invokes the new Resync hub method; server re-sends the current state to that player. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,13 @@ public sealed partial class GameManager
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Client safety net: re-send the current game state to a player who
|
||||
/// may have missed the initial broadcast (green-felt freeze guard).</summary>
|
||||
public void Resync(string userId)
|
||||
{
|
||||
if (RoomOf(userId) is { } room) room.ResendStateTo(userId);
|
||||
}
|
||||
|
||||
/// <summary>Player asked to start right now — match any humans waiting and
|
||||
/// fill the rest with bots, instead of waiting out the queue.</summary>
|
||||
public void PlayNow(string userId)
|
||||
|
||||
@@ -387,6 +387,19 @@ public sealed class GameRoom : IDisposable
|
||||
_ = _hub.Clients.User(slot.UserId!).SendAsync("state", ToDto(slot.Seat));
|
||||
}
|
||||
|
||||
/// <summary>Re-send the current state to one player on demand — the client's
|
||||
/// safety net when the initial broadcast was dropped/raced and the table
|
||||
/// would otherwise freeze waiting for a state that never arrived.</summary>
|
||||
public void ResendStateTo(string userId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var slot = Seats.FirstOrDefault(s => !s.IsBot && s.UserId == userId);
|
||||
if (slot != null)
|
||||
_ = _hub.Clients.User(userId).SendAsync("state", ToDto(slot.Seat));
|
||||
}
|
||||
}
|
||||
|
||||
private void Broadcast(string method, object payload)
|
||||
{
|
||||
foreach (var slot in Seats.Where(s => !s.IsBot && s.UserId != null && s.Connected))
|
||||
|
||||
@@ -38,6 +38,7 @@ public sealed class GameHub : Hub
|
||||
|
||||
public void CancelMatchmaking() => _manager.CancelMatchmaking(Uid);
|
||||
public void PlayNow() => _manager.PlayNow(Uid);
|
||||
public void Resync() => _manager.Resync(Uid);
|
||||
public void PlayCard(string cardId) => _manager.PlayCard(Uid, cardId);
|
||||
public void ChooseTrump(string suit) => _manager.ChooseTrump(Uid, suit);
|
||||
public void SendReaction(string reaction) => _manager.SendReaction(Uid, reaction);
|
||||
|
||||
Reference in New Issue
Block a user