have server check hashes, crash game if out of sync

This commit is contained in:
Evan
2025-02-25 16:24:22 -08:00
parent d92a68e958
commit ff33c2db50
5 changed files with 112 additions and 3 deletions
+7
View File
@@ -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);
+3 -1
View File
@@ -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) => {
+13 -2
View File
@@ -53,13 +53,15 @@ export type ClientMessage =
export type ServerMessage =
| ServerSyncMessage
| ServerStartGameMessage
| ServerPingMessage;
| ServerPingMessage
| ServerDesyncMessage;
export type ServerSyncMessage = z.infer<typeof ServerTurnMessageSchema>;
export type ServerStartGameMessage = z.infer<
typeof ServerStartGameMessageSchema
>;
export type ServerPingMessage = z.infer<typeof ServerPingMessageSchema>;
export type ServerDesyncMessage = z.infer<typeof ServerDesyncSchema>;
export type ClientSendWinnerMessage = z.infer<typeof ClientSendWinnerSchema>;
export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>;
@@ -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
+1
View File
@@ -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,
+88
View File
@@ -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<number, number>();
// 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,
};
}
}