generate mini map offline and load it from binary. this reduces loading time by 30-40%

This commit is contained in:
evanpelle
2024-12-25 11:27:36 -08:00
parent 933d32e2af
commit 111775a3f4
15 changed files with 611 additions and 304 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ export const devConfig = new class extends DefaultConfig {
unitInfo(type: UnitType): UnitInfo {
const info = super.unitInfo(type)
const oldCost = info.cost
// info.cost = (p: Player) => oldCost(p) / 10000
info.cost = (p: Player) => oldCost(p) / 10000
return info
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { info } from "console";
import { Config } from "../configuration/Config";
import { EventBus } from "../EventBus";
import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType, UnitInfo, TerrainMap, DefenseBonus } from "./Game";
import { createMiniMap, TerrainMapImpl } from "./TerrainMapLoader";
import { TerrainMapImpl } from "./TerrainMapLoader";
import { PlayerImpl } from "./PlayerImpl";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { TileImpl } from "./TileImpl";
+87
View File
@@ -0,0 +1,87 @@
import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from './Game';
import { consolex } from '../Consolex';
import { NationMap } from './TerrainMapLoader';
interface MapData {
mapBin: string;
miniMapBin: string;
nationMap: NationMap;
}
interface MapCache {
bin?: string;
miniMapBin?: string
nationMap?: NationMap;
}
interface BinModule {
default: string;
}
interface NationMapModule {
default: NationMap;
}
// Mapping from GameMap enum values to file names
const MAP_FILE_NAMES: Record<GameMap, string> = {
[GameMap.World]: 'WorldMap',
[GameMap.Europe]: 'Europe',
[GameMap.Mena]: 'Mena',
[GameMap.NorthAmerica]: 'NorthAmerica',
[GameMap.Oceania]: 'Oceania'
};
class GameMapLoader {
private maps: Map<GameMap, MapCache>;
private loadingPromises: Map<GameMap, Promise<MapData>>;
constructor() {
this.maps = new Map<GameMap, MapCache>();
this.loadingPromises = new Map<GameMap, Promise<MapData>>();
}
public async getMapData(map: GameMap): Promise<MapData> {
const cachedMap = this.maps.get(map);
if (cachedMap?.bin && cachedMap?.nationMap) {
return cachedMap as MapData;
}
if (!this.loadingPromises.has(map)) {
this.loadingPromises.set(map, this.loadMapData(map));
}
const data = await this.loadingPromises.get(map)!;
this.maps.set(map, data);
return data;
}
private async loadMapData(map: GameMap): Promise<MapData> {
const fileName = MAP_FILE_NAMES[map];
if (!fileName) {
throw new Error(`No file name mapping found for map: ${map}`);
}
const [binModule, miniBinModule, infoModule] = await Promise.all([
import(`!!binary-loader!../../../resources/maps/${fileName}.bin`) as Promise<BinModule>,
import(`!!binary-loader!../../../resources/maps/${fileName}Mini.bin`) as Promise<BinModule>,
import(`../../../resources/maps/${fileName}.json`) as Promise<NationMapModule>
]);
return {
mapBin: binModule.default,
miniMapBin: miniBinModule.default,
nationMap: infoModule.default
};
}
public isMapLoaded(map: GameMap): boolean {
const mapData = this.maps.get(map);
return !!mapData?.bin && !!mapData?.nationMap;
}
public getLoadedMaps(): GameMap[] {
return Array.from(this.maps.keys()).filter(map => this.isMapLoaded(map));
}
}
export const terrainMapFileLoader = new GameMapLoader();
+13 -76
View File
@@ -1,29 +1,8 @@
import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from './Game';
import { SearchNode } from "../pathfinding/AStar";
import europeBin from "!!binary-loader!../../../resources/maps/Europe.bin";
import europeInfo from "../../../resources/maps/Europe.json"
import worldBin from "!!binary-loader!../../../resources/maps/WorldMap.bin";
import worldInfo from "../../../resources/maps/WorldMap.json"
import menaBin from "!!binary-loader!../../../resources/maps/Mena.bin"
import menaInfo from "../../../resources/maps/Mena.json"
import northAmericaBin from "!!binary-loader!../../../resources/maps/NorthAmerica.bin"
import northAmericaInfo from "../../../resources/maps/NorthAmerica.json"
import oceaniaBin from "!!binary-loader!../../../resources/maps/Oceania.bin"
import oceaniaInfo from "../../../resources/maps/Oceania.json"
import { consolex } from '../Consolex';
import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from './Game';
import { terrainMapFileLoader } from './TerrainMapFileLoader';
const maps = new Map()
.set(GameMap.World, { bin: worldBin, info: worldInfo })
.set(GameMap.Europe, { bin: europeBin, info: europeInfo })
.set(GameMap.Mena, { bin: menaBin, info: menaInfo })
.set(GameMap.NorthAmerica, { bin: northAmericaBin, info: northAmericaInfo })
.set(GameMap.Oceania, { bin: oceaniaBin, info: oceaniaInfo })
const loadedMaps = new Map<GameMap, TerrainMapImpl>()
const loadedMaps = new Map<GameMap, { map: TerrainMapImpl, miniMap: TerrainMapImpl }>()
export interface NationMap {
name: string;
@@ -102,17 +81,21 @@ export class TerrainMapImpl implements TerrainMap {
}
}
export async function loadTerrainMap(map: GameMap): Promise<TerrainMapImpl> {
export async function loadTerrainMap(map: GameMap): Promise<{ map: TerrainMapImpl, miniMap: TerrainMapImpl }> {
if (loadedMaps.has(map)) {
return loadedMaps.get(map)
}
const mapFiles = await terrainMapFileLoader.getMapData(map)
const mapData = maps.get(map)
const mainMap = await loadTerrainFromFile(mapFiles.mapBin)
mainMap.nationMap = mapFiles.nationMap
const mini = await loadTerrainFromFile(mapFiles.miniMapBin)
loadedMaps.set(map, { map: mainMap, miniMap: mini })
return { map: mainMap, miniMap: mini }
}
export async function loadTerrainFromFile(fileData: string): Promise<TerrainMapImpl> {
// Simulate an asynchronous file load
const fileData = await new Promise<string>((resolve) => {
setTimeout(() => resolve(mapData.bin), 100);
});
consolex.log(`Loaded data length: ${fileData.length} bytes`);
@@ -130,8 +113,6 @@ export async function loadTerrainMap(map: GameMap): Promise<TerrainMapImpl> {
const terrain: TerrainTileImpl[][] = Array(width).fill(null).map(() => Array(height).fill(null));
let numLand = 0
const m = new TerrainMapImpl();
// Start from the 5th byte (index 4) when processing terrain data
@@ -172,54 +153,10 @@ export async function loadTerrainMap(map: GameMap): Promise<TerrainMapImpl> {
}
m.tiles = terrain
m.numLandTiles = numLand
m.nationMap = mapData.info
// const encoder = new TextEncoder();
// const encoded = encoder.encode(fileData);
// const buffer = new SharedArrayBuffer(encoded.length);
// const view = new Uint8Array(buffer);
// view.set(encoded)
loadedMaps.set(map, m)
return m
}
export async function createMiniMap(tm: TerrainMap): Promise<TerrainMap> {
// Create 2D array properly with correct dimensions
const miniMap: TerrainTileImpl[][] = Array(Math.floor(tm.width() / 2))
.fill(null)
.map(() => Array(Math.floor(tm.height() / 2)).fill(null));
// Process rows in chunks to avoid blocking the main thread
const chunkSize = 10; // Process 10 rows at a time
const m = new TerrainMapImpl
for (let startX = 0; startX < tm.width(); startX += chunkSize) {
// Use setTimeout to yield to the main thread between chunks
await new Promise(resolve => setTimeout(resolve, 0));
const endX = Math.min(startX + chunkSize, tm.width());
for (let x = startX; x < endX; x++) {
for (let y = 0; y < tm.height(); y++) {
const tile = tm.terrain(new Cell(x, y)) as TerrainTileImpl;
const miniX = Math.floor(x / 2);
const miniY = Math.floor(y / 2);
if (miniMap[miniX][miniY] == null || miniMap[miniX][miniY].terrainType() != TerrainType.Ocean) {
miniMap[miniX][miniY] = new TerrainTileImpl(m, tile.terrainType(), new Cell(miniX, miniY));
miniMap[miniX][miniY].shoreline = tile.shoreline;
miniMap[miniX][miniY].magnitude = tile.magnitude;
miniMap[miniX][miniY].ocean = tile.ocean;
miniMap[miniX][miniY].land = tile.land;
}
}
}
}
m.tiles = miniMap
return m
}
function logBinaryAsAscii(data: string, length: number = 8) {
consolex.log('Binary data (1 = set bit, 0 = unset bit):');
+3 -11
View File
@@ -1,14 +1,13 @@
// pathfinding.ts
import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from "../game/Game";
import { createMiniMap, loadTerrainMap } from "../game/TerrainMapLoader";
import { loadTerrainMap } from "../game/TerrainMapLoader";
import { PriorityQueue } from "@datastructures-js/priority-queue";
import { SerialAStar } from "../pathfinding/SerialAStar";
import { AStar, PathFindResultType, SearchNode } from "../pathfinding/AStar";
import { MiniAStar } from "../pathfinding/MiniAStar";
import { consolex } from "../Consolex";
let terrainMapPromise: Promise<{
terrainMap: TerrainMap,
map: TerrainMap,
miniMap: TerrainMap
}> | null = null;
let searches = new PriorityQueue<Search>((a: Search, b: Search) => (a.deadline - b.deadline))
@@ -37,20 +36,13 @@ self.onmessage = (e) => {
initializeMap(e.data);
break;
case 'findPath':
terrainMapPromise.then(tm => findPath(tm.terrainMap, tm.miniMap, e.data))
terrainMapPromise.then(tm => findPath(tm.map, tm.miniMap, e.data))
break;
}
};
function initializeMap(data: { gameMap: GameMap }) {
terrainMapPromise = loadTerrainMap(data.gameMap)
.then(async terrainMap => {
const miniMap = await createMiniMap(terrainMap);
return {
terrainMap: terrainMap,
miniMap: miniMap
};
});
self.postMessage({ type: 'initialized' });
processingInterval = setInterval(computeSearches, .1) as unknown as number;
}