Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 3ed9982822 Add JoinGameRequest on reconnect to ensure game is loaded
Server:
- Add ensureGameLoaded() to joinGame's case None branch
- This triggers game loading even for already-started games

Client:
- Send JoinGameRequest before StreamGameRequest in StreamOneGameAsync
- This ensures the game is loaded on the server before subscribing
- JoinGameRequest returns GAME_FULL for started games but still loads them

Together with the command handler fix, this provides belt-and-suspenders
protection against "key not found" errors after reconnect.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:24:47 -08:00
adminandClaude Opus 4.5 dc351a48b8 Server: ensure game is loaded before processing commands
Add ensureGameLoaded() calls to postCommand, postShardokCommand, and
postPlacementCommands. This handles the case where a command arrives
before the game subscription has loaded the game into memory.

This is cheap (just a map contains check) if the game is already loaded,
but prevents "key not found" errors when commands race with subscriptions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:23:39 -08:00
2 changed files with 31 additions and 0 deletions
@@ -196,6 +196,20 @@ namespace eagle {
/// </summary>
private async Task<bool> StreamOneGameAsync(IClientConnectionSubscriber subscriber) {
var gameId = subscriber.GameId;
// Send JoinGameRequest first to ensure game is loaded on server.
// For already-started games, this returns GAME_FULL but still triggers
// game loading, which prevents "key not found" errors on subsequent commands.
var joinRequest = new UpdateStreamRequest {
JoinGameRequest = new JoinGameRequest { GameId = gameId }
};
await DoWithStreamingCall(async (sc) => {
await sc.RequestStream.WriteAsync(joinRequest);
_remoteEagleClientLogger.LogLine(
$"[SUBSCRIBE] Sent JoinGameRequest for game {gameId} to ensure game is loaded");
return true;
});
var streamGameRequest = new StreamGameRequest {
GameId = gameId,
UnfilteredResultCount = subscriber.LastUnfilteredResultCount
@@ -778,6 +778,11 @@ class GamesManager(
token: Long
): Unit =
this.synchronized {
// Lazy load: ensure game is in memory before processing command
EagleRequire.clientRequire(
ensureGameLoaded(gameId),
s"No game found with ID ${gameId.toHexString}"
)
val _ = synchronizedHandlePostResults(
gameControllerInfos(gameId).controller
.postHumanCommand(
@@ -802,6 +807,11 @@ class GamesManager(
index: Int,
roll: Option[Int]
): Unit = this.synchronized {
// Lazy load: ensure game is in memory before processing command
EagleRequire.clientRequire(
ensureGameLoaded(eagleGameId),
s"No game found with ID ${eagleGameId.toHexString}"
)
// FIXME: need to verify username match
SimpleTimedLogger
.fileLogger(eagleGameId, "timings")
@@ -831,6 +841,11 @@ class GamesManager(
placementCommands: Vector[ShardokPlacementCommand],
shardokToken: Long
): Unit = this.synchronized {
// Lazy load: ensure game is in memory before processing command
EagleRequire.clientRequire(
ensureGameLoaded(eagleGameId),
s"No game found with ID ${eagleGameId.toHexString}"
)
// FIXME: need to verify username match
shardokClient.postPlacementCommands(
eagleGameId = eagleGameId,
@@ -987,6 +1002,8 @@ class GamesManager(
gameId = gameId
)
case None =>
// Game not in lobby - try to load it (useful for reconnection scenarios)
ensureGameLoaded(gameId)
JoinGameResponse(
JoinGameResult.GAME_FULL_JOIN_GAME_RESULT,
gameId = gameId