diff --git a/src/client/graphics/Utils.ts b/src/client/graphics/Utils.ts index 3e838b242..6368fffa5 100644 --- a/src/client/graphics/Utils.ts +++ b/src/client/graphics/Utils.ts @@ -1,5 +1,3 @@ -import twemoji from 'twemoji'; -import DOMPurify from 'dompurify'; export function renderTroops(troops: number): string { let troopsStr = '' @@ -32,21 +30,3 @@ export function createCanvas(): HTMLCanvasElement { return canvas } - -export function processName(name: string): string { - const sanitized = Array.from(name).slice(0, 10).join('').replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}]/gu, ''); - - // First sanitize the raw input - strip everything except text and emojis - const withEmojis = twemoji.parse(sanitized, { - base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/', // Use jsDelivr CDN - folder: 'svg', // or 'png' if you prefer - ext: '.svg' // or '.png' if you prefer - }); - return DOMPurify.sanitize(withEmojis, { - ALLOWED_TAGS: ['img'], - ALLOWED_ATTR: ['src', 'alt', 'class'], - // Only allow twemoji CDN URLs - ALLOWED_URI_REGEXP: /^https:\/\/cdn\.jsdelivr\.net\/gh\/twitter\/twemoji/ - }); - -} \ No newline at end of file diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 17a2dd078..f51199d71 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -1,9 +1,9 @@ -import {nullable} from "zod"; -import {EventBus, GameEvent} from "../../../core/EventBus"; -import {AllianceExpiredEvent, AllianceRequestEvent, AllianceRequestReplyEvent, AllPlayers, BrokeAllianceEvent, EmojiMessageEvent, Game, Player, PlayerID, TargetPlayerEvent} from "../../../core/game/Game"; -import {ClientID} from "../../../core/Schemas"; -import {Layer} from "./Layer"; -import {SendAllianceReplyIntentEvent} from "../../Transport"; +import { nullable } from "zod"; +import { EventBus, GameEvent } from "../../../core/EventBus"; +import { AllianceExpiredEvent, AllianceRequestEvent, AllianceRequestReplyEvent, AllPlayers, BrokeAllianceEvent, EmojiMessageEvent, Game, Player, PlayerID, TargetPlayerEvent } from "../../../core/game/Game"; +import { ClientID } from "../../../core/Schemas"; +import { Layer } from "./Layer"; +import { SendAllianceReplyIntentEvent } from "../../Transport"; export enum MessageType { SUCCESS, @@ -234,7 +234,7 @@ export class EventsDisplay implements Layer { } if (event.message.recipient == myPlayer) { this.addEvent({ - description: `${event.message.sender.name()}:${event.message.emoji}`, + description: `${event.message.sender.displayName()}:${event.message.emoji}`, type: MessageType.INFO, highlight: true, createdAt: this.game.ticks(), @@ -242,7 +242,7 @@ export class EventsDisplay implements Layer { } if (event.message.sender == myPlayer && event.message.recipient != AllPlayers) { this.addEvent({ - description: `Sent ${event.message.recipient.name()} ${event.message.emoji}`, + description: `Sent ${event.message.recipient.displayName()} ${event.message.emoji}`, type: MessageType.INFO, highlight: true, createdAt: this.game.ticks(), diff --git a/src/client/graphics/layers/Leaderboard.ts b/src/client/graphics/layers/Leaderboard.ts index 727b15169..8eb10bc8d 100644 --- a/src/client/graphics/layers/Leaderboard.ts +++ b/src/client/graphics/layers/Leaderboard.ts @@ -4,9 +4,6 @@ import { Layer } from './Layer'; import { Game, Player } from '../../../core/game/Game'; import { ClientID } from '../../../core/Schemas'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; -import { processName } from '../Utils'; - - interface Entry { name: string @@ -68,7 +65,7 @@ export class Leaderboard extends LitElement implements Layer { this.players.pop() this.players.push({ - name: myPlayer.name(), + name: myPlayer.displayName(), position: place, score: formatPercentage(myPlayer.numTilesOwned() / this.game.numLandTiles()), isMyPlayer: true, @@ -166,7 +163,7 @@ export class Leaderboard extends LitElement implements Layer { .map((player, index) => html` ${player.position} - ${unsafeHTML(processName(player.name))} + ${unsafeHTML(player.name)} ${player.score} `)} diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index f91fafdaf..ce4817911 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -7,7 +7,6 @@ export type ClientID = string export type Intent = SpawnIntent | AttackIntent | BoatAttackIntent - | UpdateNameIntent | AllianceRequestIntent | AllianceRequestReplyIntent | BreakAllianceIntent @@ -19,7 +18,6 @@ export type Intent = SpawnIntent export type AttackIntent = z.infer export type SpawnIntent = z.infer export type BoatAttackIntent = z.infer -export type UpdateNameIntent = z.infer export type AllianceRequestIntent = z.infer export type AllianceRequestReplyIntent = z.infer export type BreakAllianceIntent = z.infer @@ -101,11 +99,6 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({ y: z.number(), }) -export const UpdateNameIntentSchema = BaseIntentSchema.extend({ - type: z.literal('updateName'), - name: z.string(), -}) - export const AllianceRequestIntentSchema = BaseIntentSchema.extend({ type: z.literal('allianceRequest'), requestor: z.string(), @@ -157,7 +150,6 @@ const IntentSchema = z.union([ AttackIntentSchema, SpawnIntentSchema, BoatAttackIntentSchema, - UpdateNameIntentSchema, AllianceRequestIntentSchema, AllianceRequestReplyIntentSchema, BreakAllianceIntentSchema, diff --git a/src/core/Util.ts b/src/core/Util.ts index 7d353c867..c0f12d386 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -1,7 +1,9 @@ -import {v4 as uuidv4} from 'uuid'; +import { v4 as uuidv4 } from 'uuid'; +import twemoji from 'twemoji'; +import DOMPurify from 'dompurify'; -import {Cell, Game, Player, TerraNullius, Tile} from "./game/Game"; +import { Cell, Game, Player, TerraNullius, Tile } from "./game/Game"; export function manhattanDist(c1: Cell, c2: Cell): number { return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y); @@ -97,7 +99,7 @@ export function simpleHash(str: string): number { return Math.abs(hash); } -export function calculateBoundingBox(borderTiles: ReadonlySet): {min: Cell; max: Cell} { +export function calculateBoundingBox(borderTiles: ReadonlySet): { min: Cell; max: Cell } { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; borderTiles.forEach((tile: Tile) => { @@ -108,10 +110,10 @@ export function calculateBoundingBox(borderTiles: ReadonlySet): {min: Cell maxY = Math.max(maxY, cell.y); }); - return {min: new Cell(minX, minY), max: new Cell(maxX, maxY)} + return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) } } -export function inscribed(outer: {min: Cell; max: Cell}, inner: {min: Cell; max: Cell}): boolean { +export function inscribed(outer: { min: Cell; max: Cell }, inner: { min: Cell; max: Cell }): boolean { return ( outer.min.x <= inner.min.x && outer.min.y <= inner.min.y && @@ -122,7 +124,7 @@ export function inscribed(outer: {min: Cell; max: Cell}, inner: {min: Cell; max: export function getMode(list: string[]): string { // Count occurrences - const counts: {[key: string]: number} = {}; + const counts: { [key: string]: number } = {}; for (const item of list) { counts[item] = (counts[item] || 0) + 1; } @@ -139,4 +141,24 @@ export function getMode(list: string[]): string { } return mode; +} + +export function sanitize(name: string): string { + return Array.from(name).slice(0, 10).join('').replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}]/gu, ''); +} + +export function processName(name: string): string { + // First sanitize the raw input - strip everything except text and emojis + const withEmojis = twemoji.parse(sanitize(name), { + base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/', // Use jsDelivr CDN + folder: 'svg', // or 'png' if you prefer + ext: '.svg' // or '.png' if you prefer + }); + return DOMPurify.sanitize(withEmojis, { + ALLOWED_TAGS: ['img'], + ALLOWED_ATTR: ['src', 'alt', 'class'], + // Only allow twemoji CDN URLs + ALLOWED_URI_REGEXP: /^https:\/\/cdn\.jsdelivr\.net\/gh\/twitter\/twemoji/ + }); + } \ No newline at end of file diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 703660c1a..60bfafb0d 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -1,21 +1,20 @@ -import {Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile, PlayerType, Alliance, AllianceRequestReplyEvent, Difficulty} from "../game/Game"; -import {AttackIntent, BoatAttackIntentSchema, GameID, Intent, Turn} from "../Schemas"; -import {AttackExecution} from "./AttackExecution"; -import {SpawnExecution} from "./SpawnExecution"; -import {BotSpawner} from "./BotSpawner"; -import {BoatAttackExecution} from "./BoatAttackExecution"; -import {PseudoRandom} from "../PseudoRandom"; -import {UpdateNameExecution} from "./UpdateNameExecution"; -import {FakeHumanExecution} from "./FakeHumanExecution"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile, PlayerType, Alliance, AllianceRequestReplyEvent, Difficulty } from "../game/Game"; +import { AttackIntent, BoatAttackIntentSchema, GameID, Intent, Turn } from "../Schemas"; +import { AttackExecution } from "./AttackExecution"; +import { SpawnExecution } from "./SpawnExecution"; +import { BotSpawner } from "./BotSpawner"; +import { BoatAttackExecution } from "./BoatAttackExecution"; +import { PseudoRandom } from "../PseudoRandom"; +import { FakeHumanExecution } from "./FakeHumanExecution"; import Usernames from '../../../resources/Usernames.txt' -import {simpleHash} from "../Util"; -import {AllianceRequestExecution} from "./alliance/AllianceRequestExecution"; -import {AllianceRequestReplyExecution} from "./alliance/AllianceRequestReplyExecution"; -import {BreakAllianceExecution} from "./alliance/BreakAllianceExecution"; -import {TargetPlayerExecution} from "./TargetPlayerExecution"; -import {EmojiExecution} from "./EmojiExecution"; -import {DonateExecution} from "./DonateExecution"; -import {NukeExecution} from "./NukeExecution"; +import { processName, sanitize, simpleHash } from "../Util"; +import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution"; +import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyExecution"; +import { BreakAllianceExecution } from "./alliance/BreakAllianceExecution"; +import { TargetPlayerExecution } from "./TargetPlayerExecution"; +import { EmojiExecution } from "./EmojiExecution"; +import { DonateExecution } from "./DonateExecution"; +import { NukeExecution } from "./NukeExecution"; @@ -48,7 +47,7 @@ export class Executor { ) } else if (intent.type == "spawn") { return new SpawnExecution( - new PlayerInfo(intent.name.slice(0, 18), intent.playerType, intent.clientID, intent.playerID), + new PlayerInfo(sanitize(intent.name), intent.playerType, intent.clientID, intent.playerID), new Cell(intent.x, intent.y) ) } else if (intent.type == "boat") { @@ -58,11 +57,6 @@ export class Executor { new Cell(intent.x, intent.y), intent.troops ) - } else if (intent.type == "updateName") { - return new UpdateNameExecution( - intent.name, - intent.clientID - ) } else if (intent.type == "allianceRequest") { return new AllianceRequestExecution(intent.requestor, intent.recipient) } else if (intent.type == "allianceRequestReply") { diff --git a/src/core/execution/UpdateNameExecution.ts b/src/core/execution/UpdateNameExecution.ts deleted file mode 100644 index 3702a1ab6..000000000 --- a/src/core/execution/UpdateNameExecution.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {Execution, MutableGame, MutablePlayer, PlayerID} from "../game/Game" -import {ClientID} from "../Schemas" - -export class UpdateNameExecution implements Execution { - - private active = true - private mg: MutableGame - - constructor(private newName: string, private clientID: ClientID) { - } - - init(mg: MutableGame, ticks: number) { - this.mg = mg - } - - tick(ticks: number) { - const player = this.mg.players().find(p => p.clientID() == this.clientID) - if (player == null) { - return - } - player.setName(this.newName) - this.active = false - } - - owner(): MutablePlayer { - return null - } - - isActive(): boolean { - return this.active - } - activeDuringSpawnPhase(): boolean { - return true - } -} \ No newline at end of file diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 0208dd4f4..a2a2b8ab0 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -163,6 +163,7 @@ export interface TerraNullius { export interface Player { info(): PlayerInfo name(): string + displayName(): string clientID(): ClientID id(): PlayerID type(): PlayerType diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index e6226d98d..852aae510 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -1,12 +1,12 @@ -import {MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent, EmojiMessage, EmojiMessageEvent, AllPlayers, Currency} from "./Game"; -import {ClientID} from "../Schemas"; -import {simpleHash} from "../Util"; -import {CellString, GameImpl} from "./GameImpl"; -import {BoatImpl} from "./BoatImpl"; -import {TileImpl} from "./TileImpl"; -import {TerraNulliusImpl} from "./TerraNulliusImpl"; -import {MessageType} from "../../client/graphics/layers/EventsDisplay"; -import {renderTroops} from "../../client/graphics/Utils"; +import { MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent, EmojiMessage, EmojiMessageEvent, AllPlayers, Currency } from "./Game"; +import { ClientID } from "../Schemas"; +import { processName, simpleHash } from "../Util"; +import { CellString, GameImpl } from "./GameImpl"; +import { BoatImpl } from "./BoatImpl"; +import { TileImpl } from "./TileImpl"; +import { TerraNulliusImpl } from "./TerraNulliusImpl"; +import { MessageType } from "../../client/graphics/layers/EventsDisplay"; +import { renderTroops } from "../../client/graphics/Utils"; interface Target { tick: Tick @@ -29,6 +29,7 @@ export class PlayerImpl implements MutablePlayer { public _tiles: Map = new Map(); private _name: string; + private _displayerName: string; public pastOutgoingAllianceRequests: AllianceRequest[] = [] @@ -40,11 +41,15 @@ export class PlayerImpl implements MutablePlayer { constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, private _troops) { this._name = playerInfo.name; + this._displayerName = processName(this._name) } name(): string { return this._name; } + displayName(): string { + return this._displayerName + } clientID(): ClientID { return this.playerInfo.clientID; @@ -115,19 +120,19 @@ export class PlayerImpl implements MutablePlayer { return toRemove } - isPlayer(): this is MutablePlayer {return true as const;} - ownsTile(cell: Cell): boolean {return this._tiles.has(cell.toString());} - setTroops(troops: number) {this._troops = Math.floor(troops);} - conquer(tile: Tile) {this.gs.conquer(this, tile);} + isPlayer(): this is MutablePlayer { return true as const; } + ownsTile(cell: Cell): boolean { return this._tiles.has(cell.toString()); } + setTroops(troops: number) { this._troops = Math.floor(troops); } + conquer(tile: Tile) { this.gs.conquer(this, tile); } relinquish(tile: Tile) { if (tile.owner() != this) { throw new Error(`Cannot relinquish tile not owned by this player`); } this.gs.relinquish(tile); } - info(): PlayerInfo {return this.playerInfo;} - troops(): number {return this._troops;} - isAlive(): boolean {return this._tiles.size > 0;} + info(): PlayerInfo { return this.playerInfo; } + troops(): number { return this._troops; } + isAlive(): boolean { return this._tiles.size > 0; } executions(): Execution[] { return this.gs.executions().filter(exec => exec.owner().id() == this.id()); } @@ -204,7 +209,7 @@ export class PlayerImpl implements MutablePlayer { } target(other: Player): void { - this.targets_.push({tick: this.gs.ticks(), target: other}) + this.targets_.push({ tick: this.gs.ticks(), target: other }) this.gs.eventBus.emit(new TargetPlayerEvent(this, other)) }