Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map metadata integration #278

Merged
merged 25 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion forge.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ const config = {
},
rebuildConfig: {},
makers: [
new MakerSquirrel({}),
new MakerSquirrel({
authors: "BAR Team",
description: "Beyond All Reason Lobby",
}),
new MakerRpm({
options: {
mimeType: ["application/sdfz"],
Expand Down
1,655 changes: 317 additions & 1,338 deletions package-lock.json

Large diffs are not rendered by default.

40 changes: 16 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@
"node": "20.18.0"
},
"dependencies": {
"7zip-min": "^1.4.5",
"@extractus/feed-extractor": "^7.1.3",
"@iconify-icons/mdi": "^1.2.48",
"@iconify/json": "^2.2.264",
"@iconify/json": "^2.2.270",
"@iconify/vue": "^4.1.2",
"@octokit/rest": "^21.0.2",
"@pixi/unsafe-eval": "^7.4.2",
"@sinclair/typebox": "^0.33.17",
"@vueuse/core": "^11.1.0",
"7zip-min": "^1.4.5",
"@sinclair/typebox": "^0.33.21",
"@vueuse/core": "^11.2.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"axios": "^1.7.7",
Expand All @@ -45,18 +44,12 @@
"flag-icons": "^7.2.3",
"glob-promise": "^6.0.7",
"howler": "^2.2.4",
"jimp": "^1.6.0",
"luaparse": "^0.3.1",
"marked": "^14.1.3",
"node-stream-zip": "^1.15.0",
"marked": "^15.0.0",
"pino": "^9.5.0",
"pino-pretty": "^11.3.0",
"pixi.js": "^8.5.2",
"pino-pretty": "^12.1.0",
"primeicons": "^6.0.1",
"primevue": "3.23.0",
"remove": "^0.1.5",
"tga": "^1.0.7",
"uuid": "^10.0.0",
"vue-i18n": "^10.0.4",
"vue-router": "^4.4.5"
},
Expand All @@ -77,26 +70,25 @@
"@types/howler": "^2.2.12",
"@types/luaparse": "^0.2.12",
"@types/marked": "^6.0.0",
"@types/node": "^20.17.1",
"@types/uuid": "^10.0.0",
"@types/node": "^20.17.6",
"@vitejs/plugin-vue": "^5.1.4",
"cross-env": "^7.0.3",
"electron": "^33.0.1",
"eslint": "^9.13.0",
"electron": "33.2.0",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-vue": "^9.29.1",
"globals": "^15.11.0",
"eslint-plugin-vue": "^9.30.0",
"globals": "^15.12.0",
"prettier": "^3.3.3",
"sass": "^1.80.4",
"sass": "^1.80.6",
"ts-node": "^10.9.2",
"type-fest": "^4.26.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.11.0",
"typescript-eslint": "^8.13.0",
"unplugin-vue-router": "^0.10.8",
"vite": "^5.4.10",
"vite-plugin-static-copy": "^2.0.0",
"vitest": "^2.1.3",
"vue-tsc": "^2.1.6"
"vite-plugin-static-copy": "^2.1.0",
"vitest": "^2.1.4",
"vue-tsc": "^2.1.10"
}
}
2 changes: 1 addition & 1 deletion src/main/content/engine/engine-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export interface EngineAI {
shortName: string;
version: string;
description: string;
options: LuaOptionSection[];
options?: LuaOptionSection[];
}
29 changes: 17 additions & 12 deletions src/main/content/game/game-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ export class GameContentAPI extends PrDownloaderAPI<GameVersion> {
for (const packageFile of packages) {
const packageMd5 = packageFile.replace(".sdp", "");
const gameVersion = this.packageGameVersionLookup[packageMd5];
const luaOptionSections = await this.getGameOptions(packageMd5);
const ais = await this.getAis(packageMd5);
if (gameVersion) {
this.installedVersions.push({ gameVersion, packageMd5 });
this.installedVersions.push({ gameVersion, packageMd5, luaOptionSections, ais });
}
}
this.sortVersions();
Expand Down Expand Up @@ -117,13 +119,16 @@ export class GameContentAPI extends PrDownloaderAPI<GameVersion> {
});
}

