From 3811d3cd892031ce85e0dff2436a7dedd19393bd Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 24 May 2026 16:52:10 +0100 Subject: [PATCH] Hide clan tags in public FFA games to prevent teaming (#4000) ## Description: - Added optional `disableClanTags` to `GameConfig`. When set, the server strips clan tags from `gameInfo` (lobby broadcasts/HTTP) and `gameStartInfo` (start payload) before sending to clients. Archive keeps the originals. - Enabled `disableClanTags` for public FFA games (both the regular FFA playlist and special when randomized to FFA). No UI; clients still see their own clan tag via local input state. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/core/Schemas.ts | 1 + src/server/GameServer.ts | 18 ++++++++++++++++-- src/server/MapPlaylist.ts | 2 ++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index f14a39c76..3b18c9656 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -253,6 +253,7 @@ export const GameConfigSchema = z.object({ instantBuild: z.boolean(), disableNavMesh: z.boolean().optional(), disableAlliances: z.boolean().nullable().optional(), + disableClanTags: z.boolean().optional(), waterNukes: z.boolean().nullable().optional(), randomSpawn: z.boolean(), maxPlayers: z.number().optional(), diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 3ae18efab..dcc68c620 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -70,6 +70,10 @@ export class GameServer { // Note: This can be undefined if accessed before the game starts. private gameStartInfo!: GameStartInfo; + // Wire-only copy of gameStartInfo sent to clients. Identical to + // gameStartInfo unless disableClanTags is set, in which case clan tags + // are stripped from players. Archive uses the original gameStartInfo. + private wireGameStartInfo!: GameStartInfo; private log: Logger; @@ -753,6 +757,15 @@ export class GameServer { return; } this.gameStartInfo = result.data satisfies GameStartInfo; + this.wireGameStartInfo = this.gameConfig.disableClanTags + ? { + ...this.gameStartInfo, + players: this.gameStartInfo.players.map((p) => ({ + ...p, + clanTag: null, + })), + } + : this.gameStartInfo; this.endTurnIntervalID = setInterval( () => this.endTurn(), @@ -797,7 +810,7 @@ export class GameServer { JSON.stringify({ type: "start", turns: this.turns.slice(lastTurn), - gameStartInfo: this.gameStartInfo, + gameStartInfo: this.wireGameStartInfo, lobbyCreatedAt: this.createdAt, myClientID: client.clientID, } satisfies ServerStartGameMessage), @@ -959,11 +972,12 @@ export class GameServer { public gameInfo(): GameInfo { const friendsFor = this.buildFriendsLookup(); + const hideClanTags = this.gameConfig.disableClanTags ?? false; return { gameID: this.id, clients: this.activeClients.map((c) => ({ username: c.username, - clanTag: c.clanTag ?? null, + clanTag: hideClanTags ? null : (c.clanTag ?? null), clientID: c.clientID, friends: friendsFor(c), })), diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index b655f271a..a9341d365 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -246,6 +246,7 @@ export class MapPlaylist { bots: isCompact ? 100 : 400, spawnImmunityDuration: this.getSpawnImmunityDuration(playerTeams), disabledUnits: [], + disableClanTags: mode === GameMode.FFA ? true : undefined, } satisfies GameConfig; } @@ -457,6 +458,7 @@ export class MapPlaylist { this.getSpawnImmunityDuration(playerTeams, startingGold), disabledUnits, waterNukes: isWaterNukes ? true : undefined, + disableClanTags: mode === GameMode.FFA ? true : undefined, } satisfies GameConfig; }