mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 14:00:54 +00:00
generate mini map offline and load it from binary. this reduces loading time by 30-40%
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+199
-199
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
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user