mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:50:43 +00:00
have server check hashes, crash game if out of sync
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user