From 74476cc52082e292408da1a150b861f10eeb58d5 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 28 Jun 2025 12:26:57 -0700 Subject: [PATCH 01/35] Revert "counter attack doesn't cancel out attack (#1132)" (#1301) This reverts commit a0d17ed85ebbbff192b9d21091934bffe65b32d8. ## Description: Causes some strange edge cases. ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/core/execution/AttackExecution.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 51c07c212..1926d9991 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -122,6 +122,20 @@ export class AttackExecution implements Execution { // Record stats this.mg.stats().attack(this._owner, this.target, this.startTroops); + for (const incoming of this._owner.incomingAttacks()) { + if (incoming.attacker() === this.target) { + // Target has opposing attack, cancel them out + if (incoming.troops() > this.attack.troops()) { + incoming.setTroops(incoming.troops() - this.attack.troops()); + this.attack.delete(); + this.active = false; + return; + } else { + this.attack.setTroops(this.attack.troops() - incoming.troops()); + incoming.delete(); + } + } + } for (const outgoing of this._owner.outgoingAttacks()) { if ( outgoing !== this.attack && From 9dcceefc3391e07130579642ece824beb522d1de Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Sat, 28 Jun 2025 15:29:25 -0400 Subject: [PATCH 02/35] Graceful handling of ping before join (#1295) ## Description: Graceful handling of ping before join. ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --- src/server/Worker.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/server/Worker.ts b/src/server/Worker.ts index aedea05c7..8fc67160d 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -11,7 +11,7 @@ import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; import { COSMETICS } from "../core/CosmeticSchemas"; import { GameType } from "../core/game/Game"; import { - ClientJoinMessageSchema, + ClientMessageSchema, GameRecord, GameRecordSchema, ServerErrorMessage, @@ -299,12 +299,12 @@ export function startWorker() { try { // Parse and handle client messages - const parsed = ClientJoinMessageSchema.safeParse( + const parsed = ClientMessageSchema.safeParse( JSON.parse(message.toString()), ); if (!parsed.success) { const error = z.prettifyError(parsed.error); - log.warn("Error parsing join message client", error); + log.warn("Error parsing client message", error); ws.send( JSON.stringify({ type: "error", @@ -316,6 +316,22 @@ export function startWorker() { } const clientMsg = parsed.data; + if (clientMsg.type === "ping") { + // Ignore ping + return; + } else if (clientMsg.type !== "join") { + const error = `Invalid message before join: ${JSON.stringify(clientMsg)}`; + log.warn(error); + ws.send( + JSON.stringify({ + type: "error", + error, + } satisfies ServerErrorMessage), + ); + ws.close(1002, "ClientJoinMessageSchema"); + return; + } + // Verify this worker should handle this game const expectedWorkerId = config.workerIndex(clientMsg.gameID); if (expectedWorkerId !== workerId) { From ca522a5937622c00d505a081feec1196be25bb88 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 28 Jun 2025 12:33:19 -0700 Subject: [PATCH 03/35] refactor cosmetics out of PlayerInfo (#1299) ## Description: Remove Cosmetics from PlayerInfo. The game engine should have no knowledge of cosmetics since they shouldn't affect game play at all. Instead pass player cosmetics into the GameView. ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/client/ClientGameRunner.ts | 3 +- src/client/graphics/layers/NameLayer.ts | 4 +- .../graphics/layers/PlayerInfoOverlay.ts | 8 +-- src/client/graphics/layers/TerritoryLayer.ts | 2 +- src/core/GameRunner.ts | 11 +-- src/core/execution/BotSpawner.ts | 9 +-- src/core/game/Game.ts | 2 - src/core/game/GameUpdates.ts | 2 - src/core/game/GameView.ts | 68 ++++++++++++------- src/core/game/PlayerImpl.ts | 10 --- tests/Attack.test.ts | 4 -- tests/BotBehavior.test.ts | 4 -- tests/Disconnected.test.ts | 4 -- tests/MissileSilo.test.ts | 2 - tests/PlayerInfo.test.ts | 22 ------ tests/Stats.test.ts | 18 +---- tests/TeamAssignment.test.ts | 2 - tests/TerritoryCapture.test.ts | 9 +-- tests/UnitGrid.test.ts | 36 ++-------- tests/Warship.test.ts | 18 +---- tests/core/executions/NukeExecution.test.ts | 2 - .../executions/SAMLauncherExecution.test.ts | 8 --- tests/util/Setup.ts | 2 +- 23 files changed, 64 insertions(+), 186 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 0614732cd..12e21b1d6 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -150,9 +150,10 @@ export async function createClientGame( const gameView = new GameView( worker, config, - gameMap.gameMap, + gameMap, lobbyConfig.clientID, lobbyConfig.gameStartInfo.gameID, + lobbyConfig.gameStartInfo.players, ); console.log("going to init path finder"); diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index bb5056b3b..6b7af3197 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -198,8 +198,8 @@ export class NameLayer implements Layer { element.style.aspectRatio = "3/4"; }; - if (player.flag()) { - const flag = player.flag(); + if (player.cosmetics.flag) { + const flag = player.cosmetics.flag; if (flag !== undefined && flag !== null && flag.startsWith("!")) { const flagWrapper = document.createElement("div"); applyFlagStyles(flagWrapper); diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index 5ae2c81a0..ca2c97354 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -207,21 +207,21 @@ export class PlayerInfoOverlay extends LitElement implements Layer { ? "text-green-500" : "text-white"}" > - ${player.flag() - ? player.flag()!.startsWith("!") + ${player.cosmetics.flag + ? player.cosmetics.flag!.startsWith("!") ? html`
{ if (el instanceof HTMLElement) { requestAnimationFrame(() => { - renderPlayerFlag(player.flag()!, el); + renderPlayerFlag(player.cosmetics.flag!, el); }); } })} >
` : html`` : html``} ${player.name()} diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 28d5def51..6fdd7a48b 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -307,7 +307,7 @@ export class TerritoryLayer implements Layer { this.paintTile(tile, useBorderColor, 255); } } else { - const pattern = owner.pattern(); + const pattern = owner.cosmetics.pattern; const patternsEnabled = this.cachedTerritoryPatternsEnabled ?? false; if (pattern === undefined || patternsEnabled === false) { this.paintTile(tile, this.theme.territoryColor(owner), 150); diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index f2740c3db..93ad45fe6 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -42,8 +42,6 @@ export async function createGameRunner( const humans = gameStart.players.map( (p) => new PlayerInfo( - p.pattern, - p.flag, p.clientID === clientID ? sanitize(p.username) : fixProfaneUsername(sanitize(p.username)), @@ -60,14 +58,7 @@ export async function createGameRunner( new Nation( new Cell(n.coordinates[0], n.coordinates[1]), n.strength, - new PlayerInfo( - undefined, - n.flag || "", - n.name, - PlayerType.FakeHuman, - null, - random.nextID(), - ), + new PlayerInfo(n.name, PlayerType.FakeHuman, null, random.nextID()), ), ); diff --git a/src/core/execution/BotSpawner.ts b/src/core/execution/BotSpawner.ts index 9b9a00bb2..134a7c666 100644 --- a/src/core/execution/BotSpawner.ts +++ b/src/core/execution/BotSpawner.ts @@ -46,14 +46,7 @@ export class BotSpawner { } } return new SpawnExecution( - new PlayerInfo( - undefined, - "", - botName, - PlayerType.Bot, - null, - this.random.nextID(), - ), + new PlayerInfo(botName, PlayerType.Bot, null, this.random.nextID()), tile, ); } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index d2bbade2f..b62c62b02 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -350,8 +350,6 @@ export class PlayerInfo { public readonly clan: string | null; constructor( - public readonly pattern: string | undefined, - public readonly flag: string | undefined, public readonly name: string, public readonly playerType: PlayerType, // null if bot. diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 0c76f3a95..4f2be287c 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -133,8 +133,6 @@ export interface PlayerUpdate { type: GameUpdateType.Player; nameViewData?: NameViewData; clientID: ClientID | null; - pattern: string | undefined; - flag: string | undefined; name: string; displayName: string; id: PlayerID; diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 47f0e7128..f50bd6c85 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -1,6 +1,6 @@ import { Config } from "../configuration/Config"; import { PatternDecoder } from "../PatternDecoder"; -import { ClientID, GameID } from "../Schemas"; +import { ClientID, GameID, Player } from "../Schemas"; import { createRandomName } from "../Util"; import { WorkerClient } from "../worker/WorkerClient"; import { @@ -9,11 +9,9 @@ import { GameUpdates, Gold, NameViewData, - Player, PlayerActions, PlayerBorderTiles, PlayerID, - PlayerInfo, PlayerProfile, PlayerType, Team, @@ -32,12 +30,18 @@ import { PlayerUpdate, UnitUpdate, } from "./GameUpdates"; +import { TerrainMapData } from "./TerrainMapLoader"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { UnitGrid } from "./UnitGrid"; import { UserSettings } from "./UserSettings"; const userSettings: UserSettings = new UserSettings(); +interface PlayerCosmetics { + pattern?: string | undefined; + flag?: string | undefined; +} + export class UnitView { public _wasUpdated = true; public lastPos: TileRef[] = []; @@ -145,6 +149,7 @@ export class PlayerView { private game: GameView, public data: PlayerUpdate, public nameData: NameViewData, + public cosmetics: PlayerCosmetics, ) { if (data.clientID === game.myClientID()) { this.anonymousName = this.data.name; @@ -155,7 +160,9 @@ export class PlayerView { ); } this.decoder = - data.pattern === undefined ? undefined : new PatternDecoder(data.pattern); + this.cosmetics.pattern === undefined + ? undefined + : new PatternDecoder(this.cosmetics.pattern); } patternDecoder(): PatternDecoder | undefined { @@ -202,13 +209,6 @@ export class PlayerView { smallID(): number { return this.data.smallID; } - flag(): string | undefined { - return this.data.flag; - } - - pattern(): string | undefined { - return this.data.pattern; - } name(): string { return this.anonymousName !== null && userSettings.anonymousNames() @@ -236,7 +236,7 @@ export class PlayerView { isAlive(): boolean { return this.data.isAlive; } - isPlayer(): this is Player { + isPlayer(): this is PlayerView { return true; } numTilesOwned(): number { @@ -306,16 +306,7 @@ export class PlayerView { outgoingEmojis(): EmojiMessage[] { return this.data.outgoingEmojis; } - info(): PlayerInfo { - return new PlayerInfo( - this.pattern(), - this.flag(), - this.name(), - this.type(), - this.clientID(), - this.id(), - ); - } + hasSpawned(): boolean { return this.data.hasSpawned; } @@ -338,16 +329,35 @@ export class GameView implements GameMap { private toDelete = new Set(); + private _cosmetics: Map = new Map(); + + private _map: GameMap; + constructor( public worker: WorkerClient, private _config: Config, - private _map: GameMap, + private _mapData: TerrainMapData, private _myClientID: ClientID, private _gameID: GameID, + private _hunans: Player[], ) { + this._map = this._mapData.gameMap; this.lastUpdate = null; - this.unitGrid = new UnitGrid(_map); + this.unitGrid = new UnitGrid(this._map); + this._cosmetics = new Map( + this._hunans.map((h) => [ + h.clientID, + { flag: h.flag, pattern: h.pattern } satisfies PlayerCosmetics, + ]), + ); + for (const nation of this._mapData.manifest.nations) { + // Nations don't have client ids, so we use their name as the key instead. + this._cosmetics.set(nation.name, { + flag: nation.flag, + }); + } } + isOnEdgeOfMap(ref: TileRef): boolean { return this._map.isOnEdgeOfMap(ref); } @@ -379,7 +389,15 @@ export class GameView implements GameMap { } else { this._players.set( pu.id, - new PlayerView(this, pu, gu.playerNameViewData[pu.id]), + new PlayerView( + this, + pu, + gu.playerNameViewData[pu.id], + // First check human by clientID, then check nation by name. + this._cosmetics.get(pu.clientID ?? "") ?? + this._cosmetics.get(pu.name) ?? + {}, + ), ); } }); diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index b9f045d00..ddc0658dd 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -128,8 +128,6 @@ export class PlayerImpl implements Player { return { type: GameUpdateType.Player, clientID: this.clientID(), - pattern: this.pattern(), - flag: this.flag(), name: this.name(), displayName: this.displayName(), id: this.id(), @@ -177,14 +175,6 @@ export class PlayerImpl implements Player { return this._smallID; } - pattern(): string | undefined { - return this.playerInfo.pattern; - } - - flag(): string | undefined { - return this.playerInfo.flag; - } - name(): string { return this._name; } diff --git a/tests/Attack.test.ts b/tests/Attack.test.ts index 71beae6b5..8974f6250 100644 --- a/tests/Attack.test.ts +++ b/tests/Attack.test.ts @@ -33,8 +33,6 @@ describe("Attack", () => { infiniteTroops: true, }); const attackerInfo = new PlayerInfo( - undefined, - "us", "attacker dude", PlayerType.Human, null, @@ -42,8 +40,6 @@ describe("Attack", () => { ); game.addPlayer(attackerInfo); const defenderInfo = new PlayerInfo( - undefined, - "us", "defender dude", PlayerType.Human, null, diff --git a/tests/BotBehavior.test.ts b/tests/BotBehavior.test.ts index e296892bb..9710770ac 100644 --- a/tests/BotBehavior.test.ts +++ b/tests/BotBehavior.test.ts @@ -23,16 +23,12 @@ describe("BotBehavior.handleAllianceRequests", () => { }); const playerInfo = new PlayerInfo( - undefined, - "us", "player_id", PlayerType.Bot, null, "player_id", ); const requestorInfo = new PlayerInfo( - undefined, - "fr", "requestor_id", PlayerType.Human, null, diff --git a/tests/Disconnected.test.ts b/tests/Disconnected.test.ts index 709c8104a..e03138efa 100644 --- a/tests/Disconnected.test.ts +++ b/tests/Disconnected.test.ts @@ -16,8 +16,6 @@ describe("Disconnected", () => { }); const player1Info = new PlayerInfo( - undefined, - "us", "Active Player", PlayerType.Human, null, @@ -25,8 +23,6 @@ describe("Disconnected", () => { ); const player2Info = new PlayerInfo( - undefined, - "fr", "Disconnected Player", PlayerType.Human, null, diff --git a/tests/MissileSilo.test.ts b/tests/MissileSilo.test.ts index 6e58ba2c5..c3a1f5ab1 100644 --- a/tests/MissileSilo.test.ts +++ b/tests/MissileSilo.test.ts @@ -33,8 +33,6 @@ describe("MissileSilo", () => { beforeEach(async () => { game = await setup("plains", { infiniteGold: true, instantBuild: true }); const attacker_info = new PlayerInfo( - undefined, - "fr", "attacker_id", PlayerType.Human, null, diff --git a/tests/PlayerInfo.test.ts b/tests/PlayerInfo.test.ts index eca14e173..72ca1cceb 100644 --- a/tests/PlayerInfo.test.ts +++ b/tests/PlayerInfo.test.ts @@ -4,8 +4,6 @@ describe("PlayerInfo", () => { describe("clan", () => { test("should extract clan from name when format is [XX]Name", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[CL]PlayerName", PlayerType.Human, null, @@ -16,8 +14,6 @@ describe("PlayerInfo", () => { test("should extract clan from name when format is [XXX]Name", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[ABC]PlayerName", PlayerType.Human, null, @@ -28,8 +24,6 @@ describe("PlayerInfo", () => { test("should extract clan from name when format is [XXXX]Name", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[ABCD]PlayerName", PlayerType.Human, null, @@ -40,8 +34,6 @@ describe("PlayerInfo", () => { test("should extract clan from name when format is [XXXXX]Name", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[ABCDE]PlayerName", PlayerType.Human, null, @@ -52,8 +44,6 @@ describe("PlayerInfo", () => { test("should extract clan from name when format is [xxxxx]Name", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[abcde]PlayerName", PlayerType.Human, null, @@ -64,8 +54,6 @@ describe("PlayerInfo", () => { test("should extract clan from name when format is [XxXxX]Name", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[AbCdE]PlayerName", PlayerType.Human, null, @@ -76,8 +64,6 @@ describe("PlayerInfo", () => { test("should return null when name doesn't start with [", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "PlayerName", PlayerType.Human, null, @@ -88,8 +74,6 @@ describe("PlayerInfo", () => { test("should return null when name doesn't contain ]", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[ABCPlayerName", PlayerType.Human, null, @@ -100,8 +84,6 @@ describe("PlayerInfo", () => { test("should return null when clan tag is not 2-5 uppercase letters", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[A]PlayerName", PlayerType.Human, null, @@ -112,8 +94,6 @@ describe("PlayerInfo", () => { test("should return null when clan tag contains non alphanumeric characters", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[A1c]PlayerName", PlayerType.Human, null, @@ -124,8 +104,6 @@ describe("PlayerInfo", () => { test("should return null when clan tag is too long", () => { const playerInfo = new PlayerInfo( - undefined, - "fr", "[ABCDEF]PlayerName", PlayerType.Human, null, diff --git a/tests/Stats.test.ts b/tests/Stats.test.ts index b988210bf..c53804b4f 100644 --- a/tests/Stats.test.ts +++ b/tests/Stats.test.ts @@ -19,22 +19,8 @@ describe("Stats", () => { beforeEach(async () => { stats = new StatsImpl(); game = await setup("half_land_half_ocean", {}, [ - new PlayerInfo( - undefined, - "us", - "boat dude", - PlayerType.Human, - "client1", - "player_1_id", - ), - new PlayerInfo( - undefined, - "us", - "boat dude", - PlayerType.Human, - "client2", - "player_2_id", - ), + new PlayerInfo("boat dude", PlayerType.Human, "client1", "player_1_id"), + new PlayerInfo("boat dude", PlayerType.Human, "client2", "player_2_id"), ]); while (game.inSpawnPhase()) { diff --git a/tests/TeamAssignment.test.ts b/tests/TeamAssignment.test.ts index bea1ea74f..5d9f5bcab 100644 --- a/tests/TeamAssignment.test.ts +++ b/tests/TeamAssignment.test.ts @@ -7,8 +7,6 @@ describe("assignTeams", () => { const createPlayer = (id: string, clan?: string): PlayerInfo => { const name = clan ? `[${clan}]Player ${id}` : `Player ${id}`; return new PlayerInfo( - undefined, - "šŸ³ļø", // flag name, PlayerType.Human, null, // clientID (null for testing) diff --git a/tests/TerritoryCapture.test.ts b/tests/TerritoryCapture.test.ts index d547010ff..e46678c08 100644 --- a/tests/TerritoryCapture.test.ts +++ b/tests/TerritoryCapture.test.ts @@ -6,14 +6,7 @@ describe("Territory management", () => { test("player owns the tile it spawns on", async () => { const game = await setup("plains"); game.addPlayer( - new PlayerInfo( - undefined, - "us", - "test_player", - PlayerType.Human, - null, - "test_id", - ), + new PlayerInfo("test_player", PlayerType.Human, null, "test_id"), ); const spawnTile = game.map().ref(50, 50); game.addExecution( diff --git a/tests/UnitGrid.test.ts b/tests/UnitGrid.test.ts index ffd4ff9f6..9a0869ace 100644 --- a/tests/UnitGrid.test.ts +++ b/tests/UnitGrid.test.ts @@ -11,14 +11,7 @@ async function checkRange( const game = await setup(mapName, { infiniteGold: true, instantBuild: true }); const grid = new UnitGrid(game.map()); const player = game.addPlayer( - new PlayerInfo( - undefined, - "us", - "test_player", - PlayerType.Human, - null, - "test_id", - ), + new PlayerInfo("test_player", PlayerType.Human, null, "test_id"), ); const unitTile = game.map().ref(unitPosX, 0); grid.addUnit(player.buildUnit(UnitType.DefensePost, unitTile, {})); @@ -41,14 +34,7 @@ async function nearbyUnits( const game = await setup(mapName, { infiniteGold: true, instantBuild: true }); const grid = new UnitGrid(game.map()); const player = game.addPlayer( - new PlayerInfo( - undefined, - "us", - "test_player", - PlayerType.Human, - null, - "test_id", - ), + new PlayerInfo("test_player", PlayerType.Human, null, "test_id"), ); const unitTile = game.map().ref(unitPosX, 0); for (const unitType of unitTypes) { @@ -122,14 +108,7 @@ describe("Unit Grid range tests", () => { }); const grid = new UnitGrid(game.map()); const player = game.addPlayer( - new PlayerInfo( - undefined, - "us", - "test_player", - PlayerType.Human, - null, - "test_id", - ), + new PlayerInfo("test_player", PlayerType.Human, null, "test_id"), ); const unitTile = game.map().ref(0, 0); grid.addUnit(player.buildUnit(UnitType.City, unitTile, {})); @@ -146,14 +125,7 @@ describe("Unit Grid range tests", () => { }); const grid = new UnitGrid(game.map()); const player = game.addPlayer( - new PlayerInfo( - undefined, - "us", - "test_player", - PlayerType.Human, - null, - "test_id", - ), + new PlayerInfo("test_player", PlayerType.Human, null, "test_id"), ); const unitType = UnitType.City; const unitTile = game.map().ref(0, 0); diff --git a/tests/Warship.test.ts b/tests/Warship.test.ts index 16a72e397..ee6c556de 100644 --- a/tests/Warship.test.ts +++ b/tests/Warship.test.ts @@ -24,22 +24,8 @@ describe("Warship", () => { instantBuild: true, }, [ - new PlayerInfo( - undefined, - "us", - "boat dude", - PlayerType.Human, - null, - "player_1_id", - ), - new PlayerInfo( - undefined, - "us", - "boat dude", - PlayerType.Human, - null, - "player_2_id", - ), + new PlayerInfo("boat dude", PlayerType.Human, null, "player_1_id"), + new PlayerInfo("boat dude", PlayerType.Human, null, "player_2_id"), ], ); diff --git a/tests/core/executions/NukeExecution.test.ts b/tests/core/executions/NukeExecution.test.ts index 435d388b4..5a614ebea 100644 --- a/tests/core/executions/NukeExecution.test.ts +++ b/tests/core/executions/NukeExecution.test.ts @@ -25,8 +25,6 @@ describe("NukeExecution", () => { outer: 10, })); const player_info = new PlayerInfo( - undefined, - "us", "player_id", PlayerType.Human, null, diff --git a/tests/core/executions/SAMLauncherExecution.test.ts b/tests/core/executions/SAMLauncherExecution.test.ts index 7ceb08e1a..993488399 100644 --- a/tests/core/executions/SAMLauncherExecution.test.ts +++ b/tests/core/executions/SAMLauncherExecution.test.ts @@ -25,32 +25,24 @@ describe("SAM", () => { instantBuild: true, }); const defender_info = new PlayerInfo( - undefined, - "us", "defender_id", PlayerType.Human, null, "defender_id", ); const middle_defender_info = new PlayerInfo( - undefined, - "us", "middle_defender_id", PlayerType.Human, null, "middle_defender_id", ); const far_defender_info = new PlayerInfo( - undefined, - "us", "far_defender_id", PlayerType.Human, null, "far_defender_id", ); const attacker_info = new PlayerInfo( - undefined, - "fr", "attacker_id", PlayerType.Human, null, diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index 7c6e86daa..c552d7d4f 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -83,5 +83,5 @@ export async function setup( } export function playerInfo(name: string, type: PlayerType): PlayerInfo { - return new PlayerInfo(undefined, "fr", name, type, null, name); + return new PlayerInfo(name, type, null, name); } From 7496f1b8841479ae277dfd1a99a9d037c83806c2 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Sat, 28 Jun 2025 23:43:20 -0400 Subject: [PATCH 04/35] Remove unused MON_* credentials (#1304) ## Description: Remove unused MON_* credentials ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --- .github/workflows/deploy.yml | 2 -- .github/workflows/release.yml | 8 -------- example.env | 4 ---- 3 files changed, 14 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d0909dd6e..29255afb8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -112,8 +112,6 @@ jobs: DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} ENV: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }} HOST: ${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }} - MON_PASSWORD: ${{ secrets.MON_PASSWORD }} - MON_USERNAME: ${{ secrets.MON_USERNAME }} OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }} OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }} OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 884ab1cc7..3e59a1603 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,8 +68,6 @@ jobs: DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} DOMAIN: ${{ vars.DOMAIN }} IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }} - MON_PASSWORD: ${{ secrets.MON_PASSWORD }} - MON_USERNAME: ${{ secrets.MON_USERNAME }} OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }} OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }} OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }} @@ -124,8 +122,6 @@ jobs: DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} DOMAIN: ${{ vars.DOMAIN }} IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }} - MON_PASSWORD: ${{ secrets.MON_PASSWORD }} - MON_USERNAME: ${{ secrets.MON_USERNAME }} OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }} OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }} OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }} @@ -180,8 +176,6 @@ jobs: DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} DOMAIN: ${{ vars.DOMAIN }} IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }} - MON_PASSWORD: ${{ secrets.MON_PASSWORD }} - MON_USERNAME: ${{ secrets.MON_USERNAME }} OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }} OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }} OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }} @@ -236,8 +230,6 @@ jobs: DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} DOMAIN: ${{ vars.DOMAIN }} IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }} - MON_PASSWORD: ${{ secrets.MON_PASSWORD }} - MON_USERNAME: ${{ secrets.MON_USERNAME }} OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }} OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }} OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }} diff --git a/example.env b/example.env index 03ce7bd1d..d186dab02 100644 --- a/example.env +++ b/example.env @@ -24,9 +24,5 @@ SERVER_HOST_STAGING=123.456.78.90 SERVER_HOST_EU=123.456.78.91 SERVER_HOST_US=123.456.78.92 -# Monitoring Credentials -MON_USERNAME=monitor_username -MON_PASSWORD=monitor_password - # Version VERSION_TAG="latest" From 4fc1175ad420ad2ce3084c011b89e29877667f24 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Sun, 29 Jun 2025 20:09:26 -0400 Subject: [PATCH 05/35] Add new patterns (#1294) ## Description: Add a new patterns - Cats - Shells ![image](https://github.com/user-attachments/assets/36b4c832-8879-45f8-bfbb-d5a1b5b7aefe) ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --- resources/cosmetics/cosmetics.json | 8 ++++++++ resources/lang/en.json | 2 ++ src/core/Schemas.ts | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/resources/cosmetics/cosmetics.json b/resources/cosmetics/cosmetics.json index 30d02b72e..cd6023c60 100644 --- a/resources/cosmetics/cosmetics.json +++ b/resources/cosmetics/cosmetics.json @@ -64,6 +64,10 @@ "name": "vertical_bars", "role_group": "donor" }, + "AHEgzGfznzu43XPoL2fMn_O4O3fdL-g": { + "name": "shells", + "role_group": "donor" + }, "AHEYAAAAAAAAAkCCQUQiLnQWaA": { "name": "-w-", "role_group": "donor" @@ -76,6 +80,10 @@ "name": "goat", "role_group": "donor" }, + "ALF1AAAAAAAAABABAAAAAACwAQAAAAAA8AEAAAAAIFABIoABABDwATbAA4gQ9AU-wBPZIPgDKoSx-UD4Az4C9Kmf8AEcUlj5v_ABPvb9-X_wAX7k__A_8AN-_H_gH_AFfuJ_wBiwJf_BP0AIEBkAAAAAAAAAAAAAAAAAAAAAAAAAIgAAAAAAADYAiAAA-AA-ANgAELEBKgD4ALDhI_4HqgDwsSf-H_oIUPlH_C9xBP_BT_gn-YT_wD_8J_rEf-Af_hP8yD_gHwAM-NAf4B8AAvzhP8APAAAAAAAAAA": { + "name": "cats", + "role_group": "donor" + }, "AJhYAAAAGACABACQAAASAEACAMgBAMkBIMkAJCngJAkSIEECIEgABAKAQAAQEAACAiCAAAQQgAAECIAAAfA_AAAA": { "name": "hand", "role_group": "donor" diff --git a/resources/lang/en.json b/resources/lang/en.json index dc2848d5c..9e57b68df 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -498,9 +498,11 @@ "scattered_dots": "Scattered Dots", "circuit_board": "Circuit Board", "vertical_bars": "Vertical Bars", + "shells": "Shells", "-w-": ".w.", "white_rabbit": "White Rabbit", "goat": "Goat", + "cats": "Cats", "cursor": "Cursor", "hand": "Hand", "radiation": "Radiation", diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 44f492563..873278125 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -181,7 +181,7 @@ export const UsernameSchema = SafeString; export const FlagSchema = z.string().max(128).optional(); export const RequiredPatternSchema = z .string() - .max(128) + .max(1403) .base64url() .refine( (val) => { From a0c13c5457da1ab550efd2208ea9029f961567c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rokas=20Leonavi=C4=8Dius?= <101990229+fraxxio@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:54:31 +0300 Subject: [PATCH 06/35] Fix error-modal filling up the whole screen (#1298) ## Description: Fixes https://github.com/openfrontio/OpenFrontIO/issues/1279 Now text element is limited to max height of 400px. https://github.com/user-attachments/assets/7b95a105-f329-4f8c-b3a5-1a69f29d189d ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: fraxx. --- src/client/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/styles.css b/src/client/styles.css index dba13d5d7..e3bc2ab11 100644 --- a/src/client/styles.css +++ b/src/client/styles.css @@ -483,6 +483,7 @@ label.option-card:hover { overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; + max-height: 400px; } #error-modal button.copy-btn { From 9d25668791fd18f8234cf8c17c7f5fdd21c42bda Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Mon, 30 Jun 2025 19:43:49 -0400 Subject: [PATCH 07/35] Reapply "enable otel logs and metrics for staging environments" (#1310) This reverts commit d7e7df1df6365c939a1fefcc89883059c3c2c3e4. ## Description: enable otel logs and metrics for staging environments ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --- src/server/Logger.ts | 3 +-- src/server/Worker.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/Logger.ts b/src/server/Logger.ts index 94e508fbb..19832f50e 100644 --- a/src/server/Logger.ts +++ b/src/server/Logger.ts @@ -7,7 +7,6 @@ import { import { OpenTelemetryTransportV3 } from "@opentelemetry/winston-transport"; import * as dotenv from "dotenv"; import winston from "winston"; -import { GameEnv } from "../core/configuration/Config"; import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; import { getOtelResource } from "./OtelResource"; dotenv.config(); @@ -21,7 +20,7 @@ const loggerProvider = new LoggerProvider({ resource, }); -if (config.env() === GameEnv.Prod && config.otelEnabled()) { +if (config.otelEnabled()) { console.log("OTEL enabled"); // Configure OpenTelemetry endpoint with basic auth (if provided) const headers = {}; diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 8fc67160d..e27d91cbb 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -46,7 +46,7 @@ export function startWorker() { const privilegeChecker = new PrivilegeChecker(COSMETICS); - if (config.env() === GameEnv.Prod && config.otelEnabled()) { + if (config.otelEnabled()) { initWorkerMetrics(gm); } From 9ddd9ca48a4af6a68e41b1a54d540d91d73029f6 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:32:44 -0400 Subject: [PATCH 08/35] Separate prod release environments (#1311) ## Description: Separate prod release environments to allow each one to be approved independently. ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e59a1603..be381cf97 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,7 +100,7 @@ jobs: runs-on: ubuntu-latest needs: [build, deploy-alpha] timeout-minutes: 30 - environment: prod + environment: prod-beta steps: - uses: actions/checkout@v4 - name: šŸ”‘ Create SSH private key @@ -154,7 +154,7 @@ jobs: runs-on: ubuntu-latest needs: [build, deploy-alpha] timeout-minutes: 30 - environment: prod + environment: prod-blue steps: - uses: actions/checkout@v4 - name: šŸ”‘ Create SSH private key @@ -208,7 +208,7 @@ jobs: runs-on: ubuntu-latest needs: [build, deploy-alpha] timeout-minutes: 30 - environment: prod + environment: prod-green steps: - uses: actions/checkout@v4 - name: šŸ”‘ Create SSH private key From c8fa802d1e84fb8dbde9a72dc71c5301b1863fa7 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:04:36 -0400 Subject: [PATCH 09/35] Change news title to release notes (#1312) ## Description: Change news title to release notes ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --- .github/workflows/release.yml | 1 + resources/lang/en.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be381cf97..5f74cd170 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,7 @@ on: types: - created - edited + - published permissions: {} diff --git a/resources/lang/en.json b/resources/lang/en.json index 9e57b68df..fab3cead6 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -26,7 +26,7 @@ "news": { "full_changelog": "See the complete change log", "github_link": "on GitHub", - "title": "Version 23 released!" + "title": "Release Notes" }, "help_modal": { "hotkeys": "Hotkeys", From 91adf1fa8b2cc63d09c0d6c55264250a5b443c37 Mon Sep 17 00:00:00 2001 From: Tomasz <58752526+TomaszOleszko@users.noreply.github.com> Date: Tue, 1 Jul 2025 04:06:50 +0200 Subject: [PATCH 10/35] Add localization support for leaderboard and team-related UI elements (#1308) ## Description: As in the title ## Please complete the following: - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: pierogi69 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: evanpelle --- resources/lang/en.json | 16 +++++++++++++++- src/client/graphics/layers/GameLeftSidebar.ts | 12 ++++++++++-- src/client/graphics/layers/Leaderboard.ts | 4 +++- src/client/graphics/layers/TeamStats.ts | 10 +++++----- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index fab3cead6..9191490b4 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -43,6 +43,7 @@ "action_reset_gfx": "Reset graphics", "ui_section": "Game UI", "ui_leaderboard": "Leaderboard", + "ui_your_team": "Your team:", "ui_leaderboard_desc": "Shows the top players of the game and their names, % owned land, gold and troops. Using Show All shows all players in the game. If you don't want to see the leaderboard, click Hide.", "ui_control": "Control panel", "ui_control_desc": "The control panel contains the following elements:", @@ -203,6 +204,16 @@ "waiting": "Waiting for players...", "start": "Start Game" }, + "team_colors": { + "red": "Red", + "blue": "Blue", + "teal": "Teal", + "purple": "Purple", + "yellow": "Yellow", + "orange": "Orange", + "green": "Green", + "bot": "Bot" + }, "game_starting_modal": { "title": "Game is Starting...", "desc": "Preparing for the lobby to start. Please wait." @@ -392,9 +403,12 @@ "hide": "Hide", "rank": "Rank", "player": "Player", + "team": "Team", "owned": "Owned", "gold": "Gold", - "troops": "Troops" + "troops": "Troops", + "show_top_5": "Show Top 5", + "show_all": "Show All" }, "player_info_overlay": { "type": "Type", diff --git a/src/client/graphics/layers/GameLeftSidebar.ts b/src/client/graphics/layers/GameLeftSidebar.ts index 2f4bb2f6f..fc38ca1a4 100644 --- a/src/client/graphics/layers/GameLeftSidebar.ts +++ b/src/client/graphics/layers/GameLeftSidebar.ts @@ -3,6 +3,7 @@ import { html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; import { GameMode } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; +import { translateText } from "../../Utils"; import "../icons/LeaderboardRegularIcon"; import "../icons/LeaderboardSolidIcon"; import "../icons/TeamRegularIcon"; @@ -66,6 +67,13 @@ export class GameLeftSidebar extends LitElement implements Layer { return this.game?.config().gameConfig().gameMode === GameMode.Team; } + private getTranslatedPlayerTeamLabel(): string { + if (!this.playerTeam) return ""; + const translationKey = `team_colors.${this.playerTeam.toLowerCase()}`; + const translated = translateText(translationKey); + return translated === translationKey ? this.playerTeam : translated; + } + render() { return html`