feat: Implemented emulator launching

Fixes #1
This commit is contained in:
Simeon Radivoev 2026-04-04 03:13:09 +03:00
parent 04d5856f7d
commit 09b8b9c6f8
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
20 changed files with 351 additions and 231 deletions

View file

@ -389,7 +389,7 @@ export default new Elysia()
if (validCommand)
{
// launch command waits for the game to exit, we don't want that.
await launchCommand(validCommand, source, id, validCommands.gameId);
await launchCommand(validCommand, validCommands.gameId, validCommands.source, validCommands.sourceId);
return { type: 'application', command: null };
} else
{

View file

@ -5,22 +5,20 @@ import { existsSync, readFileSync } from 'node:fs';
import * as schema from '@schema/emulators';
import { eq } from 'drizzle-orm';
import { config, customEmulators, emulatorsDb, taskQueue } from '../../app';
import os, { platform } from 'node:os';
import os from 'node:os';
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 { getStoreEmulatorPackage } from '../../store/services/gamesService';
import { getOrCachedScoopPackage } from '../../store/services/emulatorsService';
export const varRegex = /%([^%]+)%/g;
export const assignRegex = /(%\w+%)=(\S+) /g;
export async function launchCommand (validCommand: CommandEntry, source: string, sourceId: string, id: number)
export async function launchCommand (validCommand: CommandEntry, id: FrontEndId, source?: string, sourceId?: string)
{
if (taskQueue.hasActiveOfType(LaunchGameJob))
{
throw new Error(`${id} currently running`);
throw new Error(`Game currently running`);
}
taskQueue.enqueue(LaunchGameJob.id, new LaunchGameJob(id, validCommand, source, sourceId));

View file

@ -1,6 +1,6 @@
import { RPC_URL, } from "@shared/constants";
import { config, customEmulators, db, emulatorsDb, plugins, taskQueue } from "../../app";
import { getValidLaunchCommands } from "./launchGameService";
import { findExecs, getValidLaunchCommands } from "./launchGameService";
import * as emulatorSchema from '@schema/emulators';
import { and, eq } from "drizzle-orm";
import { getErrorMessage, hashFile } from "@/bun/utils";
@ -26,7 +26,7 @@ class CommandSearchError extends Error
export async function getLocalGame (source: string, id: string)
{
const localGame = await db.query.games.findFirst({
columns: { id: true, path_fs: true },
columns: { id: true, path_fs: true, source: true, source_id: true },
where: getLocalGameMatch(id, source),
with: {
platform: { columns: { slug: true } }
@ -36,8 +36,27 @@ export async function getLocalGame (source: string, id: string)
return localGame;
}
export async function getValidLaunchCommandsForGame (source: string, id: string)
export async function getValidLaunchCommandsForGame (source: string, id: string): Promise<{ commands: CommandEntry[], gameId: FrontEndId, source?: string, sourceId?: string; } | Error | undefined>
{
if (source === 'emulator')
{
const esEmulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, id) });
const allExecs = await findExecs(id, esEmulator);
return {
commands: allExecs.map(exec => ({
command: exec.binPath,
id: exec.type,
emulator: id,
emulatorSource: exec.type,
metadata: {
emulatorBin: exec.binPath,
emulatorDir: exec.rootPath
},
valid: true
} satisfies CommandEntry)),
gameId: { source: "emulator", id: id }
};
}
const localGame = await getLocalGame(source, id);
if (localGame)
{
@ -70,7 +89,7 @@ export async function getValidLaunchCommandsForGame (source: string, id: string)
const validCommand = commands.find(c => c.valid);
if (validCommand)
{
return { commands: commands.filter(c => c.valid), gameId: localGame.id, source: source, sourceId: id };
return { commands: commands.filter(c => c.valid), gameId: { id: String(localGame.id), source: 'local' }, source: localGame.source ?? source, sourceId: String(localGame.source_id) ?? id };
}
else
{

View file

@ -1,5 +1,15 @@
import { EmulatorDownloadInfoType, EmulatorPackageType } from "@/shared/constants";
import { AsyncSeriesBailHook, AsyncSeriesHook } from "tapable";
import { any } from "zod";
interface EmulatorPostInstallContext
{
emulator: string;
emulatorPackage?: EmulatorPackageType;
path: string;
update: boolean;
info: EmulatorDownloadInfoType;
}
export class EmulatorHooks
{
@ -12,11 +22,24 @@ export class EmulatorHooks
/**
* Triggered when emulator is downloaded or updated
*/
emulatorPostInstall = new AsyncSeriesHook<[ctx: {
emulator: string;
emulatorPackage?: EmulatorPackageType;
path: string;
update: boolean;
info: EmulatorDownloadInfoType;
}]>(['ctx']);
emulatorPostInstall = new AsyncSeriesHook<[ctx: EmulatorPostInstallContext], { emulator: string; }>(['ctx']);
constructor()
{
this.emulatorPostInstall.intercept({
register (tap)
{
return {
...tap,
fn: async (ctx: EmulatorPostInstallContext, ...rest: any[]) =>
{
if (ctx.emulator === tap.emulator)
{
tap.fn(ctx, ...rest);
}
}
};
},
});
}
}

View file

@ -15,10 +15,11 @@ export class GameHooks
autoValidCommand: CommandEntry;
dryRun: boolean,
game: {
source: string;
id: number;
source?: string;
sourceId?: string;
id: FrontEndId;
};
}], string[] | undefined>(['ctx']);
}], string[] | undefined, { emulator: string; }>(['ctx']);
/**
* Is the given emulator for the given command supported
* @returns The current support level. Partial means it can affect some functionality. Full means fully integrated for example with portable ones where you can control all aspects.
@ -27,7 +28,7 @@ export class GameHooks
emulatorLaunchSupport = new SyncBailHook<[ctx: {
emulator: string;
source?: EmulatorSourceEntryType;
}], EmulatorSupport | undefined>(['ctx']);
}], EmulatorSupport | undefined, { emulator: string; }>(['ctx']);
/**
* Fetches and returns a list of games converted to frontend.
* @param ctx.localGameIds This is local game ids in the format '<source>@<sourceId>'
@ -71,4 +72,38 @@ export class GameHooks
updatePlayed = new AsyncSeriesWaterfallHook<[ctx: { source: string, id: string; }], boolean>(["ctx"]);
fetchCollections = new AsyncSeriesHook<[ctx: { collections: FrontEndCollection[]; }]>(['ctx']);
fetchCollection = new AsyncSeriesBailHook<[ctx: { source: string, id: string; }], FrontEndCollection | undefined>(['ctx']);
constructor()
{
this.emulatorLaunchSupport.intercept({
register (tap)
{
return {
...tap,
fn: (e: any, ...rest: any[]) =>
{
if (e.emulator === tap.emulator)
{
return tap.fn(e, ...rest);
}
}
};
},
});
this.emulatorLaunch.intercept({
register (tap)
{
return {
...tap,
fn: async (e: any, ...rest: any[]) =>
{
if ((e.autoValidCommand as CommandEntry).emulator === tap.emulator)
{
return tap.fn(e, ...rest);
}
}
};
},
});
}
}

View file

@ -32,6 +32,7 @@ function registerJob<
data: _job.dataSchema
}),
z.object({ type: z.literal(['completed', 'ended']), data: _job.dataSchema }),
z.object({ type: z.literal('waiting') }),
z.object({ type: z.literal('error'), error: z.string() })
]),
open (ws)
@ -41,6 +42,9 @@ function registerJob<
if (job)
{
ws.send({ type: 'data', state: job.state, progress: job.progress, data: job.job.exposeData?.() });
} else
{
ws.send({ type: 'waiting' });
}
(ws.data as any).cleanup = [
@ -97,10 +101,10 @@ function registerJob<
}
export const jobs = new Elysia({ prefix: '/api/jobs' })
.use(registerJob(LaunchGameJob))
.use(registerJob(LoginJob))
.use(registerJob(TwitchLoginJob))
.use(registerJob(UpdateStoreJob))
.use(registerJob(LaunchGameJob))
.use(registerJob(BiosDownloadJob))
.use(registerJob(InstallJob))
.use(registerJob(EmulatorDownloadJob));

View file

@ -4,40 +4,51 @@ import { ActiveGameSchema, ActiveGameType } from "@/bun/types/typesc.schema";
import { db, events, plugins } from "../app";
import * as appSchema from "@schema/app";
import { eq, sql } from "drizzle-orm";
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
import { spawn } from 'node:child_process';
export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSchema>, "playing">
{
static id = "launch-game" as const;
static dataSchema = z.optional(ActiveGameSchema);
static dataSchema = z.nullable(ActiveGameSchema);
group = "launch-game";
activeGame?: ActiveGameType;
gameId: number;
activeGame: ActiveGameType | null;
gameId: FrontEndId;
validCommand: CommandEntry;
gameSource: string;
gameSourceId: string;
gameSource?: string;
gameSourceId?: string;
constructor(gameId: number, validCommand: CommandEntry, source: string, sourceId: string)
constructor(gameId: FrontEndId, validCommand: CommandEntry, source?: string, sourceId?: string)
{
this.gameId = gameId;
this.validCommand = validCommand;
this.gameSource = source;
this.gameSourceId = sourceId;
this.activeGame = null;
}
async start (context: JobContext<IJob<z.infer<typeof LaunchGameJob.dataSchema>, "playing">, z.infer<typeof LaunchGameJob.dataSchema>, "playing">)
{
const localGame = await db.query.games.findFirst({
where: eq(appSchema.games.id, this.gameId), columns: {
name: true,
source_id: true,
source: true
}
});
let gameInfo: { name?: string, source_id?: string, source?: string; };
if (this.gameId.source === 'emulator')
{
gameInfo = { name: this.gameId.id };
} else
{
const localGame = await db.query.games.findFirst({
where: eq(appSchema.games.id, Number(this.gameId.id)), columns: {
name: true,
source_id: true,
source: true
}
});
if (localGame)
gameInfo = { name: localGame.name ?? undefined, source_id: localGame.source_id ?? undefined, source: localGame.source ?? undefined };
}
const commandArgs = await plugins.hooks.games.emulatorLaunch.promise({
autoValidCommand: this.validCommand,
game: { source: this.gameSource, id: this.gameId }
game: { source: this.gameSource, sourceId: this.gameSourceId, id: this.gameId },
dryRun: false
});
await new Promise((resolve, reject) =>
@ -70,10 +81,15 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
// We have full control over launching integrated emulators better to use bun spawn
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], {
cwd: this.validCommand.startDir,
signal: context.abortSignal
signal: context.abortSignal,
});
bunGame.exited.then(resolve).catch(e =>
context.abortSignal.addEventListener('abort', reject);
bunGame.exited.then(e =>
{
resolve(true);
}).catch(e =>
{
console.error(e);
reject(e);
@ -87,28 +103,27 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
this.activeGame = {
process: game,
name: localGame?.name ?? "Unknown",
name: gameInfo?.name ?? "Unknown",
gameId: this.gameId,
source: this.gameSource,
sourceId: this.gameSourceId,
command: this.validCommand
};
const updatePlayed = async (source: string, id: string) =>
const updatePlayed = async (id: FrontEndId, source?: string, sourceId?: string) =>
{
await db.update(appSchema.games).set({ last_played: new Date() }).where(eq(appSchema.games.id, this.gameId));
await plugins.hooks.games.updatePlayed.promise({ source, id }).then(v =>
if (this.gameId.source === 'local')
{
await db.update(appSchema.games).set({ last_played: new Date() }).where(eq(appSchema.games.id, Number(this.gameId.id)));
}
await plugins.hooks.games.updatePlayed.promise({ source: source ?? id.source, id: sourceId ?? id.id }).then(v =>
{
if (v) events.emit('notification', { message: "Updated Last Played", type: 'success' });
});
};
if (this.gameSource !== 'local')
{
updatePlayed(this.gameSource, this.gameSourceId);
}
else if (localGame?.source && localGame?.source !== 'local' && localGame.source_id)
{
updatePlayed(localGame.source, localGame.source_id);
}
updatePlayed(this.gameId, this.gameSource, this.gameSourceId);
});
/* Old spawn lanching, cases issues, needs to be ran as shell

View file

@ -6,37 +6,48 @@ import desc from './package.json';
export default class DOLPHINIntegration implements PluginType
{
emulator = 'DOLPHIN';
load (ctx: PluginContextType)
{
ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) =>
ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) =>
{
if (ctx.emulator === 'DOLPHIN')
return { id: desc.name, supportLevel: "full", capabilities: ["batch", "config", "fullscreen", "resolution", "saves", "states"] };
return { id: desc.name, supportLevel: "full", capabilities: ["batch", "config", "fullscreen", "resolution", "saves", "states"] };
});
ctx.hooks.emulators.emulatorPostInstall.tapPromise(desc.name, async (ctx) =>
ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
{
await Bun.write(path.join(ctx.path, "portable.txt"), "");
});
ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) =>
ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
{
if (ctx.autoValidCommand.emulator === 'DOLPHIN' && ctx.autoValidCommand.metadata.emulatorDir)
const args: string[] = [];
const storageFolder = path.join(config.get('downloadPath'), "storage", 'DOLPHIN');
args.push(`--user=${storageFolder}`);
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`);
const savesPath = path.join(config.get('downloadPath'), "saves", 'DOLPHIN');
args.push(`--config=Dolphin.General.WiiSDCardPath=${path.join(savesPath, 'WiiSD.raw')}`);
args.push(`--config=Dolphin.General.WiiSDCardSyncFolder=${path.join(savesPath, 'WiiSDSync')}`);
args.push(`--config=Dolphin.GBA.SavesPath=${path.join(savesPath, 'GBA')}`);
if (ctx.autoValidCommand.metadata.romPath)
{
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;
args.push("--batch");
args.push(`--exec=${ctx.autoValidCommand.metadata.romPath}`);
}
return args;
});
}
}

View file

@ -9,72 +9,73 @@ import desc from './package.json';
export default class PCSX2Integration implements PluginType
{
emulator = "PCSX2";
load (ctx: PluginContextType)
{
ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) =>
ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) =>
{
if (ctx.emulator === 'PCSX2')
{
const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen", "saves", "states"];
const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen", "saves", "states"];
if (ctx.source?.type === 'store')
{
return {
id: desc.name,
supportLevel: "full",
capabilities: [...baseCapabilities, "resolution", "config"]
};
}
else
{
return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] };
}
if (ctx.source?.type === 'store')
{
return {
id: desc.name,
supportLevel: "full",
capabilities: [...baseCapabilities, "resolution", "config"]
};
}
else
{
return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] };
}
});
ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) =>
ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
{
if (ctx.autoValidCommand.emulator === 'PCSX2' && ctx.autoValidCommand.metadata.emulatorDir)
const args: string[] = [];
if (ctx.autoValidCommand.metadata.romPath)
{
const args = ["-batch"];
if (config.get('launchInFullscreen'))
{
args.push("-fullscreen");
}
args.push(...["-bigpicture", "-portable", "--", ctx.autoValidCommand.metadata.romPath]);
if (ctx.autoValidCommand.emulatorSource === 'store' && !ctx.dryRun)
{
const configFileContents = await Bun.file(configFile).text();
const biosFolder = path.join(config.get('downloadPath'), "bios", 'PCSX2');
const storageFolder = path.join(config.get('downloadPath'), "storage", 'PCSX2');
const savesFolder = path.join(config.get('downloadPath'), "saves", 'PCSX2');
const view = {
BIOS_PATH: biosFolder,
SNAPSHOTS_PATH: path.join(storageFolder, 'snaps'),
SAVE_STATES_PATH: path.join(savesFolder, 'states'),
MEMORY_CARDS_PATH: path.join(savesFolder, 'saves'),
CACHE_PATH: path.join(storageFolder, 'cache'),
COVERS_PATH: path.join(storageFolder, 'covers'),
TEXTURES_PATH: path.join(storageFolder, 'textures'),
RECURSIVE_PATHS: path.join(config.get('downloadPath'), 'roms', 'PS2'),
};
await Promise.all(Object.values(view).map(p => ensureDir(p)));
let pscx2Path = '';
if (process.platform === 'win32')
pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'inis');
else
pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, "PCSX2", 'inis');
await Bun.write(path.join(pscx2Path, 'PCSX2.ini'), Mustache.render(configFileContents, view));
}
return args;
args.push(ctx.autoValidCommand.metadata.romPath);
args.push("-batch");
}
if (config.get('launchInFullscreen'))
{
args.push("-fullscreen");
}
args.push(...["-bigpicture", "-portable", "--"]);
if (ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir && !ctx.dryRun)
{
const configFileContents = await Bun.file(configFile).text();
const biosFolder = path.join(config.get('downloadPath'), "bios", 'PCSX2');
const storageFolder = path.join(config.get('downloadPath'), "storage", 'PCSX2');
const savesFolder = path.join(config.get('downloadPath'), "saves", 'PCSX2');
const view = {
BIOS_PATH: biosFolder,
SNAPSHOTS_PATH: path.join(storageFolder, 'snaps'),
SAVE_STATES_PATH: path.join(savesFolder, 'states'),
MEMORY_CARDS_PATH: path.join(savesFolder, 'saves'),
CACHE_PATH: path.join(storageFolder, 'cache'),
COVERS_PATH: path.join(storageFolder, 'covers'),
TEXTURES_PATH: path.join(storageFolder, 'textures'),
RECURSIVE_PATHS: path.join(config.get('downloadPath'), 'roms', 'PS2'),
};
await Promise.all(Object.values(view).map(p => ensureDir(p)));
let pscx2Path = '';
if (process.platform === 'win32')
pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'inis');
else
pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, "PCSX2", 'inis');
await Bun.write(path.join(pscx2Path, 'PCSX2.ini'), Mustache.render(configFileContents, view));
}
return args;
});
}
}

View file

@ -12,85 +12,86 @@ import { homedir } from "node:os";
export default class PCSX2Integration implements PluginType
{
emulator = "PPSSPP";
load (ctx: PluginContextType)
{
ctx.hooks.games.emulatorLaunchSupport.tap(desc.name, (ctx) =>
ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) =>
{
if (ctx.emulator === 'PPSSPP')
const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen", "saves", "states"];
if (ctx.source?.type === 'store')
{
const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen", "saves", "states"];
if (ctx.source?.type === 'store')
{
return {
id: desc.name,
supportLevel: "full",
capabilities: [...baseCapabilities, "resolution", "config"]
};
}
else
{
return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] };
}
return {
id: desc.name,
supportLevel: "full",
capabilities: [...baseCapabilities, "resolution", "config"]
};
}
else
{
return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] };
}
});
ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) =>
ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
{
if (ctx.autoValidCommand.emulator === 'PPSSPP' && ctx.autoValidCommand.metadata.emulatorDir)
const args: string[] = [];
if (ctx.autoValidCommand.metadata.romPath)
{
const args = [ctx.autoValidCommand.metadata.romPath, "--escape-exit", "--pause-menu-exit"];
if (config.get('launchInFullscreen'))
{
args.push("--fullscreen");
}
if (ctx.autoValidCommand.emulatorSource === 'store' && !ctx.dryRun)
{
let confPath: string | undefined = undefined;
let controlsPath: string | undefined = undefined;
switch (process.platform)
{
case "win32":
confPath = configFilePathWin32;
controlsPath = configControlsFilePathWin32;
break;
case 'linux':
confPath = configFilePathLinux;
controlsPath = configControlsFilePathLinux;
break;
}
let ppssppPath = '';
if (process.platform === 'win32')
{
ppssppPath = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM');
} else
{
//TODO: Use way to set custom memstick path when they support it
ensureDir(path.join(homedir(), '.config', 'ppsspp'));
ppssppPath = path.join(homedir(), '.config', 'ppsspp', 'PSP', 'SYSTEM');
}
ensureDir(ppssppPath);
if (confPath)
{
const configFileContents = await Bun.file(confPath).text();
await Bun.write(path.join(ppssppPath, 'ppsspp.ini'), Mustache.render(configFileContents, {}));
}
if (controlsPath)
{
const controlsFileContents = await Bun.file(controlsPath).text();
await Bun.write(path.join(ppssppPath, 'controls.ini'), Mustache.render(controlsFileContents, {}));
}
}
return args;
args.push(ctx.autoValidCommand.metadata.romPath);
}
args.push("--escape-exit", "--pause-menu-exit");
if (config.get('launchInFullscreen'))
{
args.push("--fullscreen");
}
if (ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir && !ctx.dryRun)
{
let confPath: string | undefined = undefined;
let controlsPath: string | undefined = undefined;
switch (process.platform)
{
case "win32":
confPath = configFilePathWin32;
controlsPath = configControlsFilePathWin32;
break;
case 'linux':
confPath = configFilePathLinux;
controlsPath = configControlsFilePathLinux;
break;
}
let ppssppPath = '';
if (process.platform === 'win32')
{
ppssppPath = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM');
} else
{
//TODO: Use way to set custom memstick path when they support it
ensureDir(path.join(homedir(), '.config', 'ppsspp'));
ppssppPath = path.join(homedir(), '.config', 'ppsspp', 'PSP', 'SYSTEM');
}
ensureDir(ppssppPath);
if (confPath)
{
const configFileContents = await Bun.file(confPath).text();
await Bun.write(path.join(ppssppPath, 'ppsspp.ini'), Mustache.render(configFileContents, {}));
}
if (controlsPath)
{
const controlsFileContents = await Bun.file(controlsPath).text();
await Bun.write(path.join(ppssppPath, 'controls.ini'), Mustache.render(controlsFileContents, {}));
}
}
return args;
});
}
}

View file

@ -147,8 +147,7 @@ export const store = new Elysia({ prefix: '/api/store' })
biosRequirement: emulatorPackage.bios,
bios: biosFiles,
integrations: findEmulatorPluginIntegration(emulatorPackage.name, execPaths),
storeDownloadInfo: storeDownloadInfo,
hasUpdate: storeDownloadInfo?.hasUpdate ?? null
storeDownloadInfo: storeDownloadInfo
};
return emulator;