public async getGameOptions(version: string): Promise<LuaOptionSection[]> {
const gameVersion = this.installedVersions.find((installedVersion) => installedVersion.gameVersion === version);
// TODO: cache per session
const gameFiles = await this.getGameFiles(gameVersion.packageMd5, "modoptions.lua", true);
const gameOptionsLua = gameFiles[0].data;
// TODO maybe send ais as well
return parseLuaOptions(gameOptionsLua);
protected async getGameOptions(packageMd5: string): Promise<LuaOptionSection[]> {
const gameFiles = await this.getGameFiles(packageMd5, "modoptions.lua", true);
const modoptions = gameFiles[0].data;
return parseLuaOptions(modoptions);
}

protected async getAis(packageMd5: string): Promise<GameAI[]> {
const gameFiles = await this.getGameFiles(packageMd5, "luaai.lua", true);
const luaai = gameFiles[0].data;
return this.parseAis(luaai);
}

public async getScenarios(): Promise<Scenario[]> {
Expand Down Expand Up @@ -259,14 +264,13 @@ export class GameContentAPI extends PrDownloaderAPI<GameVersion> {
}

protected async addGame(gameVersion: string) {
//TODO reimplement ais lookup now that its no longer in the GameVersion object
// const luaAiFile = (await this.getGameFiles({ md5: packageMd5 }, "luaai.lua", true))[0];
// const ais = await this.parseAis(luaAiFile.data);
if (gameVersion === "byar:test") {
await this.scanPackagesDir();
} else {
const packageMd5 = this.gameVersionPackageLookup[gameVersion];
this.installedVersions.push({ gameVersion, packageMd5 });
const luaOptionSections = await this.getGameOptions(packageMd5);
const ais = await this.getAis(packageMd5);
this.installedVersions.push({ gameVersion, packageMd5, luaOptionSections, ais });
this.sortVersions();
}
}
Expand All @@ -277,6 +281,7 @@ export class GameContentAPI extends PrDownloaderAPI<GameVersion> {
for (const def of aiDefinitions) {
ais.push({
name: def.name,
shortName: def.name,
description: def.desc,
});
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/content/game/game-version.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { LuaOptionSection } from "@main/content/game/lua-options";

export type GameVersion = {
gameVersion: string;
packageMd5: string;
luaOptionSections: LuaOptionSection[];
ais: GameAI[];
};

export interface GameAI {
name: string;
shortName: string;
description: string;
options?: LuaOptionSection[];
}
25 changes: 25 additions & 0 deletions src/main/content/maps/box-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//Boxes
export function pointsToXYWH(points: { x: number; y: number }[]) {
if (points.length !== 2) {
throw new Error("pointsToXYWH expects exactly 2 points");
}
const xs = points.map((point) => point.x);
const ys = points.map((point) => point.y);
const x = Math.min(...xs);
const y = Math.min(...ys);
const w = Math.max(...xs) - x;
const h = Math.max(...ys) - y;
return { x, y, w, h };
}

export function spadsPointsToLTRBPercent(points: { x: number; y: number }[]) {
if (points.length !== 2) {
throw new Error("SpadsPointsToLTRBPercent expects exactly 2 points");
}
return {
left: points[0].x / 200,
top: points[0].y / 200,
right: points[1].x / 200,
bottom: points[1].y / 200,
};
}
114 changes: 24 additions & 90 deletions src/main/content/maps/map-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import * as path from "path";
import { MapData } from "@main/content/maps/map-data";
import { logger } from "@main/utils/logger";
import { Signal } from "$/jaz-ts-utils/signal";
import { delay } from "$/jaz-ts-utils/delay";
import { PrDownloaderAPI } from "@main/content/pr-downloader";
import { CONTENT_PATH } from "@main/config/app";
import { asyncParseMap } from "@main/content/maps/parse-map";
import chokidar from "chokidar";
import { UltraSimpleMapParser } from "$/map-parser/ultrasimple-map-parser";

Expand All @@ -17,11 +15,10 @@ const log = logger("map-content.ts");
* @todo replace queue method with syncMapCache function once prd returns map file name
*/
export class MapContentAPI extends PrDownloaderAPI<MapData> {
public mapNameFileNameLookup: { [scriptName: string]: string } = {};
public mapNameFileNameLookup: { [springName: string]: string } = {};
public fileNameMapNameLookup: { [fileName: string]: string } = {};

public readonly onMapCachingStarted: Signal<string> = new Signal();
public readonly onMapCached: Signal<MapData> = new Signal();
public readonly onMapAdded: Signal<string> = new Signal();
public readonly onMapDeleted: Signal<string> = new Signal();

protected readonly mapsDir = path.join(CONTENT_PATH, "maps");
Expand All @@ -31,7 +28,6 @@ export class MapContentAPI extends PrDownloaderAPI<MapData> {
public override async init() {
await fs.promises.mkdir(this.mapsDir, { recursive: true });
this.initLookupMaps();
this.startCacheMapConsumer();
this.startWatchingMapFolder();
return super.init();
}
Expand All @@ -41,18 +37,23 @@ export class MapContentAPI extends PrDownloaderAPI<MapData> {
const sd7filePaths = filePaths.filter((path) => path.endsWith(".sd7"));
log.debug(`Found ${sd7filePaths.length} maps`);
for (const filePath of sd7filePaths) {
const mapName = await this.getMapNameFromFile(filePath);
const fileName = path.basename(filePath);
this.mapNameFileNameLookup[mapName] = fileName;
this.fileNameMapNameLookup[fileName] = mapName;
try {
const mapName = await this.getMapNameFromFile(filePath);
const fileName = path.basename(filePath);
this.mapNameFileNameLookup[mapName] = fileName;
this.fileNameMapNameLookup[fileName] = mapName;
} catch (err) {
log.error(`File may be corrupted, removing ${filePath}: ${err}`);
fs.promises.rm(path.join(this.mapsDir, filePath));
}
}
log.info(`Found ${Object.keys(this.mapNameFileNameLookup).length} maps`);
}

protected async getMapNameFromFile(file: string) {
const ultraSimpleMapParser = new UltraSimpleMapParser();
const parsedMap = await ultraSimpleMapParser.parseMap(path.join(this.mapsDir, file));
return parsedMap.scriptName;
return parsedMap.springName;
}

protected startWatchingMapFolder() {
Expand All @@ -73,7 +74,7 @@ export class MapContentAPI extends PrDownloaderAPI<MapData> {
this.mapNameFileNameLookup[mapName] = filename;
this.fileNameMapNameLookup[filename] = mapName;
});
this.queueMapsToCache([filename]);
this.onMapAdded.dispatch(filename);
})
.on("unlink", (filepath) => {
if (!filepath.endsWith("sd7")) {
Expand All @@ -86,110 +87,43 @@ export class MapContentAPI extends PrDownloaderAPI<MapData> {
});
}

public isVersionInstalled(scriptName: string): boolean {
return this.mapNameFileNameLookup[scriptName] !== undefined;
public isVersionInstalled(springName: string): boolean {
return this.mapNameFileNameLookup[springName] !== undefined;
}

public async downloadMaps(scriptNames: string[]) {
return Promise.all(scriptNames.map((scriptName) => this.downloadMap(scriptName)));
public async downloadMaps(springNames: string[]) {
return Promise.all(springNames.map((springName) => this.downloadMap(springName)));
}

public async downloadMap(scriptName: string) {
if (this.isVersionInstalled(scriptName)) return;
if (this.currentDownloads.some((download) => download.name === scriptName)) {
public async downloadMap(springName: string) {
if (this.isVersionInstalled(springName)) return;
if (this.currentDownloads.some((download) => download.name === springName)) {
return await new Promise<void>((resolve) => {
this.onDownloadComplete.addOnce((mapData) => {
if (mapData.name === scriptName) {
if (mapData.name === springName) {
resolve();
}
});
});
}
const downloadInfo = await this.downloadContent("map", scriptName);
const downloadInfo = await this.downloadContent("map", springName);
this.onDownloadComplete.dispatch(downloadInfo);
}

public async attemptCacheErrorMaps() {
throw new Error("Method not implemented.");
}

//Method to sync the cache with the maps folder, if the folder doesnt have the map, download it. If the folder has a map that is not in the cache, cache it.
public async sync(maps: { scriptName: string; fileName: string }[]) {
const existingFiles = await this.scanFolderForMaps();
const mapsToDownload = maps.filter((map) => !existingFiles.includes(map.fileName));
mapsToDownload.forEach((map) => this.onMapDeleted.dispatch(map.fileName));
this.downloadMaps(mapsToDownload.map((map) => map.scriptName));
const mapFileNames = maps.map((map) => map.fileName);
const mapsToCache = existingFiles.filter((map) => !mapFileNames.includes(map));
this.queueMapsToCache(mapsToCache);
}

public async scanFolderForMaps() {
let mapFiles = await fs.promises.readdir(this.mapsDir);
mapFiles = mapFiles.filter((mapFile) => mapFile.endsWith("sd7"));
return mapFiles;
}

protected async queueMapsToCache(filenames?: string[]) {
let mapFiles = filenames;
if (!filenames) {
mapFiles = await this.scanFolderForMaps();
}
for (const mapFileToCache of mapFiles) {
this.mapCacheQueue.add(mapFileToCache);
this.onMapCachingStarted.dispatch(mapFileToCache);
}
}

public async uninstallVersion(version: MapData) {
const mapFile = path.join(this.mapsDir, version.fileName);
const mapFile = path.join(this.mapsDir, version.filename);
await fs.promises.rm(mapFile, { force: true, recursive: true });
log.debug(`Map removed: ${version.scriptName}`);
}

protected async startCacheMapConsumer() {
if (this.cachingMaps) {
log.warn("Don't call cacheMaps more than once");
return;
}
this.cachingMaps = true;

while (true) {
const [mapToCache] = this.mapCacheQueue;
if (mapToCache) {
await this.cacheMap(mapToCache);
} else {
await delay(500);
}
}
}

protected async cacheMap(mapFileName: string) {
try {
log.debug(`Caching: ${mapFileName}`);
console.time(`Cached: ${mapFileName}`);
const mapPath = path.join(this.mapsDir, mapFileName);
log.debug(`Parsing map asynchronously: ${mapFileName}`);
const mapData = await asyncParseMap(mapPath);
log.debug(`Parsed map: ${mapFileName}`);
this.onMapCached.dispatch(mapData);
console.timeEnd(`Cached: ${mapFileName}`);
} catch (err) {
log.error(`Error parsing map: ${mapFileName}`, err);
log.error(err);
//TODO emit error signal
}
this.mapCacheQueue.delete(mapFileName);
}

protected mapCached(mapName: string) {
return new Promise<MapData>((resolve) => {
this.onMapCached.addOnce((map) => {
if (map.scriptName === mapName) {
resolve(map);
}
});
});
log.debug(`Map removed: ${version.springName}`);
}
}

Expand Down
Loading