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;