mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 09:42:03 +00:00
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
This commit is contained in:
@@ -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++) {
|
||||
|
||||
@@ -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<number> {
|
||||
private tileBFS: BFSGrid;
|
||||
@@ -314,7 +313,7 @@ export class AStarWaterHierarchical implements PathFinder<number> {
|
||||
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);
|
||||
|
||||
@@ -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<TileRef> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user