This repository has been archived on 2025-06-27. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
ludusavi-manifest/src/manifest.ts
2022-10-06 17:37:09 +08:00

216 lines
6.6 KiB
TypeScript

import { REPO, YamlFile } from ".";
import { SteamGameCache, SteamGameCacheFile } from "./steam";
import { WikiGameCache, parseTemplates } from "./wiki";
export type Os = "dos" | "linux" | "mac" | "windows";
export type Bit = 32 | 64;
export type Store = "epic" | "gog" | "microsoft" | "steam" | "uplay" | "origin";
export type Tag = "config" | "save";
export interface Manifest {
[game: string]: Game;
}
export interface Game {
files?: {
[path: string]: {
when?: Array<Omit<Constraint, "bit">>,
tags?: Array<Tag>,
}
};
installDir?: {
[name: string]: {}
};
launch?: {
[path: string]: Array<{
arguments?: string;
workingDir?: string;
when?: Array<Constraint>,
}>
},
registry?: {
[path: string]: {
when?: Array<Omit<Constraint, "bit" | "os">>,
tags?: Array<Tag>,
}
};
steam?: {
id?: number
};
}
export interface Constraint {
os?: Os;
bit?: Bit;
store?: Store;
}
function normalizeLaunchPath(raw: string): string | undefined {
if (raw.includes("://")) {
return raw;
}
const standardized = raw
.replace(/\\/g, "/")
.replace(/\/\//g, "/")
.replace(/\/(?=$)/g, "")
.replace(/^.\//, "")
.replace(/^\/+/, "")
.trim();
if (standardized.length === 0 || standardized === ".") {
return undefined;
}
return `<base>/${standardized}`;
}
function doLaunchPathsMatch(fromSteam: string | undefined, fromManifest: string | undefined): boolean {
if (fromSteam === undefined) {
return fromManifest === undefined;
} else {
return normalizeLaunchPath(fromSteam) === fromManifest;
}
}
function integrateWikiData(game: Game, cache: WikiGameCache[""]): void {
if (cache.steam !== undefined) {
game.steam = { id: cache.steam };
}
const info = parseTemplates(cache.templates ?? []);
game.files = info.files;
game.registry = info.registry;
}
function integrateSteamData(game: Game, appInfo: SteamGameCache[""]): void {
if (appInfo.installDir !== undefined) {
game.installDir = { [appInfo.installDir]: {} };
}
if (appInfo.launch !== undefined) {
delete game.launch;
for (const incoming of appInfo.launch) {
if (
incoming.executable === undefined ||
incoming.executable.includes("://") ||
![undefined, "default", "none"].includes(incoming.type) ||
incoming.config?.betakey !== undefined ||
incoming.config?.ownsdlc !== undefined
) {
continue;
}
const os: Os | undefined = {
"windows": "windows",
"macos": "mac",
"macosx": "mac",
"linux": "linux",
}[incoming.config?.oslist] as Os;
const bit: Bit | undefined = {
"32": 32,
"64": 64,
}[incoming.config?.osarch] as Bit;
const when: Constraint = { os, bit, store: "steam" };
if (when.os === undefined) {
delete when.os;
}
if (when.bit === undefined) {
delete when.bit;
}
let foundExisting = false;
for (const [existingExecutable, existingOptions] of Object.entries(game.launch ?? {})) {
for (const existing of existingOptions) {
if (
incoming.arguments === existing.arguments &&
doLaunchPathsMatch(incoming.executable, existingExecutable) &&
doLaunchPathsMatch(incoming.workingdir, existing.workingDir)
) {
foundExisting = true;
if (existing.when === undefined) {
existing.when = [];
}
if (existing.when.every(x => x.os !== os && x.bit !== bit && x.store !== "steam")) {
existing.when.push(when);
}
if (existing.when.length === 0) {
delete existing.when;
}
}
}
}
if (!foundExisting) {
const key = normalizeLaunchPath(incoming.executable);
if (key === undefined) {
continue;
}
const candidate: Game["launch"][""][0] = { when: [when] };
if (incoming.arguments !== undefined) {
candidate.arguments = incoming.arguments;
}
if (incoming.workingdir !== undefined) {
const workingDir = normalizeLaunchPath(incoming.workingdir);
if (workingDir !== undefined) {
candidate.workingDir = workingDir;
}
}
if (game.launch === undefined) {
game.launch = {};
}
if (game.launch[key] === undefined) {
game.launch[key] = [];
}
game.launch[key].push(candidate);
}
}
}
}
export class ManifestFile extends YamlFile<Manifest> {
path = `${REPO}/data/manifest.yaml`;
defaultData = {};
async updateGames(
wikiCache: WikiGameCache,
games: Array<string>,
steamCache: SteamGameCacheFile,
override: ManifestOverrideFile,
): Promise<void> {
this.data = {};
for (const [title, info] of Object.entries(wikiCache).sort()) {
const overridden = override.data[title];
if (overridden?.omit) {
continue;
}
if (games?.length > 0 && !games.includes(title)) {
continue;
}
const game: Game = {};
integrateWikiData(game, info);
if (game.files === undefined && game.registry === undefined && game.steam?.id === undefined) {
continue;
}
if (game.steam?.id !== undefined) {
const appInfo = await steamCache.getAppInfo(game.steam.id);
integrateSteamData(game, appInfo);
}
this.data[title] = game;
}
}
}
export interface ManifestOverride {
[game: string]: {
omit?: boolean;
}
}
export class ManifestOverrideFile extends YamlFile<ManifestOverride> {
path = `${REPO}/data/manifest-override.yaml`;
defaultData = {};
}