From e1b1beb24dbc6fa538a0544047528b9e244c1feb Mon Sep 17 00:00:00 2001 From: floriankilian Date: Thu, 14 Aug 2025 22:15:32 +0200 Subject: [PATCH] feat(kick-system): add translated kick reasons with error codes - Add kickReason field to ServerErrorMessage schema with enum values - Display translated kick messages instead of generic "Kicked from game" - Include error codes (KICK_ADMIN, KICK_MULTI_TAB, KICK_LOBBY_CREATOR) for debugging - Update kick handlers in GameServer, PostJoinHandler, and Worker to pass reasons - Modify showErrorModal to handle kick-specific formatting without duplication --- resources/lang/en.json | 6 +- src/client/ClientGameRunner.ts | 68 ++++++++++++++----- src/core/Schemas.ts | 1 + src/server/GameServer.ts | 9 ++- src/server/Worker.ts | 2 +- .../handler/message/PostJoinHandler.ts | 2 +- 6 files changed, 64 insertions(+), 24 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 34eec63f9a..5d00b85221 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -545,7 +545,11 @@ "copy_clipboard": "Copy to clipboard", "copied": "Copied!", "failed_copy": "Failed to copy", - "desync_notice": "You are desynced from other players. What you see might differ from other players." + "desync_notice": "You are desynced from other players. What you see might differ from other players.", + "kicked_message": "You have been kicked from the game.", + "kicked_reason_admin": "An administrator removed you from the game.", + "kicked_reason_lobby_creator": "The lobby host removed you from the game.", + "kicked_reason_multi_tab": "You may be playing on another tab or browser window." }, "heads_up_message": { "choose_spawn": "Choose a starting location" diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index b96cd82575..5dae28fd51 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -105,15 +105,31 @@ export function joinLobby( ).then((r) => r.start()); } if (message.type === "error") { - showErrorModal( - message.error, - message.message, - lobbyConfig.gameID, - lobbyConfig.clientID, - true, - false, - "error_modal.connection_error", - ); + if (message.kickReason) { + const kickMessage = translateText("error_modal.kicked_message"); + const reasonKey = `error_modal.kicked_reason_${message.kickReason.replace("kick_", "")}`; + const reasonMessage = translateText(reasonKey); + + showErrorModal( + kickMessage, + `${reasonMessage}\nError Code: ${message.kickReason.toUpperCase()}`, + lobbyConfig.gameID, + lobbyConfig.clientID, + true, + false, + "error_modal.connection_error", + ); + } else { + showErrorModal( + message.error, + message.message, + lobbyConfig.gameID, + lobbyConfig.clientID, + true, + false, + "error_modal.connection_error", + ); + } } }; transport.connect(onconnect, onmessage); @@ -336,15 +352,31 @@ export class ClientGameRunner { ); } if (message.type === "error") { - showErrorModal( - message.error, - message.message, - this.lobby.gameID, - this.lobby.clientID, - true, - false, - "error_modal.connection_error", - ); + if (message.kickReason) { + const kickMessage = translateText("error_modal.kicked_message"); + const reasonKey = `error_modal.kicked_reason_${message.kickReason.replace("kick_", "")}`; + const reasonMessage = translateText(reasonKey); + + showErrorModal( + kickMessage, + `${reasonMessage}\nError Code: ${message.kickReason.toUpperCase()}`, + this.lobby.gameID, + this.lobby.clientID, + true, + false, + "error_modal.connection_error", + ); + } else { + showErrorModal( + message.error, + message.message, + this.lobby.gameID, + this.lobby.clientID, + true, + false, + "error_modal.connection_error", + ); + } } if (message.type === "turn") { if (!this.hasJoined) { diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 3bca9fe5ea..994c9a9cbf 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -465,6 +465,7 @@ export const ServerDesyncSchema = z.object({ export const ServerErrorSchema = z.object({ error: z.string(), + kickReason: z.enum(["kick_admin", "kick_multi_tab", "kick_lobby_creator"]).optional(), message: z.string().optional(), type: z.literal("error"), }); diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 9566ff3a7c..c0cd3aa1ac 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -175,7 +175,7 @@ export class GameServer { }); // Kick the existing client instead of the new one, because this was causing issues when // a client wanted to replay the game afterwards. - this.kickClient(conflicting.clientID); + this.kickClient(conflicting.clientID, "kick_multi_tab"); } } @@ -502,7 +502,8 @@ export class GameServer { return this.gameConfig.gameType === GameType.Public; } - public kickClient(clientID: ClientID): void { + + public kickClient(clientID: ClientID, kickReason?: "kick_admin" | "kick_multi_tab" | "kick_lobby_creator"): void { if (this.kickedClients.has(clientID)) { this.log.warn(`cannot kick client, already kicked`, { clientID, @@ -513,11 +514,13 @@ export class GameServer { if (client) { this.log.info("Kicking client from game", { clientID: client.clientID, + kickReason, persistentID: client.persistentID, }); client.ws.send( JSON.stringify({ - error: "Kicked from game (you may have been playing on another tab)", + error: "Kicked from game", + kickReason, type: "error", } satisfies ServerErrorMessage), ); diff --git a/src/server/Worker.ts b/src/server/Worker.ts index a0ef684c1e..1d44bdf1d0 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -294,7 +294,7 @@ export async function startWorker() { return; } - game.kickClient(clientID); + game.kickClient(clientID, "kick_admin"); res.status(200).send("Player kicked successfully"); }), ); diff --git a/src/server/worker/websocket/handler/message/PostJoinHandler.ts b/src/server/worker/websocket/handler/message/PostJoinHandler.ts index 25ba36c473..9ecf03ded1 100644 --- a/src/server/worker/websocket/handler/message/PostJoinHandler.ts +++ b/src/server/worker/websocket/handler/message/PostJoinHandler.ts @@ -77,7 +77,7 @@ export async function postJoinMessageHandler( target: clientMsg.intent.target, }); - gs.kickClient(clientMsg.intent.target); + gs.kickClient(clientMsg.intent.target, "kick_lobby_creator"); return; }