mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-04 05:50:35 +00:00
Feat/anonymize names (#4318)
**Add approved & assigned issue number here:** Resolves #4296 ## Description: Adds an "Anonymous players" option to private lobbies (host toggle, off by default). When it is on, the server sends each client anonymized usernames for everyone except themselves. The lobby creator and admins still see real names so they can moderate. Names are hidden on every player-facing surface: the game start message, lobby info, /api/game/:id, and the link preview. It is enforced server-side, so a client extension cannot read real names off the wire. Initially added as part of our overhaul of OpenFront masters, but this feature can very well be useful for content creators, and other tournament hosts. Anonymized names reuse the existing tribe word lists (no emoji), so they pass UsernameSchema, and they are seeded per user, so a player looks different to different users but stays consistent from the lobby into the game. The saved game record keeps real names (anonymization is a per-send transform, gameStartInfo is never mutated), so replays and stats are unaffected. Nothing changes for normal games. New option selection: <img width="990" height="918" alt="image" src="https://github.com/user-attachments/assets/31df0b0b-7757-4b2b-9bff-84310faee8d9" /> The host, when enabling the option, gets a little eye icon next to the players(including himself to enable/disable the anon names for himself, and/or other player) By default(the names everyone will see are random and unique): <img width="979" height="188" alt="image" src="https://github.com/user-attachments/assets/f0caa4a4-9f14-41d3-89c6-9a38e8c2e6f0" /> Toggling the eye ON for yourself (the host, or any given player, will allow them to see the real names of everyone, in the lobby and in game): <img width="969" height="138" alt="image" src="https://github.com/user-attachments/assets/89abf0e0-1433-43ea-9870-49d96ca46d30" /> ## 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:
+77
-11
@@ -25,7 +25,7 @@ import {
|
||||
StampedIntent,
|
||||
Turn,
|
||||
} from "../core/Schemas";
|
||||
import { createPartialGameRecord } from "../core/Util";
|
||||
import { anonymousUsername, createPartialGameRecord } from "../core/Util";
|
||||
import { archive, finalizeGameRecord } from "./Archive";
|
||||
import { Client } from "./Client";
|
||||
import { ClientMsgRateLimiter } from "./ClientMsgRateLimiter";
|
||||
@@ -136,6 +136,31 @@ 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.
|
||||
private viewerSeesAllNames(viewer: ClientID | undefined): boolean {
|
||||
return (
|
||||
viewer !== undefined &&
|
||||
(this.gameConfig.nameReveals?.includes(viewer) ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
// Same (viewer, target) -> same name in the lobby and in-game.
|
||||
private anonName(viewer: ClientID | undefined, target: ClientID): string {
|
||||
return anonymousUsername(target + (viewer ?? ""));
|
||||
}
|
||||
|
||||
// Whether `viewer` should see `target`'s real identity: when names aren't
|
||||
// anonymized, when looking at themselves, or when the host granted the
|
||||
// viewer reveal access (nameReveals).
|
||||
private seesReal(viewer: ClientID | undefined, target: ClientID): boolean {
|
||||
return (
|
||||
!this.gameConfig.anonymizeNames ||
|
||||
target === viewer ||
|
||||
this.viewerSeesAllNames(viewer)
|
||||
);
|
||||
}
|
||||
|
||||
public updateGameConfig(gameConfig: Partial<GameConfig>): void {
|
||||
if (gameConfig.gameMap !== undefined) {
|
||||
this.gameConfig.gameMap = gameConfig.gameMap;
|
||||
@@ -205,6 +230,12 @@ export class GameServer {
|
||||
if (gameConfig.waterNukes !== undefined) {
|
||||
this.gameConfig.waterNukes = gameConfig.waterNukes ?? undefined;
|
||||
}
|
||||
if (gameConfig.anonymizeNames !== undefined) {
|
||||
this.gameConfig.anonymizeNames = gameConfig.anonymizeNames;
|
||||
}
|
||||
if (gameConfig.nameReveals !== undefined) {
|
||||
this.gameConfig.nameReveals = gameConfig.nameReveals;
|
||||
}
|
||||
// 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".
|
||||
@@ -715,12 +746,13 @@ export class GameServer {
|
||||
}
|
||||
|
||||
private broadcastLobbyInfo() {
|
||||
const lobbyInfo = this.gameInfo();
|
||||
// Off: same payload for everyone (build once). On: per-recipient.
|
||||
const shared = this.gameConfig.anonymizeNames ? null : this.gameInfo();
|
||||
this.activeClients.forEach((c) => {
|
||||
if (c.ws.readyState === WebSocket.OPEN) {
|
||||
const msg = JSON.stringify({
|
||||
type: "lobby_info",
|
||||
lobby: lobbyInfo,
|
||||
lobby: shared ?? this.gameInfo(c.clientID),
|
||||
myClientID: c.clientID,
|
||||
} satisfies ServerLobbyInfoMessage);
|
||||
c.ws.send(msg);
|
||||
@@ -787,6 +819,30 @@ export class GameServer {
|
||||
this.intents.push(intent);
|
||||
}
|
||||
|
||||
// Per-viewer start info. The real gameStartInfo is untouched, so the
|
||||
// archived record keeps real identities. clanTag and friends feed the
|
||||
// deterministic team assignment (TeamAssignment.ts), so they are blanked
|
||||
// for every player here, identical on every client, never per-viewer, or
|
||||
// clients desync. Only the username of players this viewer can't see is
|
||||
// anonymized, and their cosmetics hidden, neither of which the simulation
|
||||
// reads.
|
||||
private startInfoFor(viewer: ClientID): GameStartInfo {
|
||||
if (!this.gameConfig.anonymizeNames) return this.wireGameStartInfo;
|
||||
return {
|
||||
...this.wireGameStartInfo,
|
||||
players: this.wireGameStartInfo.players.map((p) => {
|
||||
const real = this.seesReal(viewer, p.clientID);
|
||||
return {
|
||||
...p,
|
||||
username: real ? p.username : this.anonName(viewer, p.clientID),
|
||||
clanTag: null,
|
||||
friends: undefined,
|
||||
cosmetics: real ? p.cosmetics : undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private sendStartGameMsg(ws: WebSocket, lastTurn: number) {
|
||||
// Find which client this websocket belongs to
|
||||
const client = this.activeClients.find((c) => c.ws === ws);
|
||||
@@ -813,7 +869,7 @@ export class GameServer {
|
||||
JSON.stringify({
|
||||
type: "start",
|
||||
turns: this.turns.slice(lastTurn),
|
||||
gameStartInfo: this.wireGameStartInfo,
|
||||
gameStartInfo: this.startInfoFor(client.clientID),
|
||||
lobbyCreatedAt: this.createdAt,
|
||||
myClientID: client.clientID,
|
||||
} satisfies ServerStartGameMessage),
|
||||
@@ -954,17 +1010,27 @@ export class GameServer {
|
||||
return this._hasStarted || this._hasPrestarted;
|
||||
}
|
||||
|
||||
public gameInfo(): GameInfo {
|
||||
// Omitting viewer (e.g. the HTTP /api/game/:id and link-preview routes)
|
||||
// anonymizes all names when the option is on.
|
||||
public gameInfo(viewer?: ClientID): GameInfo {
|
||||
const friendsFor = this.buildFriendsLookup();
|
||||
const hideClanTags = this.gameConfig.disableClanTags ?? false;
|
||||
return {
|
||||
gameID: this.id,
|
||||
clients: this.activeClients.map((c) => ({
|
||||
username: c.username,
|
||||
clanTag: hideClanTags ? null : (c.clanTag ?? null),
|
||||
clientID: c.clientID,
|
||||
friends: friendsFor(c),
|
||||
})),
|
||||
clients: this.activeClients.map((c) =>
|
||||
this.seesReal(viewer, c.clientID)
|
||||
? {
|
||||
username: c.username,
|
||||
clanTag: hideClanTags ? null : (c.clanTag ?? null),
|
||||
clientID: c.clientID,
|
||||
friends: friendsFor(c),
|
||||
}
|
||||
: {
|
||||
username: this.anonName(viewer, c.clientID),
|
||||
clanTag: null,
|
||||
clientID: c.clientID,
|
||||
},
|
||||
),
|
||||
lobbyCreatorClientID: this.lobbyCreatorID,
|
||||
gameConfig: this.gameConfig,
|
||||
startsAt: this.startsAt,
|
||||
|
||||
Reference in New Issue
Block a user