From 9563c189eb2e07dbc1edbd2906fd603b0637bb9c Mon Sep 17 00:00:00 2001 From: evanpelle Date: Fri, 23 May 2025 13:04:51 -0700 Subject: [PATCH] Archive game when client sends win message Fixes #823 (#852) ## Description Have the Game Server archive when it receives a win event instead of when the game completes. It will still archive when the game completes if there are no winners. It only uses the winner message from clients that are in sync and are not kicked. * Fixed a bug with the stats collection, the arrays were not expanded enough causing NaN to be inserted into the array which caused Zod validation to fail * Fixed a bug with the win modal, was incorrectly showing player as winner even if they lost. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: --- src/client/ClientGameRunner.ts | 4 +- src/client/graphics/layers/WinModal.ts | 7 ++- src/core/game/StatsImpl.ts | 8 +-- src/server/GameServer.ts | 82 ++++++++++++++++---------- 4 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index f1b233987..b9530fbc1 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -187,7 +187,9 @@ export class ClientGameRunner { } private saveGame(update: WinUpdate) { - if (this.myPlayer === null) throw new Error("Not initialized"); + if (this.myPlayer === null) { + return; + } const players: PlayerRecord[] = [ { playerID: this.myPlayer.id(), diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 8a99b6b38..c287530a8 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -207,10 +207,13 @@ export class WinModal extends LitElement implements Layer { new SendWinnerEvent(winnerClient, wu.allPlayersStats, "player"), ); } - if (winner === this.game.myPlayer()) { + if ( + winnerClient !== null && + winnerClient === this.game.myPlayer()?.clientID() + ) { this._title = translateText("win_modal.you_won"); } else { - this._title = translateText("win_modal.you_won", { + this._title = translateText("win_modal.other_won", { player: winner.name(), }); } diff --git a/src/core/game/StatsImpl.ts b/src/core/game/StatsImpl.ts index e81f1d74c..2546fa2cb 100644 --- a/src/core/game/StatsImpl.ts +++ b/src/core/game/StatsImpl.ts @@ -56,7 +56,7 @@ export class StatsImpl implements Stats { const p = this._makePlayerStats(player); if (p === undefined) return; if (p.attacks === undefined) p.attacks = [0]; - while (p.attacks.length < index) p.attacks.push(0); + while (p.attacks.length <= index) p.attacks.push(0); p.attacks[index] += value; } @@ -80,7 +80,7 @@ export class StatsImpl implements Stats { if (p === undefined) return; if (p.boats === undefined) p.boats = { [type]: [0] }; if (p.boats[type] === undefined) p.boats[type] = [0]; - while (p.boats[type].length < index) p.boats[type].push(0); + while (p.boats[type].length <= index) p.boats[type].push(0); p.boats[type][index] += value; } @@ -95,7 +95,7 @@ export class StatsImpl implements Stats { if (p === undefined) return; if (p.bombs === undefined) p.bombs = { [type]: [0] }; if (p.bombs[type] === undefined) p.bombs[type] = [0]; - while (p.bombs[type].length < index) p.bombs[type].push(0); + while (p.bombs[type].length <= index) p.bombs[type].push(0); p.bombs[type][index] += value; } @@ -103,7 +103,7 @@ export class StatsImpl implements Stats { const p = this._makePlayerStats(player); if (p === undefined) return; if (p.gold === undefined) p.gold = [0]; - while (p.gold.length < index) p.gold.push(0); + while (p.gold.length <= index) p.gold.push(0); p.gold[index] += value; } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index d10a6e050..949bd2b68 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -56,6 +56,7 @@ export class GameServer { private _hasPrestarted = false; private kickedClients: Set = new Set(); + private outOfSyncClients: Set = new Set(); constructor( public readonly id: string, @@ -200,7 +201,15 @@ export class GameServer { client.hashes.set(clientMsg.turnNumber, clientMsg.hash); } if (clientMsg.type === "winner") { + if ( + this.outOfSyncClients.has(client.clientID) || + this.kickedClients.has(client.clientID) || + this.winner !== null + ) { + return; + } this.winner = clientMsg; + this.archiveGame(); } } catch (error) { this.log.info( @@ -382,40 +391,16 @@ export class GameServer { } this.log.info(`ending game with ${this.turns.length} turns`); try { - if (this.allClients.size > 0) { - const playerRecords: PlayerRecord[] = Array.from( - this.allClients.values(), - ).map((client) => { - const stats = this.winner?.allPlayersStats[client.clientID]; - if (stats === undefined) { - this.log.warn( - `Unable to find stats for clientID ${client.clientID}`, - ); - } - return { - playerID: client.playerID, - clientID: client.clientID, - username: client.username, - persistentID: client.persistentID, - stats, - } satisfies PlayerRecord; - }); - archive( - createGameRecord( - this.id, - this.gameStartInfo.config, - playerRecords, - this.turns, - this._startTime ?? 0, - Date.now(), - this.winner?.winner ?? null, - this.winner?.winnerType ?? null, - ), - ); - } else { + if (this.allClients.size === 0) { this.log.info("no clients joined, not archiving game", { gameID: this.id, }); + } else if (this.winner !== null) { + this.log.info("game already archived", { + gameID: this.id, + }); + } else { + this.archiveGame(); } } catch (error) { let errorDetails; @@ -549,6 +534,40 @@ export class GameServer { } } + private archiveGame() { + this.log.info("archiving game", { + gameID: this.id, + winner: this.winner?.winner, + }); + const playerRecords: PlayerRecord[] = Array.from( + this.allClients.values(), + ).map((client) => { + const stats = this.winner?.allPlayersStats[client.clientID]; + if (stats === undefined) { + this.log.warn(`Unable to find stats for clientID ${client.clientID}`); + } + return { + playerID: client.playerID, + clientID: client.clientID, + username: client.username, + persistentID: client.persistentID, + stats, + } satisfies PlayerRecord; + }); + archive( + createGameRecord( + this.id, + this.gameStartInfo.config, + playerRecords, + this.turns, + this._startTime ?? 0, + Date.now(), + this.winner?.winner ?? null, + this.winner?.winnerType ?? null, + ), + ); + } + private handleSynchronization() { if (this.activeClients.length <= 1) { return; @@ -586,6 +605,7 @@ export class GameServer { const desyncMsg = JSON.stringify(serverDesync.data); for (const c of outOfSyncClients) { + this.outOfSyncClients.add(c.clientID); if (this.sentDesyncMessageClients.has(c.clientID)) { continue; }