Skip to content

Conversation

floriankilian
Copy link
Contributor

Description:

Added translated kick reasons so players see specific context instead of generic "Kicked from game" errors.

Fixes #1654

What's new:

  • Translated kick messages for admin, lobby host, and multi-tab kicks
  • Error codes for debugging (KICK_ADMIN, KICK_LOBBY_CREATOR, KICK_MULTI_TAB)

Example:

image

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced
  • I have read and accepted the CLA agreement (only required once).

- 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
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

Walkthrough

Introduces kick-reason support end-to-end: schema adds optional kickReason, server kickClient API accepts an optional reason and passes it on at call sites (admin, lobby-creator, multi-tab). Client handles kickReason to show localized messages. English translations add kicked message and reason-specific strings.

Changes

Cohort / File(s) Summary
Translations
resources/lang/en.json
Added error_modal keys: kicked_message, kicked_reason_admin, kicked_reason_lobby_creator, kicked_reason_multi_tab.
Client error handling
src/client/ClientGameRunner.ts
Error-modal logic reads message.kickReason; builds localized title/body using translation keys; falls back to previous behavior if absent.
Schemas and types
src/core/Schemas.ts
ServerErrorSchema extended with optional kickReason enum: "kick_admin"
Server kick API
src/server/GameServer.ts
kickClient signature updated to accept optional kickReason; when multi-tab conflict detected, passes "kick_multi_tab"; error payload includes kickReason.
Admin endpoint
src/server/Worker.ts
Admin kick now calls kickClient with "kick_admin".
Websocket post-join handler
src/server/worker/websocket/handler/message/PostJoinHandler.ts
Lobby-creator kick now calls gs.kickClient with "kick_lobby_creator".

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Client
  participant Server
  participant GameServer

  User->>Client: Attempt join / in-game
  Client->>Server: WebSocket connect / actions
  alt Multi-tab detected
    Server->>GameServer: kickClient(existingClient, "kick_multi_tab")
  else Admin kicks
    Server->>GameServer: kickClient(targetClient, "kick_admin")
  else Lobby creator kicks
    Server->>GameServer: kickClient(targetClient, "kick_lobby_creator")
  end
  GameServer-->>Client: {"type":"error","error":"Kicked from game","kickReason":...}
  Client->>Client: Translate kicked_message + kicked_reason_* and show modal
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Assessment against linked issues

Objective Addressed Explanation
Add optional reason parameter to kickClient() [#1654]
Send appropriate error message based on kick reason [#1654]
Update all kick call sites to specify kick reason [#1654]
Add messages to translation system [#1654]

Possibly related issues

Possibly related PRs

Suggested labels

Translation, Feature - Backend, Feature - Frontend

Suggested reviewers

  • evanpelle
  • scottanderson

Poem

A tap on the wire, a whisper: “You’re out.”
Reasons ride shotgun, leaving little doubt.
Admin, host, or tabs that multiply—
The client translates, with a gentle sigh.
Kicks now speak plainly—goodbye, not a shout.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
resources/lang/en.json (1)

548-553: Kick translations LGTM; add a generic fallback key

The new keys read well and cover all current reasons. For forward-compatibility (mixed client/server versions or future reasons), add a generic fallback message so the UI never shows a raw translation key.

Apply this diff:

-    "kicked_reason_multi_tab": "You may be playing on another tab or browser window."
+    "kicked_reason_multi_tab": "You may be playing on another tab or browser window.",
+    "kicked_reason_default": "You were removed from the game by the server."
src/server/Worker.ts (1)

297-299: Prefer a structured JSON response and echo the reason

Minor API polish: return JSON consistently and include the reason used. Easier to parse by tools and consistent with other endpoints.

-      game.kickClient(clientID, "kick_admin");
-      res.status(200).send("Player kicked successfully");
+      const reason = "kick_admin" as const;
+      game.kickClient(clientID, reason);
+      res.status(200).json({ success: true, clientID, reason });
src/client/ClientGameRunner.ts (1)

108-121: DRY: extract a small helper to show kick error modals

Both handlers now share identical logic. Extract a helper to keep things tidy and reduce future drift.

Example helper (outside the selected ranges):

function showKickError(gameID: GameID, clientID: ClientID, kickReason: string) {
  const reasonKey = `error_modal.kicked_reason_${kickReason.replace("kick_", "")}`;
  let reasonMessage = translateText(reasonKey);
  if (reasonMessage === reasonKey) {
    reasonMessage = translateText("error_modal.kicked_reason_multi_tab");
  }
  showErrorModal(
    reasonMessage,
    `Error Code: ${kickReason.toUpperCase()}`,
    gameID,
    clientID,
    true,
    false,
    "error_modal.kicked_message",
  );
}

Then replace both in-place blocks with:

showKickError(lobbyConfig.gameID, lobbyConfig.clientID, message.kickReason);

I can push the refactor if you want.

Also applies to: 355-369

src/server/GameServer.ts (1)

517-524: Add more signal in the close frame (optional)

Not critical, but consider using a policy violation close code and include the reason as the close message for easier client-side triage with no prior “error” frame.

-      client.ws.send(
+      client.ws.send(
         JSON.stringify({
           error: "Kicked from game",
           kickReason,
           type: "error",
         } satisfies ServerErrorMessage),
       );
-      client.ws.close(1000, "Kicked from game");
+      // 1008 = Policy Violation (standard). The message is free-form.
+      client.ws.close(1008, kickReason ?? "Kicked from game");

Also applies to: 527-527

src/core/Schemas.ts (1)

468-471: Nit: Document the new field with allowed values and usage

A short JSDoc helps future readers understand how this couples with i18n and client UX.

Example:

/**
 * Optional machine-readable reason for a server kick.
 * Used by the client to show a localized message.
 * Allowed values: "kick_admin" | "kick_multi_tab" | "kick_lobby_creator".
 * When absent, the client should show a generic kicked message.
 */
kickReason: KickReasonSchema.optional(),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2ed0cef and e1b1beb.

📒 Files selected for processing (6)
  • resources/lang/en.json (1 hunks)
  • src/client/ClientGameRunner.ts (2 hunks)
  • src/core/Schemas.ts (1 hunks)
  • src/server/GameServer.ts (3 hunks)
  • src/server/Worker.ts (1 hunks)
  • src/server/worker/websocket/handler/message/PostJoinHandler.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/server/Worker.ts (1)
src/server/GameManager.ts (1)
  • game (18-20)
src/client/ClientGameRunner.ts (2)
src/client/LangSelector.ts (1)
  • translateText (246-266)
src/client/Utils.ts (1)
  • translateText (82-137)
src/server/GameServer.ts (4)
src/core/game/GameView.ts (1)
  • clientID (260-262)
src/core/game/PlayerImpl.ts (1)
  • clientID (194-196)
src/core/game/TerraNulliusImpl.ts (1)
  • clientID (9-11)
src/core/Schemas.ts (1)
  • ClientID (22-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
src/server/worker/websocket/handler/message/PostJoinHandler.ts (1)

80-80: LGTM: correct reason supplied to kick

Calling gs.kickClient(..., "kick_lobby_creator") ensures the client sees the right, localized reason. This aligns with the schema and client handling.

src/server/GameServer.ts (1)

178-179: LGTM: conflict kick includes explicit reason

Forwarding "kick_multi_tab" here is correct and matches the client’s translation keys.

src/core/Schemas.ts (1)

468-471: No i18n key mismatch — client strips the "kick_" prefix before building the key

Verified: server sends "kick_*" codes, client does message.kickReason.replace("kick_", "") when building error_modal.kicked_reason_*, and resources/lang/en.json contains kicked_reason_* entries — alignment confirmed. No change required.

Relevant locations:

  • src/core/Schemas.ts — kickReason enum includes "kick_admin", "kick_multi_tab", "kick_lobby_creator" (around line 468)
  • src/server/GameServer.ts — kickClient sends the kickReason in the error message (around lines 506–526)
  • src/client/ClientGameRunner.ts — builds reasonKey with .replace("kick_", "") and calls translateText (lines 108–116 and 355–363)
  • resources/lang/en.json — "kicked_reason_admin", "kicked_reason_lobby_creator", "kicked_reason_multi_tab" (lines 550–552)

Comment on lines +108 to +121
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",
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix import path to avoid duplicate module instances of Utils

The file imports Utils twice with two different relative paths (“./Utils” and “../client/Utils”). This can cause the bundler/loader to create separate module instances, breaking shared caches in translateText and adding bytes.

Update the import at the top of this file:

// before
import { translateText } from "../client/Utils";

// after
import { translateText } from "./Utils";
🤖 Prompt for AI Agents
In src/client/ClientGameRunner.ts around lines 108 to 121, the file currently
imports translateText from "../client/Utils" which creates a duplicate module
instance alongside "./Utils"; update the import to use "./Utils" (replace the
"../client/Utils" import) so both imports resolve to the same module instance,
remove the duplicate import if present, and run a quick build to confirm shared
caches (e.g., translateText) behave correctly.

🛠️ Refactor suggestion

⚠️ Potential issue

Use the “kicked” heading and add a robust translation fallback

Set the modal heading to the translated “kicked” title, and fall back to a safe reason message if the specific translation key is missing (e.g., mixed versions). Also remove the now-unused kickMessage variable.

Apply this diff:

-      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",
-        );
+      if (message.kickReason) {
+        const reasonKey = `error_modal.kicked_reason_${message.kickReason.replace("kick_", "")}`;
+        let reasonMessage = translateText(reasonKey);
+        if (reasonMessage === reasonKey) {
+          // Fallback if key missing (e.g., mixed versions)
+          reasonMessage = translateText("error_modal.kicked_reason_multi_tab");
+        }
+        showErrorModal(
+          reasonMessage,
+          `Error Code: ${message.kickReason.toUpperCase()}`,
+          lobbyConfig.gameID,
+          lobbyConfig.clientID,
+          true,
+          false,
+          "error_modal.kicked_message",
+        );
       } else {
         showErrorModal(
           message.error,
           message.message,
           lobbyConfig.gameID,
           lobbyConfig.clientID,
           true,
           false,
           "error_modal.connection_error",
         );
       }

Also applies to: 123-132

Comment on lines +355 to +369
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Duplicate logic: apply same heading and fallback in the in-game handler

Mirror the fixes from join flow inside the in-game WebSocket handler to keep UX consistent.

-      if (message.type === "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 {
+      if (message.type === "error") {
+        if (message.kickReason) {
+          const reasonKey = `error_modal.kicked_reason_${message.kickReason.replace("kick_", "")}`;
+          let reasonMessage = translateText(reasonKey);
+          if (reasonMessage === reasonKey) {
+            reasonMessage = translateText("error_modal.kicked_reason_multi_tab");
+          }
+          showErrorModal(
+            reasonMessage,
+            `Error Code: ${message.kickReason.toUpperCase()}`,
+            this.lobby.gameID,
+            this.lobby.clientID,
+            true,
+            false,
+            "error_modal.kicked_message",
+          );
+        } else {
           showErrorModal(
             message.error,
             message.message,
             this.lobby.gameID,
             this.lobby.clientID,
             true,
             false,
             "error_modal.connection_error",
           );
         }
       }

Also applies to: 370-379

🤖 Prompt for AI Agents
In src/client/ClientGameRunner.ts around lines 355 to 369 (and similarly 370 to
379), the in-game WebSocket handler duplicates old logic for kicked messages
instead of mirroring the join-flow fixes; update the block so the heading and
reason fallback behavior match the join flow: compute kickMessage via
translateText("error_modal.kicked_message"), derive reasonKey by stripping
"kick_" and attempt translateText(reasonKey) with a fallback to
translateText("error_modal.kicked_reason_unknown") (or the same fallback used in
join flow), and call showErrorModal with the same parameters and formatted body
(reasonMessage plus "Error Code: ..." in uppercase) so UX is consistent across
handlers.

@@ -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(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract KickReason into a shared schema and type for reuse

Inline string unions tend to get duplicated across server and client. Centralizing the values improves consistency and reduces drift.

  • Add these definitions near other shared schemas:
export const KickReasonValues = ["kick_admin", "kick_multi_tab", "kick_lobby_creator"] as const;
export type KickReason = typeof KickReasonValues[number];
export const KickReasonSchema = z.enum(KickReasonValues);
  • Then update the field here:
-  kickReason: z.enum(["kick_admin", "kick_multi_tab", "kick_lobby_creator"] as const).optional(),
+  kickReason: KickReasonSchema.optional(),

This also lets you import KickReason as a type for signatures like kickClient(reason?: KickReason).

🤖 Prompt for AI Agents
In src/core/Schemas.ts around line 468 the kickReason field uses an inline
z.enum which should be centralized; add three shared exports near other shared
schemas: define a const KickReasonValues as the readonly array of
["kick_admin","kick_multi_tab","kick_lobby_creator"], export a type KickReason =
typeof KickReasonValues[number], and export KickReasonSchema =
z.enum(KickReasonValues); then replace the inline z.enum at line 468 with
KickReasonSchema.optional() (and update any imports/usages to use the KickReason
type where needed).

⚠️ Potential issue

Add as const to z.enum literal array to keep strict types and avoid TS error

Without as const, TypeScript often widens the array to string[], which is not assignable to the tuple type [string, ...string[]] expected by z.enum. This can break type inference or fail compilation.

Apply this minimal fix:

-  kickReason: z.enum(["kick_admin", "kick_multi_tab", "kick_lobby_creator"]).optional(),
+  kickReason: z.enum(["kick_admin", "kick_multi_tab", "kick_lobby_creator"] as const).optional(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
kickReason: z.enum(["kick_admin", "kick_multi_tab", "kick_lobby_creator"]).optional(),
kickReason: z.enum(["kick_admin", "kick_multi_tab", "kick_lobby_creator"] as const).optional(),
🤖 Prompt for AI Agents
In src/core/Schemas.ts around line 468, the z.enum call uses a plain string
array which TypeScript widens to string[] and can cause type errors; update the
enum literal to use a readonly tuple by appending "as const" to the array (e.g.
z.enum(["kick_admin", "kick_multi_tab", "kick_lobby_creator"] as
const).optional()) so the types remain strict and satisfy z.enum's expected
tuple type.

Comment on lines +505 to 507

public kickClient(clientID: ClientID, kickReason?: "kick_admin" | "kick_multi_tab" | "kick_lobby_creator"): void {
if (this.kickedClients.has(clientID)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid duplicating the KickReason union; import a shared type

The inline union risks drift if a new reason is added elsewhere. Prefer a single source of truth (Schemas).

Option A (preferred, if exported):

-  public kickClient(clientID: ClientID, kickReason?: "kick_admin" | "kick_multi_tab" | "kick_lobby_creator"): void {
+  public kickClient(clientID: ClientID, kickReason?: KickReason): void {

And add at top:

import type { KickReason } from "../core/Schemas";

Option B (if KickReason type isn’t exported yet): export it from Schemas and use it here. I can open a follow-up PR to centralize this type.

🤖 Prompt for AI Agents
In src/server/GameServer.ts around lines 505 to 507, the method kickClient uses
an inline union type for the kick reason which duplicates the KickReason
definition; replace the inline union with the shared KickReason type from the
Schemas module by importing it at the top of the file (import type { KickReason
} from "../core/Schemas") and update the method signature to use that type; if
KickReason is not exported from Schemas, export it there first (or ask to open a
follow-up PR) then import and use it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Development
Development

Successfully merging this pull request may close these issues.

Add handling of kick reasons
2 participants