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
This commit is contained in:
Evan
2026-05-24 16:52:10 +01:00
committed by GitHub
parent b4a14f9b9d
commit 3811d3cd89
3 changed files with 19 additions and 2 deletions
+1
View File
@@ -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(),
+16 -2
View File
@@ -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),
})),
+2
View File
@@ -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;
}