feat: Implemented dolphin integration
This commit is contained in:
parent
edbc390d14
commit
a69147a4f7
24 changed files with 220 additions and 59 deletions
|
|
@ -20,6 +20,7 @@ export async function getOrCached<T> (key: string, getter: () => Promise<T>, opt
|
|||
}
|
||||
|
||||
const data = await getter();
|
||||
if (data === undefined) return data;
|
||||
|
||||
const expire_at = options?.expireMs ? new Date(updated_at.getTime() + options.expireMs) : new Date(updated_at.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { cores } from '../../emulatorjs/emulatorjs';
|
|||
import { LaunchGameJob } from '../../jobs/launch-game-job';
|
||||
import { EmulatorPackageType } from '@/shared/constants';
|
||||
import { getStoreEmulatorPackage, getStoreFolder } from '../../store/services/gamesService';
|
||||
import { getOrCached } from '../../cache';
|
||||
import { getScoopPackage } from '../../store/services/emulatorsService';
|
||||
|
||||
export const varRegex = /%([^%]+)%/g;
|
||||
export const assignRegex = /(%\w+%)=(\S+) /g;
|
||||
|
|
@ -285,11 +287,27 @@ export async function findStoreEmulatorExec (id: string, emulator?: { systempath
|
|||
const storeExecName = (await Promise.all(storeEmulator.downloads[`${process.platform}:${process.arch}`].map(async dl =>
|
||||
{
|
||||
// glob file search causes issues so do manual search
|
||||
const glob = new Glob(dl.pattern);
|
||||
if (await fs.exists(storeEmulatorFolder))
|
||||
{
|
||||
const glob = (dl as any).pattern ? new Glob((dl as any).pattern) : undefined;
|
||||
let bin: string | undefined = (dl as any).bin;
|
||||
if (!bin && dl.type === 'scoop')
|
||||
{
|
||||
const data = await getScoopPackage(id, dl.url);
|
||||
|
||||
if (data)
|
||||
{
|
||||
bin = data.bin;
|
||||
}
|
||||
}
|
||||
|
||||
const files = (await fs.readdir(storeEmulatorFolder))
|
||||
.filter(f => glob.match(f));
|
||||
.filter(f =>
|
||||
{
|
||||
if (glob && glob.match(f)) return true;
|
||||
if (bin && f === bin) return true;
|
||||
});
|
||||
|
||||
return files.map(f => path.join(storeEmulatorFolder, f));
|
||||
}
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ export class GameHooks
|
|||
id: number;
|
||||
};
|
||||
}], string[] | undefined>(['ctx']);
|
||||
/**
|
||||
* Is the given emulator for the given command supported
|
||||
* @returns The possible value is if it can support it but not right now. To show grayed out icon.
|
||||
*/
|
||||
emulatorLaunchSupport = new SyncBailHook<[ctx: {
|
||||
emulator: string;
|
||||
source?: EmulatorSourceEntryType;
|
||||
}], { id: string; possible: boolean; } | undefined>(['ctx']);
|
||||
/**
|
||||
* Fetches and returns a list of games converted to frontend.
|
||||
* @param ctx.localGameIds This is local game ids in the format '<source>@<sourceId>'
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { Downloader } from "@/bun/utils/downloader";
|
|||
import { ensureDir, move } from "fs-extra";
|
||||
import { simulateProgress } from "@/bun/utils";
|
||||
import { path7za } from "7zip-bin";
|
||||
import { getScoopPackage } from "../store/services/emulatorsService";
|
||||
|
||||
type EmulatorDownloadStates = "download" | "extract";
|
||||
|
||||
|
|
@ -55,6 +56,34 @@ export class EmulatorDownloadJob implements IJob<z.infer<typeof EmulatorDownload
|
|||
} else if (validDownload.type === 'direct')
|
||||
{
|
||||
downloadUrl = new URL(validDownload.url);
|
||||
} else if (validDownload.type === 'scoop')
|
||||
{
|
||||
const data = await getScoopPackage(this.emulator, validDownload.url);
|
||||
let scoopDownload: URL | undefined;
|
||||
if (data)
|
||||
{
|
||||
if (data.url)
|
||||
{
|
||||
scoopDownload = new URL(data.url);
|
||||
} else if (data.architecture)
|
||||
{
|
||||
if (process.arch === 'x64' && data.architecture["64bit"])
|
||||
{
|
||||
scoopDownload = new URL(data.architecture["64bit"].url);
|
||||
} else if (process.arch === "arm64" && data.architecture["arm64"])
|
||||
{
|
||||
scoopDownload = new URL(data.architecture["arm64"].url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scoopDownload)
|
||||
{
|
||||
downloadUrl = scoopDownload;
|
||||
} else
|
||||
{
|
||||
throw new Error("Could not find scoop download");
|
||||
}
|
||||
} else
|
||||
{
|
||||
throw new Error("Download Type Unsupported");
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default class UpdateStoreJob implements IJob<never, never>
|
|||
await ensureDir(storeFolder);
|
||||
|
||||
console.log("Updating Store");
|
||||
const proc = Bun.spawn([process.execPath, "add", `${this.packageName}@${this.storeVersion}`, "--production", "--registry", this.registry.href], {
|
||||
const proc = Bun.spawn([process.execPath, "install", `${this.packageName}@${this.storeVersion}`, "--registry", this.registry.href], {
|
||||
cwd: storeFolder,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
import { config, db } from "@/bun/api/app";
|
||||
import { PluginContextType, PluginType } from "@/bun/types/typesc.schema";
|
||||
import path from 'node:path';
|
||||
import desc from './package.json';
|
||||
|
||||
export default class DOLPHINIntegration implements PluginType
|
||||
{
|
||||
load (ctx: PluginContextType)
|
||||
{
|
||||
ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) =>
|
||||
{
|
||||
if (ctx.emulator === 'DOLPHIN')
|
||||
return { id: desc.name, possible: !!ctx.source };
|
||||
});
|
||||
|
||||
ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) =>
|
||||
{
|
||||
if (ctx.autoValidCommand.emulator === 'DOLPHIN' && ctx.autoValidCommand.metadata.emulatorDir)
|
||||
{
|
||||
const args = ["--batch"];
|
||||
|
||||
const storageFolder = path.join(config.get('downloadPath'), "saves", 'DOLPHIN');
|
||||
|
||||
args.push(...[`--user=${storageFolder}`, `--exec=${ctx.autoValidCommand.metadata.romPath}`]);
|
||||
args.push(`--config=Dolphin.Display.Fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`);
|
||||
args.push(`--config=Dolphin.General.ISOPath0=${path.join(config.get('downloadPath'), 'roms', 'gc')}`);
|
||||
args.push(`--config=Dolphin.General.ISOPath1=${path.join(config.get('downloadPath'), 'roms', 'wii')}`);
|
||||
args.push(`--config=Dolphin.Interface.ConfirmStop=False`);
|
||||
args.push(`--config=Dolphin.Interface.SkipNKitWarning=True`);
|
||||
args.push(`--config=Dolphin.Analytics.PermissionAsked=True`);
|
||||
|
||||
return args;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "com.simeonradivoev.gameflow.dolphin",
|
||||
"displayName": "DOLPHIN Integration",
|
||||
"version": "0.0.1",
|
||||
"description": "DOLPHIN Emulator Integration",
|
||||
"main": "./dolphin.ts",
|
||||
"icon": "https://upload.wikimedia.org/wikipedia/commons/5/53/Dolphin_Emulator_Logo_Refresh.svg",
|
||||
"keywords": [
|
||||
"integration",
|
||||
"emulator",
|
||||
"wiiu",
|
||||
"gc",
|
||||
"dolphin"
|
||||
]
|
||||
}
|
||||
|
|
@ -11,6 +11,14 @@ export default class PCSX2Integration implements PluginType
|
|||
{
|
||||
load (ctx: PluginContextType)
|
||||
{
|
||||
ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) =>
|
||||
{
|
||||
if (ctx.emulator === 'PCSX2')
|
||||
{
|
||||
return { id: desc.name, possible: ctx.source?.type === 'store' };
|
||||
}
|
||||
});
|
||||
|
||||
ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) =>
|
||||
{
|
||||
if (ctx.autoValidCommand.emulator === 'PCSX2' && ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@ export default class PCSX2Integration implements PluginType
|
|||
{
|
||||
load (ctx: PluginContextType)
|
||||
{
|
||||
|
||||
ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) =>
|
||||
{
|
||||
if (ctx.emulator === 'PPSSPP')
|
||||
{
|
||||
return { id: desc.name, possible: ctx.source?.type === 'store' };
|
||||
}
|
||||
});
|
||||
|
||||
ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) =>
|
||||
{
|
||||
if (ctx.autoValidCommand.emulator === 'PPSSPP' && ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { PluginManager } from "./plugin-manager";
|
|||
|
||||
import pcsx2 from './builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json';
|
||||
import ppsspp from './builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json';
|
||||
import dolphin from './builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json';
|
||||
import romm from './builtin/sources/com.simeonradivoev.gameflow.romm/package.json';
|
||||
import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@/bun/types/typesc.schema";
|
||||
|
||||
|
|
@ -11,6 +12,7 @@ export default async function register (pluginManager: PluginManager)
|
|||
const plugins: (PluginDescriptionType & { main: string; load: () => Promise<any>; })[] = [
|
||||
{ ...pcsx2, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2') },
|
||||
{ ...ppsspp, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp') },
|
||||
{ ...dolphin, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin') },
|
||||
{ ...romm, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.romm/romm') },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { EmulatorPackageType } from "@/shared/constants";
|
||||
import { EmulatorPackageType, ScoopPackageSchema } from "@/shared/constants";
|
||||
import { emulatorsDb, plugins } from "../../app";
|
||||
import * as emulatorSchema from '@schema/emulators';
|
||||
import { findExecs } from "../../games/services/launchGameService";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getOrCached } from "../../cache";
|
||||
|
||||
export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageType, gameCount: number, systems: EmulatorSystem[])
|
||||
{
|
||||
|
|
@ -21,16 +22,32 @@ export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageT
|
|||
systems,
|
||||
gameCount,
|
||||
validSources: execPaths,
|
||||
integration: findEmulatorPluginIntegration(emulator.name)
|
||||
integration: findEmulatorPluginIntegration(emulator.name, execPaths)
|
||||
};
|
||||
|
||||
return em;
|
||||
}
|
||||
|
||||
export function findEmulatorPluginIntegration (name: string)
|
||||
export function findEmulatorPluginIntegration (name: string, validSources: (EmulatorSourceEntryType | undefined)[])
|
||||
{
|
||||
const lowerCaseName = name.toLowerCase();
|
||||
const integration = Object.entries(plugins.plugins).find(p => p[1].description.keywords?.includes(lowerCaseName));
|
||||
if (!integration) return undefined;
|
||||
return { name: integration[0], version: integration[1].description.version };
|
||||
const hasSupport = validSources.concat(undefined).map(s => plugins.hooks.games.emulatorLaunchSupport.call({ emulator: name, source: s })).filter(s => !!s);
|
||||
|
||||
if (hasSupport.length <= 0) return undefined;
|
||||
return { name: hasSupport[0].id, version: plugins.plugins[hasSupport[0].id]?.description.version, possible: hasSupport.some(s => s.possible) };
|
||||
}
|
||||
|
||||
export async function getScoopPackage (id: string, url: string)
|
||||
{
|
||||
const data = await getOrCached(`scoop-dl-${id}`, async () =>
|
||||
{
|
||||
const res = await fetch(url);
|
||||
if (res.ok)
|
||||
{
|
||||
return ScoopPackageSchema.parseAsync(await res.json());
|
||||
}
|
||||
console.error(res.statusText);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -101,14 +101,7 @@ export async function getAllStoreEmulatorPackages ()
|
|||
const emulators = await fs.readdir(emulatorsBucket);
|
||||
const emulatorsRawData = await Promise.all(emulators.map(e => fs.readFile(path.join(emulatorsBucket, e), 'utf-8')));
|
||||
|
||||
const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.safeParse(JSON.parse(d))).filter(e =>
|
||||
{
|
||||
if (e.error)
|
||||
{
|
||||
console.error(e.error);
|
||||
}
|
||||
return e.data;
|
||||
}).map(e => e.data!);
|
||||
const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.parse(JSON.parse(d)));
|
||||
|
||||
return emulatesParsed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export const store = new Elysia({ prefix: '/api/store' })
|
|||
sources: execPaths,
|
||||
biosRequirement: emulatorPackage.bios,
|
||||
bios: biosFiles,
|
||||
integration: findEmulatorPluginIntegration(emulatorPackage.name)
|
||||
integration: findEmulatorPluginIntegration(emulatorPackage.name, execPaths)
|
||||
};
|
||||
|
||||
return emulator;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue