Move maps generation out of repo, new map structure (#1256)

## Description:

Move map generation outside of main repo, it has been rewritten in Go
and is much faster. Also refactor how maps are stored, one dir per map.
The map binaries are basically identical to before. Some maps like
Africa have 1% difference in bytes, but playing it looks exactly the
same.

Use lazy loading for map data access so only needed files are accessed.

Unit tests now load map binary instead of regenerating it from scratch,
speeding them up.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
This commit is contained in:
evanpelle
2025-06-23 11:23:56 -07:00
committed by GitHub
parent 06b18892e4
commit b48770faf0
169 changed files with 611 additions and 1207 deletions
-2
View File
@@ -67,11 +67,9 @@ export class GameMapImpl implements GameMap {
private static readonly IS_LAND_BIT = 7;
private static readonly SHORELINE_BIT = 6;
private static readonly OCEAN_BIT = 5;
private static readonly MAGNITUDE_OFFSET = 4; // Uses bits 3-7 (5 bits)
private static readonly MAGNITUDE_MASK = 0x1f; // 11111 in binary
// State bits (Uint16Array)
private static readonly PLAYER_ID_OFFSET = 0; // Uses bits 0-11 (12 bits)
private static readonly PLAYER_ID_MASK = 0xfff;
private static readonly FALLOUT_BIT = 13;
private static readonly DEFENSE_BONUS_BIT = 14;
+55 -85
View File
@@ -2,18 +2,13 @@ import { GameMapType } from "./Game";
import { NationMap } from "./TerrainMapLoader";
interface MapData {
mapBin: string;
miniMapBin: string;
nationMap: NationMap;
mapBin: () => Promise<string>;
miniMapBin: () => Promise<string>;
nationMap: () => Promise<NationMap>;
webpPath: () => Promise<string>;
}
interface MapCache {
bin?: string;
miniMapBin?: string;
nationMap?: NationMap;
}
interface BinModule {
export interface BinModule {
default: string;
}
@@ -21,90 +16,65 @@ interface NationMapModule {
default: NationMap;
}
// Mapping from GameMap enum values to file names
const MAP_FILE_NAMES: Record<GameMapType, string> = {
[GameMapType.World]: "WorldMap",
[GameMapType.GiantWorldMap]: "WorldMapGiant",
[GameMapType.Europe]: "Europe",
[GameMapType.Mena]: "Mena",
[GameMapType.NorthAmerica]: "NorthAmerica",
[GameMapType.Oceania]: "Oceania",
[GameMapType.BlackSea]: "BlackSea",
[GameMapType.Africa]: "Africa",
[GameMapType.Pangaea]: "Pangaea",
[GameMapType.Asia]: "Asia",
[GameMapType.Mars]: "Mars",
[GameMapType.SouthAmerica]: "SouthAmerica",
[GameMapType.Britannia]: "Britannia",
[GameMapType.GatewayToTheAtlantic]: "GatewayToTheAtlantic",
[GameMapType.Australia]: "Australia",
[GameMapType.Iceland]: "Iceland",
[GameMapType.EastAsia]: "EastAsia",
[GameMapType.BetweenTwoSeas]: "BetweenTwoSeas",
[GameMapType.FaroeIslands]: "FaroeIslands",
[GameMapType.DeglaciatedAntarctica]: "DeglaciatedAntarctica",
[GameMapType.EuropeClassic]: "EuropeClassic",
[GameMapType.FalklandIslands]: "FalklandIslands",
[GameMapType.Baikal]: "Baikal",
[GameMapType.Halkidiki]: "Halkidiki",
};
class GameMapLoader {
private maps: Map<GameMapType, MapCache>;
private loadingPromises: Map<GameMapType, Promise<MapData>>;
private maps: Map<GameMapType, MapData>;
constructor() {
this.maps = new Map<GameMapType, MapCache>();
this.loadingPromises = new Map<GameMapType, Promise<MapData>>();
this.maps = new Map<GameMapType, MapData>();
}
public async getMapData(map: GameMapType): 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: GameMapType): 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,
private createLazyLoader<T>(importFn: () => Promise<T>): () => Promise<T> {
let cache: Promise<T> | null = null;
return () => {
if (!cache) {
cache = importFn();
}
return cache;
};
}
public isMapLoaded(map: GameMapType): boolean {
const mapData = this.maps.get(map);
return !!mapData?.bin && !!mapData?.nationMap;
}
public getMapData(map: GameMapType): MapData {
const cachedMap = this.maps.get(map);
if (cachedMap) {
return cachedMap;
}
public getLoadedMaps(): GameMapType[] {
return Array.from(this.maps.keys()).filter((map) => this.isMapLoaded(map));
const key = Object.keys(GameMapType).find((k) => GameMapType[k] === map);
const fileName = key?.toLowerCase();
const mapData = {
mapBin: this.createLazyLoader(() =>
(
import(
`!!binary-loader!../../../resources/maps/${fileName}/map.bin`
) as Promise<BinModule>
).then((m) => m.default),
),
miniMapBin: this.createLazyLoader(() =>
(
import(
`!!binary-loader!../../../resources/maps/${fileName}/mini_map.bin`
) as Promise<BinModule>
).then((m) => m.default),
),
nationMap: this.createLazyLoader(() =>
(
import(
`../../../resources/maps/${fileName}/manifest.json`
) as Promise<NationMapModule>
).then((m) => m.default),
),
webpPath: this.createLazyLoader(() =>
(
import(
`../../../resources/maps/${fileName}/thumbnail.webp`
) as Promise<{ default: string }>
).then((m) => m.default),
),
} satisfies MapData;
this.maps.set(map, mapData);
return mapData;
}
}
+5 -4
View File
@@ -11,6 +11,7 @@ export type TerrainMapData = {
const loadedMaps = new Map<GameMapType, TerrainMapData>();
export interface NationMap {
name: string;
nations: Nation[];
}
@@ -26,12 +27,12 @@ export async function loadTerrainMap(
): Promise<TerrainMapData> {
const cached = loadedMaps.get(map);
if (cached !== undefined) return cached;
const mapFiles = await terrainMapFileLoader.getMapData(map);
const mapFiles = terrainMapFileLoader.getMapData(map);
const gameMap = await genTerrainFromBin(mapFiles.mapBin);
const miniGameMap = await genTerrainFromBin(mapFiles.miniMapBin);
const gameMap = await genTerrainFromBin(await mapFiles.mapBin());
const miniGameMap = await genTerrainFromBin(await mapFiles.miniMapBin());
const result = {
nationMap: mapFiles.nationMap,
nationMap: await mapFiles.nationMap(),
gameMap: gameMap,
miniGameMap: miniGameMap,
};