mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-17 05:49:24 +00:00
generate mini map offline and load it from binary. this reduces loading time by 30-40%
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
@@ -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):');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user