mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-02 16:08:09 +00:00
0a40bcebf0
## Description: Enable the `sort-imports` eslint rule. Fixes #1784 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced
163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
import {
|
|
ClientMessageSchema,
|
|
ClientSendWinnerMessage,
|
|
ServerErrorMessage,
|
|
} from "../../../../../core/Schemas";
|
|
import { Client } from "../../../../Client";
|
|
import { GameServer } from "../../../../GameServer";
|
|
import { Logger } from "winston";
|
|
import { z } from "zod";
|
|
|
|
export async function postJoinMessageHandler(
|
|
gs: GameServer,
|
|
log: Logger,
|
|
client: Client,
|
|
message: string,
|
|
) {
|
|
try {
|
|
const parsed = ClientMessageSchema.safeParse(JSON.parse(message));
|
|
if (!parsed.success) {
|
|
const error = z.prettifyError(parsed.error);
|
|
log.error("Failed to parse client message", error, {
|
|
clientID: client.clientID,
|
|
});
|
|
client.ws.send(
|
|
JSON.stringify({
|
|
error,
|
|
message,
|
|
type: "error",
|
|
} satisfies ServerErrorMessage),
|
|
);
|
|
client.ws.close(1002, "ClientMessageSchema");
|
|
return;
|
|
}
|
|
const clientMsg = parsed.data;
|
|
switch (clientMsg.type) {
|
|
case "intent": {
|
|
if (clientMsg.intent.clientID !== client.clientID) {
|
|
log.warn(
|
|
`client id mismatch, client: ${client.clientID}, intent: ${clientMsg.intent.clientID}`,
|
|
);
|
|
return;
|
|
}
|
|
switch (clientMsg.intent.type) {
|
|
case "mark_disconnected": {
|
|
log.warn(`Should not receive mark_disconnected intent from client`);
|
|
return;
|
|
}
|
|
|
|
// Handle kick_player intent via WebSocket
|
|
case "kick_player": {
|
|
const authenticatedClientID = client.clientID;
|
|
|
|
// Check if the authenticated client is the lobby creator
|
|
if (authenticatedClientID !== gs.lobbyCreatorID) {
|
|
log.warn(`Only lobby creator can kick players`, {
|
|
clientID: authenticatedClientID,
|
|
creatorID: gs.lobbyCreatorID,
|
|
gameID: gs.id,
|
|
target: clientMsg.intent.target,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Don't allow lobby creator to kick themselves
|
|
if (authenticatedClientID === clientMsg.intent.target) {
|
|
log.warn(`Cannot kick yourself`, {
|
|
clientID: authenticatedClientID,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Log and execute the kick
|
|
log.info(`Lobby creator initiated kick of player`, {
|
|
creatorID: authenticatedClientID,
|
|
gameID: gs.id,
|
|
kickMethod: "websocket",
|
|
target: clientMsg.intent.target,
|
|
});
|
|
|
|
gs.kickClient(clientMsg.intent.target);
|
|
return;
|
|
}
|
|
|
|
default: {
|
|
gs.addIntent(clientMsg.intent);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "ping": {
|
|
gs.lastPingUpdate = Date.now();
|
|
client.lastPing = Date.now();
|
|
break;
|
|
}
|
|
case "hash": {
|
|
client.hashes.set(clientMsg.turnNumber, clientMsg.hash);
|
|
break;
|
|
}
|
|
case "winner": {
|
|
handleWinner(gs, log, client, clientMsg);
|
|
break;
|
|
}
|
|
default: {
|
|
log.warn(`Unknown message type: ${clientMsg.type}`, {
|
|
clientID: client.clientID,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
log.info(`error handline websocket request in game server: ${error}`, {
|
|
clientID: client.clientID,
|
|
});
|
|
}
|
|
}
|
|
|
|
function handleWinner(
|
|
gs: GameServer,
|
|
log: Logger,
|
|
client: Client, clientMsg: ClientSendWinnerMessage) {
|
|
if (
|
|
gs.outOfSyncClients.has(client.clientID) ||
|
|
gs.kickedClients.has(client.clientID) ||
|
|
gs.winner !== null ||
|
|
client.reportedWinner !== null
|
|
) {
|
|
return;
|
|
}
|
|
client.reportedWinner = clientMsg.winner;
|
|
|
|
// Add client vote
|
|
const winnerKey = JSON.stringify(clientMsg.winner);
|
|
if (!gs.winnerVotes.has(winnerKey)) {
|
|
gs.winnerVotes.set(winnerKey, { ips: new Set(), winner: clientMsg });
|
|
}
|
|
const potentialWinner = gs.winnerVotes.get(winnerKey)!;
|
|
potentialWinner.ips.add(client.ip);
|
|
|
|
const activeUniqueIPs = new Set(gs.activeClients.map((c) => c.ip));
|
|
|
|
// Require at least two unique IPs to agree
|
|
if (activeUniqueIPs.size < 2) {
|
|
return;
|
|
}
|
|
|
|
// Check if winner has majority
|
|
if (potentialWinner.ips.size * 2 < activeUniqueIPs.size) {
|
|
return;
|
|
}
|
|
|
|
// Vote succeeded
|
|
gs.winner = potentialWinner.winner;
|
|
log.info(
|
|
`Winner determined by ${potentialWinner.ips.size}/${activeUniqueIPs.size} active IPs`,
|
|
{
|
|
gameID: gs.id,
|
|
winnerKey: winnerKey,
|
|
},
|
|
);
|
|
gs.archiveGame();
|
|
}
|