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
This commit is contained in:
floriankilian
2025-08-14 22:15:32 +02:00
parent 2ed0cef65c
commit e1b1beb24d
6 changed files with 64 additions and 24 deletions
+5 -1
View File
@@ -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"
+50 -18
View File
@@ -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) {
+1
View File
@@ -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"),
});
+6 -3
View File
@@ -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),
);
+1 -1
View File
@@ -294,7 +294,7 @@ export async function startWorker() {
return;
}
game.kickClient(clientID);
game.kickClient(clientID, "kick_admin");
res.status(200).send("Player kicked successfully");
}),
);
@@ -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;
}