Skip to content

Commit ee8b89e

Browse files
committed
fix(anon): let pinned teammates see each other
With anonymizeNames on, seesReal was true only for yourself or a viewer granted reveal access, so in a Team game you could not identify your own teammate. A team that cannot coordinate is not a team. A player now also sees the real name of anyone on their pinned team. Only pinned teams: those are assigned server-side (matchmakingTeams), so the server knows them here. A team game that groups by clanTag/friends is resolved on the clients and the server has no answer to give, so nothing is revealed there. Safe for the same reason 'target === viewer' already is: this widens only username and cosmetics, neither of which the simulation reads (Player.hash excludes names). clanTag and friends DO feed assignTeams and are still blanked identically for every viewer.
1 parent 49d52b0 commit ee8b89e

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

src/server/GameServer.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,37 @@ export class GameServer {
335335
// Whether `viewer` should see `target`'s real identity: when names aren't
336336
// anonymized, when looking at themselves, or when the host granted the
337337
// viewer reveal access (nameReveals).
338+
// Teammates in a matchmade game. Anonymizing a player from their own team
339+
// makes the team unplayable — you cannot coordinate with someone you cannot
340+
// identify — so a pinned team sees itself, exactly as a player already sees
341+
// themselves. Only PINNED teams: those are assigned server-side, so the server
342+
// knows them here. A team game that groups by clanTag/friends is resolved on
343+
// the clients, and the server has no answer to give.
344+
//
345+
// Safe for the same reason `target === viewer` is: this only widens `username`
346+
// and `cosmetics`, neither of which the simulation reads (Player.hash excludes
347+
// names). The fields that DO feed assignTeams — clanTag and friends — are
348+
// blanked identically for every viewer and are untouched here.
349+
private sameMatchmadeTeam(
350+
viewer: ClientID | undefined,
351+
target: ClientID,
352+
): boolean {
353+
if (viewer === undefined) return false;
354+
const viewerClient = this.allClients.get(viewer);
355+
const targetClient = this.allClients.get(target);
356+
if (viewerClient === undefined || targetClient === undefined) return false;
357+
const viewerTeam = this.matchmakingTeamIndex(viewerClient);
358+
return (
359+
viewerTeam !== undefined &&
360+
viewerTeam === this.matchmakingTeamIndex(targetClient)
361+
);
362+
}
363+
338364
private seesReal(viewer: ClientID | undefined, target: ClientID): boolean {
339365
return (
340366
!this.gameConfig.anonymizeNames ||
341367
target === viewer ||
368+
this.sameMatchmadeTeam(viewer, target) ||
342369
this.viewerSeesAllNames(viewer)
343370
);
344371
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { GameType } from "../../src/core/game/Game";
2+
import { Client } from "../../src/server/Client";
3+
import { GameServer } from "../../src/server/GameServer";
4+
5+
function makeMockWs() {
6+
return {
7+
on: () => {},
8+
removeAllListeners: () => {},
9+
send: vi.fn(),
10+
close: vi.fn(),
11+
readyState: 1,
12+
};
13+
}
14+
15+
function makeClient(clientID: string, username: string, publicId: string) {
16+
return new Client(
17+
clientID,
18+
`${clientID}-pid`,
19+
null,
20+
null,
21+
undefined,
22+
"127.0.0.1",
23+
username,
24+
null,
25+
makeMockWs() as any,
26+
undefined,
27+
publicId,
28+
[],
29+
);
30+
}
31+
32+
// alice+bob are one pinned team, carol+dave the other.
33+
function makeGame(matchmakingTeams?: string[][]) {
34+
const logger: any = {
35+
child: vi.fn().mockReturnThis(),
36+
info: vi.fn(),
37+
warn: vi.fn(),
38+
error: vi.fn(),
39+
};
40+
const game = new GameServer(
41+
"g1",
42+
logger,
43+
Date.now(),
44+
{ gameType: GameType.Private, anonymizeNames: true } as any,
45+
"creator-pid",
46+
undefined,
47+
undefined,
48+
matchmakingTeams,
49+
);
50+
[
51+
makeClient("alice", "AliceReal", "alice-pub"),
52+
makeClient("bob", "BobReal", "bob-pub"),
53+
makeClient("carol", "CarolReal", "carol-pub"),
54+
makeClient("dave", "DaveReal", "dave-pub"),
55+
].forEach((c) => game.joinClient(c));
56+
return game;
57+
}
58+
59+
const TEAMS = [
60+
["alice-pub", "bob-pub"],
61+
["carol-pub", "dave-pub"],
62+
];
63+
const REAL = ["AliceReal", "BobReal", "CarolReal", "DaveReal"];
64+
const byId = (info: any, id: string) =>
65+
info.clients.find((c: any) => c.clientID === id);
66+
67+
describe("anonymizeNames: pinned teammates see each other", () => {
68+
beforeEach(() => vi.useFakeTimers());
69+
afterEach(() => {
70+
vi.clearAllTimers();
71+
vi.useRealTimers();
72+
});
73+
74+
it("shows a teammate's real name", () => {
75+
// Anonymizing a player from their own team makes the team unplayable.
76+
const info = makeGame(TEAMS).gameInfo("alice");
77+
expect(byId(info, "bob").username).toBe("BobReal");
78+
});
79+
80+
it("still hides the other team", () => {
81+
const info = makeGame(TEAMS).gameInfo("alice");
82+
for (const id of ["carol", "dave"]) {
83+
expect(REAL).not.toContain(byId(info, id).username);
84+
}
85+
});
86+
87+
it("hides everyone when the game is not matchmade", () => {
88+
// Without pins the server has no team to compare — teams are resolved on the
89+
// clients from clanTag/friends — so nothing is revealed.
90+
const info = makeGame(undefined).gameInfo("alice");
91+
expect(byId(info, "alice").username).toBe("AliceReal"); // self, as before
92+
for (const id of ["bob", "carol", "dave"]) {
93+
expect(REAL).not.toContain(byId(info, id).username);
94+
}
95+
});
96+
97+
it("reveals nothing to a player who is in no pinned team", () => {
98+
const info = makeGame([["carol-pub", "dave-pub"]]).gameInfo("alice");
99+
for (const id of ["bob", "carol", "dave"]) {
100+
expect(REAL).not.toContain(byId(info, id).username);
101+
}
102+
});
103+
104+
it("is symmetric — the teammate sees back", () => {
105+
const info = makeGame(TEAMS).gameInfo("bob");
106+
expect(byId(info, "alice").username).toBe("AliceReal");
107+
});
108+
109+
it("keeps the team-assignment inputs blank for everyone", () => {
110+
// clanTag and friends feed assignTeams, so revealing them per viewer would
111+
// desync. Only username/cosmetics widen.
112+
const info = makeGame(TEAMS).gameInfo("alice");
113+
expect(byId(info, "bob").clanTag ?? null).toBeNull();
114+
});
115+
});

0 commit comments

Comments
 (0)