Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 04895c65bc Fix final Shardok battle results not displaying before model removal
The ShardokGameModel was being removed from ShardokGameModels BEFORE
UpdateAction.Invoke() was called. This meant the UI callback received
a model that no longer contained the ended battle's final results.

Flow before:
1. Final Shardok results arrive with GameStatus = Victory/Defeat
2. Model updated with final results
3. Model REMOVED from ShardokGameModels
4. UpdateAction.Invoke() - UI doesn't see the model
5. Final results never displayed

Flow after:
1. Final Shardok results arrive with GameStatus = Victory/Defeat
2. Model updated with final results
3. Model STAYS in ShardokGameModels
4. UpdateAction.Invoke() - UI sees model with final results
5. Model removed AFTER callback

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 17:09:15 -08:00
adminandClaude Opus 4.5 e1e8c0abe7 Fix missing final Shardok battle results due to race condition
When Eagle's RemovedBattleIds update arrived before the Shardok update
with final battle results, the Shardok results were dropped because:

1. RemovedBattleIds processing removed the battle from ShardokBattles
2. Shardok update arrived, but ShardokGameModels didn't have a model
3. MakeGameModel() tried to create one but returned null (no battle in ShardokBattles)
4. The Shardok update was skipped with `continue`, dropping final results

Fix: Defer ShardokBattles cleanup from RemovedBattleIds processing to
the Shardok handler. This ensures final Shardok results can still be
processed even if the Eagle "battle ended" update arrives first.

This was causing "last turns of the battle" to not appear when watching
AI games, as the final Shardok turns were being dropped.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 14:58:27 -08:00
adminandClaude Opus 4.5 2b8a32b6e0 Fix missing battle results by always sending count updates
When `filteredResults` was empty (e.g., AI turns filtered out as not
visible to the human player), the `update` method returned early
without sending a message to the client. However, it still advanced
the server's tracking of the client's count (`unfilteredKnownHistoryCount`).

This caused sync mismatches: the server thought the client was up-to-date
(so it wouldn't send the "missing" results on subsequent updates), but
the client never received the new count.

The fix: always call `afterSendingResults`, even when `filteredResults`
is empty. This ensures the client receives the count update (plus
`availableCommands` and `serverGameStatus`) even when there are no
visible results.

This was particularly problematic after battles ended, when AI turns
might be filtered out, causing the "last turns of the battle" to never
appear on the client.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 14:48:54 -08:00
2 changed files with 45 additions and 24 deletions
@@ -348,6 +348,10 @@ namespace eagle {
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
var specResponse = updateItem.ShardokActionResultResponse.ShardokGameResponses;
// Collect ended games to remove AFTER the UI callback, so the UI
// can see the final battle results before the models are removed.
var endedGames = new List<ShardokGameId>();
foreach (var oneResponse in specResponse) {
if (!_currentModel.ShardokGameModels.TryGetValue(
oneResponse.ShardokGameId,
@@ -388,14 +392,32 @@ namespace eagle {
_currentModel.ShardokGameModels[oneResponse.ShardokGameId] =
shardokGameModel;
} else {
// Game ended - remove from active models so UI knows battle is over
_currentModel.ShardokGameModels.TryRemove(
oneResponse.ShardokGameId,
out _);
_shardokResultCounts.TryRemove(oneResponse.ShardokGameId, out _);
// Game ended - mark for removal after UI callback
_currentModel.ShardokGameModels[oneResponse.ShardokGameId] =
shardokGameModel;
endedGames.Add(oneResponse.ShardokGameId);
}
}
// Invoke UI callback BEFORE removing ended games, so the UI can
// see the final battle results (with GameStatus = Victory/Defeat)
if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
// Now remove ended games from active models
foreach (var endedGameId in endedGames) {
_currentModel.ShardokGameModels.TryRemove(endedGameId, out _);
_shardokResultCounts.TryRemove(endedGameId, out _);
// Also remove from ShardokBattles. We defer this cleanup to here
// (rather than in RemovedBattleIds processing) to ensure Shardok
// results with final battle turns are processed before cleanup.
for (int i = 0; i < _currentModel.ShardokBattles.Count; i++) {
if (_currentModel.ShardokBattles[i].ShardokGameId == endedGameId) {
_currentModel.ShardokBattles.RemoveAt(i);
break;
}
}
}
break;
case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
@@ -837,12 +859,12 @@ namespace eagle {
}
_shardokResultCounts.TryRemove(rb, out _);
for (int i = 0; i < _currentModel.ShardokBattles.Count; i++) {
if (_currentModel.ShardokBattles[i].ShardokGameId == rb) {
_currentModel.ShardokBattles.RemoveAt(i);
break;
}
}
// NOTE: We intentionally do NOT remove from ShardokBattles here.
// Shardok results (with the final battle turns) may arrive AFTER this
// Eagle update. If we remove from ShardokBattles now, MakeGameModel()
// would return null and the final Shardok results would be dropped.
// The cleanup from ShardokBattles happens when processing the Shardok
// update with "game over" status (see ShardokActionResultResponse handler).
}
foreach (BattalionType bt in entry.NewBattalionTypes) {
@@ -188,20 +188,19 @@ case class HumanPlayerClientConnectionState(
if knownShardokResults.isEmpty then this
else this.enqueueShardokResults(knownShardokResults)
val newHistoryClient = afterShardok.withNewCount(
unfilteredCountAfter = unfilteredCountAfter
// Always send the count update to the client, even when filteredResults is empty.
// Previously, we returned early when filteredResults.isEmpty, which advanced the
// server's tracking without notifying the client. This caused sync mismatches:
// the server thought the client was up-to-date, but the client never received
// the new count. This was particularly problematic after battles ended, when
// AI turns might be filtered out (not visible to the human player).
afterShardok.afterSendingResults(
availableCommands = availableCommands,
startingState = None,
filteredResults = filteredResults,
unfilteredCountAfter = unfilteredCountAfter,
serverGameStatus = serverGameStatus
)
if filteredResults.isEmpty then newHistoryClient
else
newHistoryClient
.afterSendingResults(
availableCommands = availableCommands,
startingState = None,
filteredResults = filteredResults,
unfilteredCountAfter = unfilteredCountAfter,
serverGameStatus = serverGameStatus
)
}
private def withNewCount(