From ae96eb7e98fdc6d429b0cbf05c57493beb611eea Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:32:49 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20nation=20nuke=20crash=20when=20attacker?= =?UTF-8?q?=20has=20no=20remaining=20tiles=20=F0=9F=9B=A1=EF=B8=8F=20(#370?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Fixes a game crash (`Error: array must not be empty`) thrown from `PseudoRandom.randElement` when a nation tries to pick a nuke target whose territory no longer exists. ## Root cause `NationNukeBehavior.findBestNukeTarget` calls `findIncomingAttackPlayer`, which iterates the player's `_incomingAttacks`. `AttackImpl` instances can linger in this array past the point where the attacker has lost all tiles — the attack is only removed on explicit `delete()` and `removeOnDeath` cleanup isn't guaranteed to run before other executions tick within the same turn. A dead attacker gets returned as the nuke target, `randTerritoryTileArray` samples their (empty) territory, and `randElement` throws on the empty array. ## Fix - `Player.incomingAttacks()` now filters out attacks whose attacker is no longer alive, so consumers can't observe stale references from mid-tick deaths. - `randTerritoryTile` guards against `numTilesOwned() === 0` before falling back to `randElement(p.tiles())` as a defense-in-depth safeguard at the util level. - `PlayerImpl.toUpdate()` now uses the `incomingAttacks()` / `outgoingAttacks()` accessors (rather than the raw `_` arrays) so serialized client state stays consistent with the server-side view. `outgoingAttacks()` is intentionally left unfiltered, the engine relies on seeing in-flight attacks during their retreat phase after a target is conquered (attack merging/cancellation in `AttackExecution.init`). ### Bug report from discord Please paste the following in your bug report in Discord: Game crashed! game id: LQDSWbh6 client id: JjwysSLN Error: array must not be empty Message: Error: array must not be empty at sa.randElement (https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:28:39166) at H1 (https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:116429) at Jc (https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:116135) at G1.maybeSendNuke (https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:117597) at n5.tick (https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:171764) at https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:251463 at Array.forEach () at c.executeNextTick (https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:251383) at b.executeNextTick (https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:271256) at S_ (https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:366356) ## 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: FloPinguin --- src/core/execution/nation/NationUtils.ts | 2 +- src/core/game/PlayerImpl.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/execution/nation/NationUtils.ts b/src/core/execution/nation/NationUtils.ts index a63c959f8..684b69d98 100644 --- a/src/core/execution/nation/NationUtils.ts +++ b/src/core/execution/nation/NationUtils.ts @@ -41,7 +41,7 @@ function randTerritoryTile( } } - if (p.numTilesOwned() <= 100) { + if (p.numTilesOwned() > 0 && p.numTilesOwned() <= 100) { return random.randElement(Array.from(p.tiles())); } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 05a90d1a6..40e92b13c 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -144,7 +144,7 @@ export class PlayerImpl implements Player { traitorRemainingTicks: this.getTraitorRemainingTicks(), targets: this.targets().map((p) => p.smallID()), outgoingEmojis: this.outgoingEmojis(), - outgoingAttacks: this._outgoingAttacks.map((a) => { + outgoingAttacks: this.outgoingAttacks().map((a) => { return { attackerID: a.attacker().smallID(), targetID: a.target().smallID(), @@ -153,7 +153,7 @@ export class PlayerImpl implements Player { retreating: a.retreating(), } satisfies AttackUpdate; }), - incomingAttacks: this._incomingAttacks.map((a) => { + incomingAttacks: this.incomingAttacks().map((a) => { return { attackerID: a.attacker().smallID(), targetID: a.target().smallID(), @@ -1381,7 +1381,7 @@ export class PlayerImpl implements Player { return this._outgoingAttacks; } incomingAttacks(): Attack[] { - return this._incomingAttacks; + return this._incomingAttacks.filter((a) => a.attacker().isAlive()); } public isImmune(): boolean {