diff --git a/src/server/Client.ts b/src/server/Client.ts index 68f0a2bfb..ecac7f885 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -1,13 +1,15 @@ import WebSocket from "ws"; import { TokenPayload } from "../core/ApiSchemas"; import { Tick } from "../core/game/Game"; -import { ClientID } from "../core/Schemas"; +import { ClientID, Winner } from "../core/Schemas"; export class Client { public lastPing: number = Date.now(); public hashes: Map = new Map(); + public reportedWinner: Winner | null = null; + constructor( public readonly clientID: ClientID, public readonly persistentID: string, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 0ac2621f5..c4d917e14 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -64,6 +64,11 @@ export class GameServer { private websockets: Set = new Set(); + private winnerVotes: Map< + string, + { winner: ClientSendWinnerMessage; ips: Set } + > = new Map(); + constructor( public readonly id: string, readonly log_: Logger, @@ -184,6 +189,7 @@ export class GameServer { } client.lastPing = existing.lastPing; + client.reportedWinner = existing.reportedWinner; this.activeClients = this.activeClients.filter((c) => c !== existing); } @@ -283,15 +289,7 @@ export class GameServer { break; } case "winner": { - if ( - this.outOfSyncClients.has(client.clientID) || - this.kickedClients.has(client.clientID) || - this.winner !== null - ) { - return; - } - this.winner = clientMsg; - this.archiveGame(); + this.handleWinner(client, clientMsg); break; } default: { @@ -793,4 +791,48 @@ export class GameServer { outOfSyncClients, }; } + + private handleWinner(client: Client, clientMsg: ClientSendWinnerMessage) { + if ( + this.outOfSyncClients.has(client.clientID) || + this.kickedClients.has(client.clientID) || + this.winner !== null || + client.reportedWinner !== null + ) { + return; + } + client.reportedWinner = clientMsg.winner; + + // Add client vote + const winnerKey = JSON.stringify(clientMsg.winner); + if (!this.winnerVotes.has(winnerKey)) { + this.winnerVotes.set(winnerKey, { ips: new Set(), winner: clientMsg }); + } + const potentialWinner = this.winnerVotes.get(winnerKey)!; + potentialWinner.ips.add(client.ip); + + const activeUniqueIPs = new Set(this.activeClients.map((c) => c.ip)); + + const ratio = `${potentialWinner.ips.size}/${activeUniqueIPs.size}`; + this.log.info( + `recieved winner vote ${clientMsg.winner}, ${ratio} votes for this winner`, + { + clientID: client.clientID, + }, + ); + + if (potentialWinner.ips.size * 2 < activeUniqueIPs.size) { + return; + } + + // Vote succeeded + this.winner = potentialWinner.winner; + this.log.info( + `Winner determined by ${potentialWinner.ips.size}/${activeUniqueIPs.size} active IPs`, + { + winnerKey: winnerKey, + }, + ); + this.archiveGame(); + } }