From ff33c2db507f65dad1add1f9d1f7fcaf45d8c365 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 25 Feb 2025 16:24:22 -0800 Subject: [PATCH] have server check hashes, crash game if out of sync --- src/client/ClientGameRunner.ts | 7 +++ src/client/Transport.ts | 4 +- src/core/Schemas.ts | 15 +++++- src/core/game/GameImpl.ts | 1 + src/server/GameServer.ts | 88 ++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 95561e35f..985060473 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -213,6 +213,13 @@ export class ClientGameRunner { this.turnsSeen++; } } + if (message.type == "desync") { + showErrorModal( + `desync from server: ${JSON.stringify(message)}`, + "", + this.clientID, + ); + } if (message.type == "turn") { if (!this.hasJoined) { this.transport.joinGame(0); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 5cdbeb2fe..2a64b4017 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -246,7 +246,9 @@ export class Transport { const serverMsg = ServerMessageSchema.parse(JSON.parse(event.data)); this.onmessage(serverMsg); } catch (error) { - console.error("Failed to process server message:", error); + console.error( + `Failed to process server message ${event.data}: ${error}`, + ); } }; this.socket.onerror = (err) => { diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 95d0833dc..74ce4f52f 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -53,13 +53,15 @@ export type ClientMessage = export type ServerMessage = | ServerSyncMessage | ServerStartGameMessage - | ServerPingMessage; + | ServerPingMessage + | ServerDesyncMessage; export type ServerSyncMessage = z.infer; export type ServerStartGameMessage = z.infer< typeof ServerStartGameMessageSchema >; export type ServerPingMessage = z.infer; +export type ServerDesyncMessage = z.infer; export type ClientSendWinnerMessage = z.infer; export type ClientPingMessage = z.infer; @@ -242,7 +244,7 @@ export const TurnSchema = z.object({ // Server const ServerBaseMessageSchema = z.object({ - type: SafeString, + type: z.enum(["turn", "ping", "start", "desync"]), }); export const ServerTurnMessageSchema = ServerBaseMessageSchema.extend({ @@ -261,10 +263,19 @@ export const ServerStartGameMessageSchema = ServerBaseMessageSchema.extend({ config: GameConfigSchema, }); +export const ServerDesyncSchema = ServerBaseMessageSchema.extend({ + type: z.literal("desync"), + turn: z.number(), + correctHash: z.number().nullable(), + clientsWithCorrectHash: z.number(), + totalActiveClients: z.number(), +}); + export const ServerMessageSchema = z.union([ ServerTurnMessageSchema, ServerStartGameMessageSchema, ServerPingMessageSchema, + ServerDesyncSchema, ]); // Client diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index a83ad78cd..97f380c06 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -30,6 +30,7 @@ import { UnitImpl } from "./UnitImpl"; import { consolex } from "../Consolex"; import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap"; import { DefenseGrid } from "./DefensePostGrid"; +import { simpleHash } from "../Util"; export function createGame( gameMap: GameMap, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index cae877f40..9cb1ccb8a 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -5,6 +5,8 @@ import { GameConfig, Intent, PlayerRecord, + ServerDesyncSchema, + ServerMessageSchema, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn, @@ -253,6 +255,8 @@ export class GameServer { this.turns.push(pastTurn); this.intents = []; + this.maybeSendDesync(); + let msg = ""; try { msg = JSON.stringify( @@ -265,6 +269,7 @@ export class GameServer { console.log(`error sending message for game ${this.id}`); return; } + this.activeClients.forEach((c) => { c.ws.send(msg); }); @@ -383,4 +388,87 @@ export class GameServer { hasStarted(): boolean { return this._hasStarted; } + + private maybeSendDesync() { + if (this.activeClients.length <= 1) { + return; + } + if (this.turns.length % 10 == 0 && this.turns.length != 0) { + const lastHashTurn = this.turns.length - 10; + console.log(`checking validity for turn ${lastHashTurn}`); + + let { mostCommonHash, outOfSyncClients } = + this.findOutOfSyncClients(lastHashTurn); + + if ( + outOfSyncClients.length >= Math.floor(this.activeClients.length / 2) + ) { + // If half clients out of sync assume all are out of sync. + outOfSyncClients = this.activeClients; + } + + const serverDesync = ServerDesyncSchema.safeParse({ + type: "desync", + turn: lastHashTurn, + correctHash: mostCommonHash, + clientsWithCorrectHash: + this.activeClients.length - outOfSyncClients.length, + totalActiveClients: this.activeClients.length, + }); + if (serverDesync.success) { + const desyncMsg = JSON.stringify(serverDesync.data); + for (const c of outOfSyncClients) { + console.log( + `game: ${this.id}: sending desync to client ${c.clientID}`, + ); + c.ws.send(desyncMsg); + } + } else { + console.warn(`failed to create desync message ${serverDesync.error}`); + } + } + } + + findOutOfSyncClients(turnNumber: number): { + mostCommonHash: number | null; + outOfSyncClients: Client[]; + } { + const counts = new Map(); + + // Count occurrences of each hash + for (const client of this.activeClients) { + if (client.hashes.has(turnNumber)) { + const clientHash = client.hashes.get(turnNumber)!; + counts.set(clientHash, (counts.get(clientHash) || 0) + 1); + } + } + + // Find the most common hash + let mostCommonHash: number | null = null; + let maxCount = 0; + + for (const [hash, count] of counts.entries()) { + if (count > maxCount) { + mostCommonHash = hash; + maxCount = count; + } + } + + // Create a list of clients whose hash doesn't match the most common one + const outOfSyncClients: Client[] = []; + + for (const client of this.activeClients) { + if (client.hashes.has(turnNumber)) { + const clientHash = client.hashes.get(turnNumber)!; + if (clientHash !== mostCommonHash) { + outOfSyncClients.push(client); + } + } + } + + return { + mostCommonHash, + outOfSyncClients, + }; + } }