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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+199 -199
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -3
View File
@@ -6,7 +6,7 @@ import { Config, getConfig } from "../core/configuration/Config";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler"
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientMessageSchema, GameConfig, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn } from "../core/Schemas";
import { createMiniMap, loadTerrainMap, TerrainMapImpl } from "../core/game/TerrainMapLoader";
import { loadTerrainFromFile, loadTerrainMap, TerrainMapImpl } from "../core/game/TerrainMapLoader";
import { and, bfs, dist, generateID, manhattanDist } from "../core/Util";
import { WinCheckExecution } from "../core/execution/WinCheckExecution";
import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport";
@@ -73,9 +73,8 @@ export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: Gam
const config = getConfig()
const terrainMap = await loadTerrainMap(gameConfig.gameMap);
const miniMap = await createMiniMap(terrainMap);
let game = createGame(terrainMap, miniMap, eventBus, config, gameConfig)
let game = createGame(terrainMap.map, terrainMap.miniMap, eventBus, config, gameConfig)
const worker = new WorkerClient(game, gameConfig.gameMap)
consolex.log('going to init path finder')
+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;
}
+30 -11
View File
@@ -38,10 +38,6 @@ export async function loadTerrainMap(): Promise<void> {
const terrain: Terrain[][] = Array(img.width).fill(null).map(() => Array(img.height).fill(null));
let max = 0
let min = 1000
// Iterate through each pixel
for (let x = 0; x < img.width; x++) {
for (let y = 0; y < img.height; y++) {
@@ -63,20 +59,40 @@ export async function loadTerrainMap(): Promise<void> {
}
}
}
// console.log(`min: ${min}, max ${max}, arr ${array}`)
// console.log('Array contents (index, value):');
// array.forEach((val, index) => {
// console.log(`(${index}, ${val})`);
// });
const shorelineWaters = processShore(terrain)
processDistToLand(shorelineWaters, terrain)
processOcean(terrain)
const packed = packTerrain(terrain)
const outputPath = path.join(__dirname, '..', '..', 'resources', 'maps', mapName + '.bin');
fs.writeFile(outputPath, packed);
fs.writeFile(outputPath, packTerrain(terrain));
const miniTerrain = await createMiniMap(terrain)
const miniOutputPath = path.join(__dirname, '..', '..', 'resources', 'maps', mapName + 'Mini.bin');
fs.writeFile(miniOutputPath, packTerrain(miniTerrain))
}
export async function createMiniMap(tm: Terrain[][]): Promise<Terrain[][]> {
// Create 2D array properly with correct dimensions
const miniMap: Terrain[][] = Array(Math.floor(tm.length / 2))
.fill(null)
.map(() => Array(Math.floor(tm[0].length / 2)).fill(null));
for (let x = 0; x < tm.length; x++) {
for (let y = 0; y < tm[0].length; y++) {
const miniX = Math.floor(x / 2);
const miniY = Math.floor(y / 2);
if (miniMap[miniX][miniY] == null || miniMap[miniX][miniY].type != TerrainType.Water) {
// We shrink 4 tiles into 1 tile. If any of the 4 large tiles
// has water, then the mini tile is considered water.
miniMap[miniX][miniY] = tm[x][y]
}
}
}
return miniMap
}
function processShore(map: Terrain[][]): Coord[] {
const shorelineWaters: Coord[] = []
for (let x = 0; x < map.length; x++) {
@@ -158,6 +174,9 @@ function packTerrain(map: Terrain[][]): Uint8Array {
for (let y = 0; y < height; y++) {
const terrain = map[x][y];
let packedByte = 0;
if (terrain == null) {
throw new Error(`terrain null at ${x}:${y}`)
}
if (terrain.type === TerrainType.Land) {
packedByte |= 0b10000000;