From 37d358fbdc68642509cc8788d5ce0cbe0e2c19e9 Mon Sep 17 00:00:00 2001 From: abdallahbahrawi1 Date: Fri, 19 Dec 2025 13:33:43 +0200 Subject: [PATCH] Add player limit feature for private lobbies --- resources/lang/debug.json | 7 ++- resources/lang/en.json | 7 ++- src/client/ClientGameRunner.ts | 30 ++++++++++ src/client/HostLobbyModal.ts | 91 ++++++++++++++++++++++++++++- src/client/JoinPrivateLobbyModal.ts | 8 ++- src/core/Schemas.ts | 2 +- src/server/GameServer.ts | 10 ++++ 7 files changed, 148 insertions(+), 7 deletions(-) diff --git a/resources/lang/debug.json b/resources/lang/debug.json index 924720ce3..bb8788f69 100644 --- a/resources/lang/debug.json +++ b/resources/lang/debug.json @@ -127,7 +127,8 @@ "checking": "private_lobby.checking", "not_found": "private_lobby.not_found", "error": "private_lobby.error", - "joined_waiting": "private_lobby.joined_waiting" + "joined_waiting": "private_lobby.joined_waiting", + "lobby_full": "private_lobby.lobby_full" }, "public_lobby": { "join": "public_lobby.join", @@ -154,7 +155,9 @@ "player": "host_modal.player", "players": "host_modal.players", "waiting": "host_modal.waiting", - "start": "host_modal.start" + "start": "host_modal.start", + "player_limit": "host_modal.player_limit", + "player_limit_warning": "host_modal.player_limit_warning" }, "game_starting_modal": { "title": "game_starting_modal.title", diff --git a/resources/lang/en.json b/resources/lang/en.json index 7031576ac..2c1bb4826 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -239,7 +239,8 @@ "not_found": "Lobby not found. Please check the ID and try again.", "error": "An error occurred. Please try again or contact support.", "joined_waiting": "Joined successfully! Waiting for game to start...", - "version_mismatch": "This game was created with a different version. Cannot join." + "version_mismatch": "This game was created with a different version. Cannot join.", + "lobby_full": "Lobby is full (limit: {limit})." }, "public_lobby": { "join": "Join next Game", @@ -292,7 +293,9 @@ "assigned_teams": "Assigned Teams", "empty_teams": "Empty Teams", "empty_team": "Empty", - "remove_player": "Remove {username}" + "remove_player": "Remove {username}", + "player_limit": "Player limit", + "player_limit_warning": "Limit is below current player count; new players can't join." }, "team_colors": { "red": "Red", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 5089b6be2..4224c3390 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -125,6 +125,36 @@ export function joinLobby( } if (message.type === "error") { if (message.error === "full-lobby") { + // Parse the message to get the player limit + let limitInfo = ""; + try { + if (message.message) { + const data = JSON.parse(message.message); + if (data.maxPlayers) { + limitInfo = translateText("private_lobby.lobby_full", { + limit: data.maxPlayers, + }); + } + } + } catch { + limitInfo = translateText("private_lobby.lobby_full", { + limit: "?", + }); + } + + // Show the full lobby message + if (limitInfo) { + showErrorModal( + message.error, + limitInfo, + lobbyConfig.gameID, + lobbyConfig.clientID, + true, + false, + "error_modal.connection_error", + ); + } + document.dispatchEvent( new CustomEvent("leave-lobby", { detail: { lobby: lobbyConfig.gameID }, diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 5d94324e9..2e8c9a906 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -59,6 +59,11 @@ export class HostLobbyModal extends LitElement { @state() private lobbyCreatorClientID: string = ""; @state() private lobbyIdVisible: boolean = true; @state() private nationCount: number = 0; + @state() private playerLimit: number | null = null; // null = unlimited + @state() private playerLimitWarning: boolean = false; + + private static readonly MIN_PLAYER_LIMIT = 2; + private static readonly MAX_PLAYER_LIMIT = 1000; private playersInterval: NodeJS.Timeout | null = null; // Add a new timer for debouncing bot changes @@ -525,6 +530,56 @@ export class HostLobbyModal extends LitElement { ${translateText("host_modal.max_timer")} + + + + ${ + this.playerLimitWarning + ? html`
+ ${translateText("host_modal.player_limit_warning")} +
` + : "" + }
@@ -549,7 +604,7 @@ export class HostLobbyModal extends LitElement {
- ${this.clients.length} + ${this.clients.length}${this.playerLimit !== null ? `/${this.playerLimit}` : ""} ${ this.clients.length === 1 ? translateText("host_modal.player") @@ -735,6 +790,38 @@ export class HostLobbyModal extends LitElement { this.putGameConfig(); } + private handlePlayerLimitKeyDown(e: KeyboardEvent) { + if (["-", "+", "e"].includes(e.key)) { + e.preventDefault(); + } + } + + private handlePlayerLimitChange(e: Event) { + const input = e.target as HTMLInputElement; + // Remove invalid characters + input.value = input.value.replace(/[e+-]/gi, ""); + + const value = parseInt(input.value); + + // Validate: must be a number between MIN and MAX + if ( + isNaN(value) || + value < HostLobbyModal.MIN_PLAYER_LIMIT || + value > HostLobbyModal.MAX_PLAYER_LIMIT + ) { + return; + } + + this.playerLimit = value; + this.updatePlayerLimitWarning(); + this.putGameConfig(); + } + + private updatePlayerLimitWarning() { + this.playerLimitWarning = + this.playerLimit !== null && this.clients.length > this.playerLimit; + } + private async handleDisableNPCsChange(e: Event) { this.disableNPCs = Boolean((e.target as HTMLInputElement).checked); console.log(`updating disable npcs to ${this.disableNPCs}`); @@ -786,6 +873,7 @@ export class HostLobbyModal extends LitElement { }), maxTimerValue: this.maxTimer === true ? this.maxTimerValue : undefined, + maxPlayers: this.playerLimit ?? undefined, } satisfies Partial), }, ); @@ -854,6 +942,7 @@ export class HostLobbyModal extends LitElement { console.log(`got game info response: ${JSON.stringify(data)}`); this.clients = data.clients ?? []; + this.updatePlayerLimitWarning(); }); } diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts index 300ea08a9..fc90aa23e 100644 --- a/src/client/JoinPrivateLobbyModal.ts +++ b/src/client/JoinPrivateLobbyModal.ts @@ -18,6 +18,7 @@ export class JoinPrivateLobbyModal extends LitElement { @state() private message: string = ""; @state() private hasJoined = false; @state() private players: string[] = []; + @state() private maxPlayers: number | null = null; private playersInterval: NodeJS.Timeout | null = null; @@ -75,7 +76,9 @@ export class JoinPrivateLobbyModal extends LitElement { ${this.hasJoined && this.players.length > 0 ? html`
- ${this.players.length} + ${this.players.length}${this.maxPlayers !== null + ? `/${this.maxPlayers}` + : ""} ${this.players.length === 1 ? translateText("private_lobby.player") : translateText("private_lobby.players")} @@ -127,6 +130,8 @@ export class JoinPrivateLobbyModal extends LitElement { this.close(); this.hasJoined = false; this.message = ""; + this.maxPlayers = null; + this.players = []; this.dispatchEvent( new CustomEvent("leave-lobby", { detail: { lobby: this.lobbyIdInput.value }, @@ -315,6 +320,7 @@ export class JoinPrivateLobbyModal extends LitElement { .then((response) => response.json()) .then((data: GameInfo) => { this.players = data.clients?.map((p) => p.username) ?? []; + this.maxPlayers = data.gameConfig?.maxPlayers ?? null; }) .catch((error) => { console.error("Error polling players:", error); diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 5ba0b04a5..db5fa393f 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -170,7 +170,7 @@ export const GameConfigSchema = z.object({ infiniteTroops: z.boolean(), instantBuild: z.boolean(), randomSpawn: z.boolean(), - maxPlayers: z.number().optional(), + maxPlayers: z.number().int().min(2).max(1000).optional(), maxTimerValue: z.number().int().min(1).max(120).optional(), disabledUnits: z.enum(UnitType).array().optional(), playerTeams: TeamCountConfigSchema.optional(), diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index fa3ddf50f..3f5754d9f 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -128,6 +128,10 @@ export class GameServer { if (gameConfig.playerTeams !== undefined) { this.gameConfig.playerTeams = gameConfig.playerTeams; } + + if (gameConfig.maxPlayers !== undefined) { + this.gameConfig.maxPlayers = gameConfig.maxPlayers; + } } public joinClient(client: Client) { @@ -152,12 +156,18 @@ export class GameServer { ) { this.log.warn(`cannot add client, game full`, { clientID: client.clientID, + currentPlayers: this.activeClients.length, + maxPlayers: this.gameConfig.maxPlayers, }); client.ws.send( JSON.stringify({ type: "error", error: "full-lobby", + message: JSON.stringify({ + currentPlayers: this.activeClients.length, + maxPlayers: this.gameConfig.maxPlayers, + }), } satisfies ServerErrorMessage), ); return;