From 0c4c7d7993c91bd058af2790c5b9f7b48fa8e90b Mon Sep 17 00:00:00 2001 From: Zixer1 <99333209+Zixer1@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:49:20 -0400 Subject: [PATCH] fix:name reveal works by publicid during game config (#4415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Adds nameRevealPublicIds to GameConfig — the same per-player reveal as nameReveals but keyed by stable account publicId instead of per-game clientID. Lets an automated host (the admin bot / OFM) grant casters and observers real-name vision at create_game, where it only knows publicIds and never learns a client's per-game clientID. viewerSeesAllNames resolves the viewer's clientID to its publicId via allClients and checks membership; nameReveals (clientID) is unchanged. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: zixer._ --- src/core/Schemas.ts | 3 +++ src/server/GameServer.ts | 17 +++++++++++++---- tests/server/AnonymizeNames.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index cfcc6849d..6c122050f 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -293,6 +293,9 @@ export const GameConfigSchema = z.object({ // While anonymizeNames is on, clientIDs the host has granted real-name // visibility to (e.g. casters / observers). Everyone else stays anonymized. nameReveals: z.string().array().optional(), + // Like nameReveals but keyed by stable account publicId (for automated hosts + // that only know publicIds at create_game); resolved to clientID at lookup. + nameRevealPublicIds: z.string().array().max(200).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 acaf6e4a0..6a63e1080 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -152,12 +152,18 @@ export class GameServer { : undefined; } - // anonymizeNames: only players the host granted (nameReveals) see real names. - // Nobody is exempt by default, not even the host, until he grants them. + // anonymizeNames: only players the host granted (nameReveals, or by account via + // nameRevealPublicIds) see real names. Nobody is exempt by default, not even the + // host, until he grants them. private viewerSeesAllNames(viewer: ClientID | undefined): boolean { + if (viewer === undefined) return false; + if (this.gameConfig.nameReveals?.includes(viewer) ?? false) return true; + // Resolve the per-game clientID to its stable account publicId so a host that + // only knows publicIds (the admin bot) can grant reveal access at create_game. + const publicId = this.allClients.get(viewer)?.publicId; return ( - viewer !== undefined && - (this.gameConfig.nameReveals?.includes(viewer) ?? false) + publicId !== undefined && + (this.gameConfig.nameRevealPublicIds?.includes(publicId) ?? false) ); } @@ -252,6 +258,9 @@ export class GameServer { if (gameConfig.nameReveals !== undefined) { this.gameConfig.nameReveals = gameConfig.nameReveals; } + if (gameConfig.nameRevealPublicIds !== undefined) { + this.gameConfig.nameRevealPublicIds = gameConfig.nameRevealPublicIds; + } // Unconditional on purpose: the host clears cheats by omitting hostCheats // (the full config it sends has hostCheats: undefined when the toggle is // off), so `undefined` here means "clear", not "leave unchanged". diff --git a/tests/server/AnonymizeNames.test.ts b/tests/server/AnonymizeNames.test.ts index e0d5649f0..0b109080d 100644 --- a/tests/server/AnonymizeNames.test.ts +++ b/tests/server/AnonymizeNames.test.ts @@ -43,6 +43,7 @@ function makeGame( anonymizeNames: boolean, disableClanTags = false, nameReveals: string[] = [], + nameRevealPublicIds: string[] = [], ) { const logger: any = { child: vi.fn().mockReturnThis(), @@ -59,6 +60,7 @@ function makeGame( anonymizeNames, disableClanTags, nameReveals, + nameRevealPublicIds, } as any, "creator-pid", ); @@ -122,6 +124,21 @@ describe("anonymizeNames: gameInfo (lobby / HTTP / preview)", () => { expect(REAL_NAMES).not.toContain(byId(info, "alice").username); }); + it("on: a viewer granted by account (nameRevealPublicIds) sees everyone's real names", () => { + // alice's clientID is "alice", her account publicId is "alice-pub" — the grant + // is keyed by publicId and resolved back to her clientID at lookup. + const info = makeGame(true, false, [], ["alice-pub"]).gameInfo("alice"); + for (const id of ["creator", "admin", "bob"]) { + expect(REAL_NAMES).toContain(byId(info, id).username); + } + }); + + it("on: a viewer NOT in nameRevealPublicIds still sees only themselves", () => { + const info = makeGame(true, false, [], ["alice-pub"]).gameInfo("bob"); + expect(byId(info, "bob").username).toBe("BobReal"); // self + expect(REAL_NAMES).not.toContain(byId(info, "alice").username); + }); + it("on: no viewer (HTTP / preview) anonymizes everyone", () => { const info = makeGame(true).gameInfo(); for (const id of ["creator", "admin", "alice", "bob"]) { @@ -157,6 +174,15 @@ describe("anonymizeNames: config updates propagate", () => { game.updateGameConfig({ nameReveals: [] }); expect(byId(game.gameInfo("alice"), "bob").username).not.toBe("BobReal"); // revoked }); + + it("granting nameRevealPublicIds at runtime reveals by account; clearing revokes", () => { + const game = makeGame(true); + expect(byId(game.gameInfo("alice"), "bob").username).not.toBe("BobReal"); // not granted + game.updateGameConfig({ nameRevealPublicIds: ["alice-pub"] }); + expect(byId(game.gameInfo("alice"), "bob").username).toBe("BobReal"); // granted by account + game.updateGameConfig({ nameRevealPublicIds: [] }); + expect(byId(game.gameInfo("alice"), "bob").username).not.toBe("BobReal"); // revoked + }); }); describe("anonymizeNames: startInfoFor (in-game start payload)", () => {