fix:name reveal works by publicid during game config (#4415)

## 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._
This commit is contained in:
Zixer1
2026-06-25 17:49:20 -04:00
committed by evanpelle
parent 9c3fe97bf3
commit 0c4c7d7993
3 changed files with 42 additions and 4 deletions
+3
View File
@@ -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(),
+13 -4
View File
@@ -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".
+26
View File
@@ -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)", () => {