From 0489c63f4b5fd4b4e9217164bab1507c9f01a221 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Sun, 20 Jul 2025 23:17:29 -0400 Subject: [PATCH] Validate spawn tile (#1512) ## Description: Enforce valid tile during spawn, to prevent the game from crashing for all players. ## 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 - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [ ] I have read and accepted the CLA aggreement (only required once). --- src/client/ClientGameRunner.ts | 2 +- src/client/Transport.ts | 11 ++++------- src/client/graphics/layers/BuildMenu.ts | 8 +------- .../graphics/layers/PlayerActionHandler.ts | 7 ++++--- .../graphics/layers/RadialMenuElements.ts | 13 ++----------- src/core/Schemas.ts | 6 ++---- src/core/execution/ConstructionExecution.ts | 19 +++++-------------- src/core/execution/ExecutionManager.ts | 13 +++---------- src/core/execution/SpawnExecution.ts | 5 +++++ 9 files changed, 27 insertions(+), 57 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index e5c731b92..b09d8198b 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -385,7 +385,7 @@ export class ClientGameRunner { !this.gameView.hasOwner(tile) && this.gameView.inSpawnPhase() ) { - this.eventBus.emit(new SendSpawnIntentEvent(cell)); + this.eventBus.emit(new SendSpawnIntentEvent(tile)); return; } if (this.gameView.inSpawnPhase()) { diff --git a/src/client/Transport.ts b/src/client/Transport.ts index dee57fc2b..c9f8fff22 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -2,7 +2,6 @@ import { z } from "zod/v4"; import { EventBus, GameEvent } from "../core/EventBus"; import { AllPlayers, - Cell, GameType, Gold, PlayerID, @@ -68,7 +67,7 @@ export class SendAllianceExtensionIntentEvent implements GameEvent { } export class SendSpawnIntentEvent implements GameEvent { - constructor(public readonly cell: Cell) {} + constructor(public readonly tile: TileRef) {} } export class SendAttackIntentEvent implements GameEvent { @@ -90,7 +89,7 @@ export class SendBoatAttackIntentEvent implements GameEvent { export class BuildUnitIntentEvent implements GameEvent { constructor( public readonly unit: UnitType, - public readonly cell: Cell, + public readonly tile: TileRef, ) {} } @@ -438,8 +437,7 @@ export class Transport { pattern: this.lobbyConfig.pattern, name: this.lobbyConfig.playerName, playerType: PlayerType.Human, - x: event.cell.x, - y: event.cell.y, + tile: event.tile, }); } @@ -540,8 +538,7 @@ export class Transport { type: "build_unit", clientID: this.lobbyConfig.clientID, unit: event.unit, - x: event.cell.x, - y: event.cell.y, + tile: event.tile, }); } diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index 4475381ab..7644347fc 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -15,7 +15,6 @@ import { translateText } from "../../../client/Utils"; import { EventBus } from "../../../core/EventBus"; import { BuildableUnit, - Cell, Gold, PlayerActions, UnitType, @@ -396,12 +395,7 @@ export class BuildMenu extends LitElement implements Layer { ), ); } else if (buildableUnit.canBuild) { - this.eventBus.emit( - new BuildUnitIntentEvent( - buildableUnit.type, - new Cell(this.game.x(tile), this.game.y(tile)), - ), - ); + this.eventBus.emit(new BuildUnitIntentEvent(buildableUnit.type, tile)); } this.hideMenu(); } diff --git a/src/client/graphics/layers/PlayerActionHandler.ts b/src/client/graphics/layers/PlayerActionHandler.ts index 42eff7ba7..e40a40d3d 100644 --- a/src/client/graphics/layers/PlayerActionHandler.ts +++ b/src/client/graphics/layers/PlayerActionHandler.ts @@ -1,5 +1,5 @@ import { EventBus } from "../../../core/EventBus"; -import { Cell, PlayerActions, PlayerID } from "../../../core/game/Game"; +import { PlayerActions, PlayerID } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { PlayerView } from "../../../core/game/GameView"; import { @@ -61,8 +61,9 @@ export class PlayerActionHandler { ): Promise { return await player.bestTransportShipSpawn(tile); } - handleSpawn(spawnCell: Cell) { - this.eventBus.emit(new SendSpawnIntentEvent(spawnCell)); + + handleSpawn(tile: TileRef) { + this.eventBus.emit(new SendSpawnIntentEvent(tile)); } handleAllianceRequest(player: PlayerView, recipient: PlayerView) { diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts index 1425fac73..693ff7526 100644 --- a/src/client/graphics/layers/RadialMenuElements.ts +++ b/src/client/graphics/layers/RadialMenuElements.ts @@ -1,10 +1,5 @@ import { Config } from "../../../core/configuration/Config"; -import { - AllPlayers, - Cell, - PlayerActions, - UnitType, -} from "../../../core/game/Game"; +import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { flattenedEmojiTable } from "../../../core/Util"; @@ -427,11 +422,7 @@ export const centerButtonElement: CenterButtonElement = { }, action: (params: MenuElementParams) => { if (params.game.inSpawnPhase()) { - const cell = new Cell( - params.game.x(params.tile), - params.game.y(params.tile), - ); - params.playerActionHandler.handleSpawn(cell); + params.playerActionHandler.handleSpawn(params.tile); } else { params.playerActionHandler.handleAttack( params.myPlayer, diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 7520f3558..f439ea5fa 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -255,8 +255,7 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({ flag: FlagSchema, pattern: PatternSchema, playerType: PlayerTypeSchema, - x: z.number(), - y: z.number(), + tile: z.number(), }); export const BoatAttackIntentSchema = BaseIntentSchema.extend({ @@ -320,8 +319,7 @@ export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({ export const BuildUnitIntentSchema = BaseIntentSchema.extend({ type: z.literal("build_unit"), unit: z.enum(UnitType), - x: z.number(), - y: z.number(), + tile: z.number(), }); export const UpgradeStructureIntentSchema = BaseIntentSchema.extend({ diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index ad7467366..8217f497d 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -1,5 +1,4 @@ import { - Cell, Execution, Game, Gold, @@ -27,12 +26,11 @@ export class ConstructionExecution implements Execution { private ticksUntilComplete: Tick; private cost: Gold; - private tile: TileRef; constructor( private player: Player, private constructionType: UnitType, - private tileOrCell: TileRef | Cell, + private tile: TileRef, ) {} init(mg: Game, ticks: number): void { @@ -46,17 +44,10 @@ export class ConstructionExecution implements Execution { return; } - if (this.tileOrCell instanceof Cell) { - if (!this.mg.isValidCoord(this.tileOrCell.x, this.tileOrCell.y)) { - console.warn( - `cannot build construction invalid coordinates ${this.tileOrCell.x}, ${this.tileOrCell.y}`, - ); - this.active = false; - return; - } - this.tile = this.mg.ref(this.tileOrCell.x, this.tileOrCell.y); - } else { - this.tile = this.tileOrCell; + if (!this.mg.isValidRef(this.tile)) { + console.warn(`cannot build construction invalid tile ${this.tile}`); + this.active = false; + return; } } diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 2b7a479e0..7a7ea2b95 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -1,4 +1,4 @@ -import { Cell, Execution, Game } from "../game/Game"; +import { Execution, Game } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; import { ClientID, GameID, Intent, Turn } from "../Schemas"; import { simpleHash } from "../Util"; @@ -67,10 +67,7 @@ export class Executor { case "move_warship": return new MoveWarshipExecution(player, intent.unitId, intent.tile); case "spawn": - return new SpawnExecution( - player.info(), - this.mg.ref(intent.x, intent.y), - ); + return new SpawnExecution(player.info(), intent.tile); case "boat": return new TransportShipExecution( player, @@ -106,11 +103,7 @@ export class Executor { case "embargo": return new EmbargoExecution(player, intent.targetID, intent.action); case "build_unit": - return new ConstructionExecution( - player, - intent.unit, - new Cell(intent.x, intent.y), - ); + return new ConstructionExecution(player, intent.unit, intent.tile); case "allianceExtension": { return new AllianceExtensionExecution(player, intent.recipient); } diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index d6d45b3d4..57baff6ee 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -20,6 +20,11 @@ export class SpawnExecution implements Execution { tick(ticks: number) { this.active = false; + if (!this.mg.isValidRef(this.tile)) { + console.warn(`SpawnExecution: tile ${this.tile} not valid`); + return; + } + if (!this.mg.inSpawnPhase()) { this.active = false; return;