Skip to content

Commit 7d8c1c2

Browse files
Fix: prevent desync after clan team assignment for profane username (#2511)
## Description: Clan tag was removed when overwriting profane username. The local player still sees the name they put in though, and are assigned to a team based on the clan tag. Other player's browsers don't assign them to their team because the clan tag isn't visible to them. **Fixes:** - GameRunner.ts > username.ts: fetch clan tag before potentially overwriting bad username. Prepend non-profane clan tag back to the name string afterwards. - Util.ts: added getClanTagOriginalCase; we can't use getClanTag in censorNameWithClanTag because it returns all caps and we needed to retain the orginal capitalization. Explained in code comment. - Game.ts: no changes. By keeping the getClanTag in PlayerInfo contructor, TeamAssignment still gets clan param to correctly assign clan teams, other players get to see the clan tag of the "BeNicer" player, and GameServer archivegame() and LocalServer endGame() can still do getClanTag to have the same data at the end of the game, and test files keep working. **Screencap after fix:** https://github.com/user-attachments/assets/564c0ffd-577e-4653-ba33-155d2135a9f0 ## 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: tryout33
1 parent 1255f19 commit 7d8c1c2

File tree

3 files changed

+72
-15
lines changed

3 files changed

+72
-15
lines changed

src/core/GameRunner.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
3030
import { PseudoRandom } from "./PseudoRandom";
3131
import { ClientID, GameStartInfo, Turn } from "./Schemas";
3232
import { sanitize, simpleHash } from "./Util";
33-
import { fixProfaneUsername } from "./validations/username";
33+
import { censorNameWithClanTag } from "./validations/username";
3434

3535
export async function createGameRunner(
3636
gameStart: GameStartInfo,
@@ -46,17 +46,16 @@ export async function createGameRunner(
4646
);
4747
const random = new PseudoRandom(simpleHash(gameStart.gameID));
4848

49-
const humans = gameStart.players.map(
50-
(p) =>
51-
new PlayerInfo(
52-
p.clientID === clientID
53-
? sanitize(p.username)
54-
: fixProfaneUsername(sanitize(p.username)),
55-
PlayerType.Human,
56-
p.clientID,
57-
random.nextID(),
58-
),
59-
);
49+
const humans = gameStart.players.map((p) => {
50+
return new PlayerInfo(
51+
p.clientID === clientID
52+
? sanitize(p.username)
53+
: censorNameWithClanTag(p.username),
54+
PlayerType.Human,
55+
p.clientID,
56+
random.nextID(),
57+
);
58+
});
6059

6160
const nations = gameStart.config.disableNPCs
6261
? []

src/core/Util.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,18 @@ export function sigmoid(
339339

340340
// Compute clan from name
341341
export function getClanTag(name: string): string | null {
342+
const clanTag = clanMatch(name);
343+
return clanTag ? clanTag[1].toUpperCase() : null;
344+
}
345+
346+
export function getClanTagOriginalCase(name: string): string | null {
347+
const clanTag = clanMatch(name);
348+
return clanTag ? clanTag[1] : null;
349+
}
350+
351+
function clanMatch(name: string): RegExpMatchArray | null {
342352
if (!name.includes("[") || !name.includes("]")) {
343353
return null;
344354
}
345-
const clanMatch = name.match(/\[([a-zA-Z0-9]{2,5})\]/);
346-
return clanMatch ? clanMatch[1].toUpperCase() : null;
355+
return name.match(/\[([a-zA-Z0-9]{2,5})\]/);
347356
}

src/core/validations/username.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
skipNonAlphabeticTransformer,
99
} from "obscenity";
1010
import { translateText } from "../../client/Utils";
11-
import { simpleHash } from "../Util";
11+
import { getClanTagOriginalCase, sanitize, simpleHash } from "../Util";
1212

1313
const matcher = new RegExpMatcher({
1414
...englishDataset.build(),
@@ -45,6 +45,55 @@ export function isProfaneUsername(username: string): boolean {
4545
return matcher.hasMatch(username);
4646
}
4747

48+
/**
49+
* Sanitizes and censors profane usernames and clan tags.
50+
* Profane username is overwritten, profane clan tag is removed.
51+
*
52+
* Preserves non-profane clan tag:
53+
* prevents desync after clan team assignment because local player's own clan tag and name aren't overwritten
54+
*
55+
* Removing bad clan tags won't hurt existing clans nor cause desyncs:
56+
* - full name including clan tag was overwritten in the past, if any part of name was bad
57+
* - only each seperate local player name with a profane clan tag will remain, no clan team assignment
58+
*
59+
* Examples:
60+
* - "GoodName" -> "GoodName"
61+
* - "Good$Name" -> "GoodName"
62+
* - "BadName" -> "Censored"
63+
* - "[CLAN]GoodName" -> "[CLAN]GoodName"
64+
* - "[CLaN]BadName" -> "[CLaN] Censored"
65+
* - "[BAD]GoodName" -> "GoodName"
66+
* - "[BAD]BadName" -> "Censored"
67+
*/
68+
export function censorNameWithClanTag(username: string): string {
69+
const sanitizedUsername = sanitize(username);
70+
71+
// Don't use getClanTag because that returns upperCase and if original isn't, str replace `[{$clanTag}]` won't match
72+
const clanTag = getClanTagOriginalCase(sanitizedUsername);
73+
74+
const nameWithoutClan = clanTag
75+
? sanitizedUsername.replace(`[${clanTag}]`, "").trim()
76+
: sanitizedUsername;
77+
78+
const clanTagIsProfane = clanTag ? isProfaneUsername(clanTag) : false;
79+
const usernameIsProfane = isProfaneUsername(nameWithoutClan);
80+
81+
const censoredNameWithoutClan = usernameIsProfane
82+
? fixProfaneUsername(nameWithoutClan)
83+
: nameWithoutClan;
84+
85+
// Restore clan tag if it existed and is not profane
86+
if (clanTag && !clanTagIsProfane) {
87+
if (usernameIsProfane) {
88+
return `[${clanTag}] ${censoredNameWithoutClan}`;
89+
}
90+
return sanitizedUsername;
91+
}
92+
93+
// Don't restore profane or nonexistent clan tag
94+
return censoredNameWithoutClan;
95+
}
96+
4897
export function validateUsername(username: string): {
4998
isValid: boolean;
5099
error?: string;

0 commit comments

Comments
 (0)