From 3c196cb7e7c90528d64968a3d9369622ae5ce966 Mon Sep 17 00:00:00 2001 From: blon Date: Fri, 3 Jul 2026 15:45:30 -0400 Subject: [PATCH] crit fix: indian subcontinent map crash (#4479) Resolves #4401 NOTE: While this PR is an improvment, the Indian subcontinent crash IS NOT caused by >253 water components, as the map only has ~15 water components. ## Description: Fixes a critical browser tab crash ("Aw, Snap! Something went wrong") when loading the game on the new Indian Subcontinent map (or any map with >= 253 water components) in Solo Mode. ### Technical Cause: 1. When a map contains >= 253 disconnected water components, the array mapping tiles to component IDs is dynamically promoted from a Uint8Array to a Uint16Array. 2. This promotion upgrades the land sentinel LAND_MARKER from 0xff (255) to LAND_MARKER_WIDE (0xffff / 65535). 3. The BFS local search filter in AStarWaterHierarchical had a hardcoded sentinel check: (t: TileRef) => this.graph.getComponentId(t) !== LAND_MARKER (evaluating against 255). 4. On promoted maps, land tiles (65535) matched this check as water. The local BFS then traversed the entire landmass of the map, resulting in CPU exhaustion and memory/stack overflows that crashed the rendering process. ### Solution: Changed the hardcoded sentinel check to query the map's terrain directly via this.map.isWater(t). This makes the check immune to any component ID promotions or sentinel representation upgrades. Verified that the existing water pathfinding test suite passes successfully. ## Please complete the following: - [ ] I have added screenshots for all UI updates (N/A) - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file (N/A) - [x] I have added relevant tests to the test directory (Existing tests in tests/core/pathfinding/PathFinding.Water.test.ts cover water pathfinding behavior and run successfully) ## Please put your Discord username so you can be contacted if a bug or regression is found: blontd6 --- src/core/game/GameMap.ts | 22 ++++++++++++------- .../algorithms/AStar.WaterHierarchical.ts | 3 +-- .../pathfinding/PathFinding.Water.test.ts | 21 ++++++++++++++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts index 92689275b..292e3c47c 100644 --- a/src/core/game/GameMap.ts +++ b/src/core/game/GameMap.ts @@ -111,10 +111,16 @@ export class GameMapImpl implements GameMap { private readonly width_: number; private readonly height_: number; - // Lookup tables (LUTs) contain pre-computed values to avoid performing division at runtime - private readonly refToX: number[]; - private readonly refToY: number[]; - private readonly yToRef: number[]; + // Lookup tables (LUTs) contain pre-computed values to avoid performing division at runtime. + // Typed arrays are used instead of plain JS Array to keep memory tight on large maps: + // Uint16Array uses 2 bytes/element vs ~8 bytes for a boxed number, saving ~53 MB on + // the Indian Subcontinent map (2000×2220 = 4.44 M tiles). + // Coordinates never exceed 65535 for any map in the game, so Uint16 is safe for x/y. + // yToRef stores tile refs (up to width*height-1) which can exceed 65535 for large maps, + // so it uses Int32Array. + private readonly refToX: Uint16Array; + private readonly refToY: Uint16Array; + private readonly yToRef: Int32Array; // Terrain bits (Uint8Array) private static readonly IS_LAND_BIT = 7; @@ -148,11 +154,11 @@ export class GameMapImpl implements GameMap { this.height_ = height; this.terrain = terrainData; this.state = new Uint16Array(width * height); - // Precompute the LUTs + // Precompute the LUTs using typed arrays (see field declarations for rationale). let ref = 0; - this.refToX = new Array(width * height); - this.refToY = new Array(width * height); - this.yToRef = new Array(height); + this.refToX = new Uint16Array(width * height); + this.refToY = new Uint16Array(width * height); + this.yToRef = new Int32Array(height); for (let y = 0; y < height; y++) { this.yToRef[y] = ref; for (let x = 0; x < width; x++) { diff --git a/src/core/pathfinding/algorithms/AStar.WaterHierarchical.ts b/src/core/pathfinding/algorithms/AStar.WaterHierarchical.ts index 2958de79a..933804fbb 100644 --- a/src/core/pathfinding/algorithms/AStar.WaterHierarchical.ts +++ b/src/core/pathfinding/algorithms/AStar.WaterHierarchical.ts @@ -5,7 +5,6 @@ import { AbstractGraphAStar } from "./AStar.AbstractGraph"; import { AStarWaterBounded } from "./AStar.WaterBounded"; import { AbstractGraph, AbstractNode } from "./AbstractGraph"; import { BFSGrid } from "./BFS.Grid"; -import { LAND_MARKER } from "./ConnectedComponents"; export class AStarWaterHierarchical implements PathFinder { private tileBFS: BFSGrid; @@ -314,7 +313,7 @@ export class AStarWaterHierarchical implements PathFinder { this.map.height(), tile, maxDistance, - (t: TileRef) => this.graph.getComponentId(t) !== LAND_MARKER, + (t: TileRef) => this.map.isWater(t), (t: TileRef, _dist: number) => { const tileX = this.map.x(t); const tileY = this.map.y(t); diff --git a/tests/core/pathfinding/PathFinding.Water.test.ts b/tests/core/pathfinding/PathFinding.Water.test.ts index 48d06b20f..fceef9764 100644 --- a/tests/core/pathfinding/PathFinding.Water.test.ts +++ b/tests/core/pathfinding/PathFinding.Water.test.ts @@ -12,6 +12,7 @@ import { createGame, L, W } from "./_fixtures"; describe("PathFinding.Water", () => { let game: Game; let worldGame: Game; + let giantWorldGame: Game; function createPathFinder(g: Game = game): SteppingPathFinder { return PathFinding.Water(g); @@ -20,6 +21,7 @@ describe("PathFinding.Water", () => { beforeAll(async () => { game = await setup("ocean_and_land"); worldGame = await setup("world", { disableNavMesh: false }); + giantWorldGame = await setup("giantworldmap", { disableNavMesh: false }); }); describe("findPath", () => { @@ -274,4 +276,23 @@ describe("PathFinding.Water", () => { expect(path).not.toBeNull(); }); }); + + describe("Giant World Map routes", () => { + it("routes correctly in promoted components", () => { + const pathFinder = createPathFinder(giantWorldGame); + const map = giantWorldGame.map(); + + // Coordinates inside Component 459 in map4x (promoted) + const from = map.ref(2616, 452); + const to = map.ref(2676, 474); + + expect(map.isWater(from)).toBe(true); + expect(map.isWater(to)).toBe(true); + + const path = pathFinder.findPath(from, to); + expect(path).not.toBeNull(); + expect(path![0]).toBe(from); + expect(path![path!.length - 1]).toBe(to); + }); + }); });