fix: Issues with launching and installation on the steam deck
This commit is contained in:
parent
dc0f2d150a
commit
ccc5a05ed7
19 changed files with 247 additions and 80 deletions
|
|
@ -1,32 +1,38 @@
|
|||
// ./gamepad/index.ts
|
||||
|
||||
import { platform } from "os";
|
||||
import { GamepadWindows } from "./windows";
|
||||
import { GamepadLinux } from "./linux";
|
||||
|
||||
import type { IGamepadBackend, GamepadState } from "./types";
|
||||
|
||||
export class Gamepad
|
||||
{
|
||||
private backend: IGamepadBackend;
|
||||
private index: number;
|
||||
private backend: IGamepadBackend | undefined;
|
||||
|
||||
constructor(index = 0)
|
||||
{
|
||||
if (platform() === "win32")
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
async init ()
|
||||
{
|
||||
if (process.platform === "win32")
|
||||
{
|
||||
this.backend = new GamepadWindows(index);
|
||||
const { GamepadWindows } = await import("./windows");
|
||||
this.backend = new GamepadWindows(this.index);
|
||||
} else
|
||||
{
|
||||
this.backend = new GamepadLinux(index);
|
||||
const { GamepadLinux } = await import("./linux");
|
||||
this.backend = new GamepadLinux(this.index);
|
||||
}
|
||||
}
|
||||
|
||||
update (): GamepadState | null
|
||||
{
|
||||
return this.backend.update();
|
||||
return this.backend?.update() ?? null;
|
||||
}
|
||||
|
||||
close ()
|
||||
{
|
||||
this.backend.close?.();
|
||||
this.backend?.close?.();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import path from 'node:path';
|
||||
import { which } from 'bun';
|
||||
import { Glob, which } from 'bun';
|
||||
import fs from 'node:fs/promises';
|
||||
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 from 'node:os';
|
||||
import os, { platform } 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';
|
||||
|
||||
export const varRegex = /%([^%]+)%/g;
|
||||
export const assignRegex = /(%\w+%)=(\S+) /g;
|
||||
|
|
@ -129,10 +131,22 @@ export async function getValidLaunchCommands (data: {
|
|||
|
||||
function escapeWindowsArg (arg: string): string
|
||||
{
|
||||
return `"${arg
|
||||
.replace(/(\\*)"/g, '$1$1\\"') // escape quotes
|
||||
.replace(/(\\*)$/, '$1$1') // escape trailing backslashes
|
||||
}"`;
|
||||
if (process.platform === 'win32')
|
||||
{
|
||||
return `"${arg
|
||||
.replace(/(\\*)"/g, '$1$1\\"') // escape quotes
|
||||
.replace(/(\\*)$/, '$1$1') // escape trailing backslashes
|
||||
}"`;
|
||||
} else
|
||||
{
|
||||
if (arg.includes(' '))
|
||||
{
|
||||
return `"${arg}"`;
|
||||
} else
|
||||
{
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formattedCommands = await Promise.all(system.commands
|
||||
|
|
@ -196,7 +210,10 @@ export async function getValidLaunchCommands (data: {
|
|||
return [
|
||||
[value, validExec ? validExec.binPath : undefined] as [string, string | undefined],
|
||||
[`%EMUSOURCE%`, validExec?.type] as [string, string | undefined],
|
||||
['%EMUDIR%', validExec?.rootPath ?? (validExec ? escapeWindowsArg(path.dirname(validExec.binPath)) : undefined)] as [string, string | undefined]];
|
||||
['%EMUDIR%', validExec?.rootPath ?? (validExec ? escapeWindowsArg(path.dirname(validExec.binPath)) : undefined)] as [string, string | undefined],
|
||||
['%EMUDIRRAW%', validExec?.rootPath ?? (validExec ? path.dirname(validExec.binPath) : undefined)] as [string, string | undefined]
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
const key = value[0].substring(1, value.length - 1);
|
||||
|
|
@ -233,9 +250,9 @@ export async function getValidLaunchCommands (data: {
|
|||
valid: !invalid, emulator,
|
||||
emulatorSource: vars['%EMUSOURCE%'] as any,
|
||||
metadata: {
|
||||
romPath: staticVars['%ROM%'],
|
||||
romPath: validFiles[0],
|
||||
emulatorBin: varList.flatMap(l => l).find(v => v[0].includes('%EMULATOR_'))?.[1],
|
||||
emulatorDir: vars['%EMUDIR%']
|
||||
emulatorDir: vars['%EMUDIRRAW%']
|
||||
}
|
||||
} satisfies CommandEntry;
|
||||
}));
|
||||
|
|
@ -253,7 +270,7 @@ export async function findExecsByName (emulatorName: string)
|
|||
return findExecs(emulatorName, emulator);
|
||||
}
|
||||
|
||||
export function findStoreEmulatorExec (id: string, emulator?: { systempath: string[]; }): EmulatorSourceEntryType | undefined
|
||||
export async function findStoreEmulatorExec (id: string, emulator?: { systempath: string[]; }): Promise<EmulatorSourceEntryType | undefined>
|
||||
{
|
||||
const storeEmulatorFolder = path.join(config.get('downloadPath'), 'emulators', id);
|
||||
const storeExecName = emulator?.systempath.find(name => existsSync(path.join(storeEmulatorFolder, name)));
|
||||
|
|
@ -262,6 +279,30 @@ export function findStoreEmulatorExec (id: string, emulator?: { systempath: stri
|
|||
return { binPath: path.join(storeEmulatorFolder, storeExecName), rootPath: storeEmulatorFolder, exists: true, type: "store" };
|
||||
}
|
||||
|
||||
const storeEmulator = await getStoreEmulatorPackage(id);
|
||||
if (storeEmulator?.downloads)
|
||||
{
|
||||
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 files = (await fs.readdir(storeEmulatorFolder))
|
||||
.filter(f => glob.match(f));
|
||||
return files.map(f => path.join(storeEmulatorFolder, f));
|
||||
}
|
||||
return [];
|
||||
|
||||
}))).flatMap(f => f);
|
||||
|
||||
if (storeExecName.length > 0)
|
||||
{
|
||||
return { binPath: storeExecName[0], rootPath: storeEmulatorFolder, exists: true, type: 'store' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +317,7 @@ export async function findExecs (id: string, emulator?: { winregistrypath: strin
|
|||
|
||||
if (emulator && emulator.systempath.length > 0)
|
||||
{
|
||||
const storePath = findStoreEmulatorExec(id, emulator);
|
||||
const storePath = await findStoreEmulatorExec(id, emulator);
|
||||
if (storePath) execs.push(storePath);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export async function getValidLaunchCommandsForGame (source: string, id: string)
|
|||
{
|
||||
try
|
||||
{
|
||||
const commands = await getValidLaunchCommands({ systemSlug: esPlatform.system, customEmulatorConfig: customEmulators, gamePath: localGame.path_fs });
|
||||
const commands = await getValidLaunchCommands({ systemSlug: esPlatform.system, gamePath: localGame.path_fs });
|
||||
|
||||
if (cores[esPlatform.system])
|
||||
{
|
||||
|
|
@ -103,7 +103,8 @@ export default function buildStatusResponse ()
|
|||
response: z.discriminatedUnion('status', [
|
||||
z.object({ status: z.literal('error'), error: z.unknown() }),
|
||||
z.object({ status: z.literal('installed'), commands: z.array(z.any()), details: z.string().optional() }),
|
||||
z.object({ status: z.literal(['refresh', 'queued']) }),
|
||||
z.object({ status: z.literal('refresh'), localId: z.number().optional() }),
|
||||
z.object({ status: z.literal(['queued']) }),
|
||||
z.object({ status: z.literal('playing'), details: z.string() }),
|
||||
z.object({ status: z.literal('install'), details: z.string() }),
|
||||
z.object({ status: z.literal('present'), details: z.string() }),
|
||||
|
|
@ -241,7 +242,7 @@ export default function buildStatusResponse ()
|
|||
{
|
||||
if (data.id === installJobId)
|
||||
{
|
||||
ws.send({ status: 'refresh' });
|
||||
ws.send({ status: 'refresh', localId: (data.job.job as InstallJob).localGameId });
|
||||
} else if (data.job.job instanceof LaunchGameJob)
|
||||
{
|
||||
handleActiveExit({});
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ export async function checkFiles (files: DownloadFileEntry[], isArchive: boolean
|
|||
{
|
||||
// file is either zip or doesn't support sha checking
|
||||
if (!f.sha1 || isArchive) return { ...f, exists: false, matches: false } satisfies LocalDownloadFileEntry;
|
||||
const localPath = path.join(f.file_path, f.file_name);
|
||||
const localPath = path.join(config.get('downloadPath'), f.file_path, f.file_name);
|
||||
if (await fs.exists(localPath))
|
||||
{
|
||||
if (f.size && f.size !== (await fs.stat(localPath)).size)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { getOrCachedGithubRelease } from "../cache";
|
|||
import Seven from 'node-7z';
|
||||
import fs from "node:fs/promises";
|
||||
import { Downloader } from "@/bun/utils/downloader";
|
||||
import { move } from "fs-extra";
|
||||
import { ensureDir, move } from "fs-extra";
|
||||
import { simulateProgress } from "@/bun/utils";
|
||||
|
||||
type EmulatorDownloadStates = "download" | "extract";
|
||||
|
|
@ -82,7 +82,7 @@ export class EmulatorDownloadJob implements IJob<z.infer<typeof EmulatorDownload
|
|||
{
|
||||
if (isArchive)
|
||||
{
|
||||
if (await downloader.start() && destinationPaths[0])
|
||||
if (destinationPaths[0])
|
||||
{
|
||||
let destinationPath = destinationPaths[0];
|
||||
await new Promise((resolve, reject) =>
|
||||
|
|
@ -108,6 +108,13 @@ export class EmulatorDownloadJob implements IJob<z.infer<typeof EmulatorDownload
|
|||
}
|
||||
}
|
||||
}
|
||||
} else
|
||||
{
|
||||
await ensureDir(emulatorsFolder);
|
||||
for (const destPath of destinationPaths)
|
||||
{
|
||||
await fs.rename(destPath, path.join(emulatorsFolder, path.basename(destPath)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ export class InstallJob implements IJob<never, InstallJobStates>
|
|||
public gameId: string;
|
||||
public source: string;
|
||||
public config?: JobConfig;
|
||||
|
||||
// The local game ID of newly created entry, if successful
|
||||
public localGameId?: number;
|
||||
public group = InstallJob.id;
|
||||
|
||||
constructor(id: string, source: string, config?: JobConfig)
|
||||
|
|
@ -252,6 +253,7 @@ export class InstallJob implements IJob<never, InstallJobStates>
|
|||
})));
|
||||
}
|
||||
|
||||
this.localGameId = id;
|
||||
});
|
||||
} else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ 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 { killBrowser } from "@/bun/utils/browser-spawner";
|
||||
|
||||
export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSchema>, "playing">
|
||||
{
|
||||
|
|
@ -43,34 +42,44 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
|
|||
|
||||
await new Promise((resolve, reject) =>
|
||||
{
|
||||
let game: Bun.Subprocess;
|
||||
let game: any;
|
||||
if (!commandArgs)
|
||||
{
|
||||
game = Bun.spawn(this.validCommand.command.split(' '), {
|
||||
// ES-DE commands require shell execution. Some emulators fail otherwise.
|
||||
const spawnGame = spawn(this.validCommand.command, {
|
||||
shell: true,
|
||||
cwd: this.validCommand.startDir,
|
||||
windowsVerbatimArguments: true,
|
||||
signal: context.abortSignal
|
||||
});
|
||||
|
||||
game.exited.then(resolve).catch(e =>
|
||||
spawnGame.stdout.on('data', data => console.log(data));
|
||||
spawnGame.on('close', (code) =>
|
||||
{
|
||||
resolve(code);
|
||||
});
|
||||
spawnGame.on('error', e =>
|
||||
{
|
||||
console.error(e);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
game = spawnGame;
|
||||
}
|
||||
else if (this.validCommand.metadata.emulatorBin)
|
||||
{
|
||||
game = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], {
|
||||
// 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,
|
||||
windowsVerbatimArguments: true,
|
||||
signal: context.abortSignal
|
||||
});
|
||||
|
||||
game.exited.then(resolve).catch(e =>
|
||||
bunGame.exited.then(resolve).catch(e =>
|
||||
{
|
||||
console.error(e);
|
||||
reject(e);
|
||||
});
|
||||
game = bunGame;
|
||||
} else
|
||||
{
|
||||
reject(new Error("No Emulator Bin"));
|
||||
|
|
|
|||
|
|
@ -41,7 +41,13 @@ export default class PCSX2Integration implements PluginType
|
|||
|
||||
await Promise.all(Object.values(view).map(p => ensureDir(p)));
|
||||
|
||||
await Bun.write(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'inis', 'PCSX2.ini'), Mustache.render(configFileContents, view));
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import configControlsFilePathLinux from './linux/controls.ini' with { type: 'fil
|
|||
import path from "node:path";
|
||||
import Mustache from "mustache";
|
||||
import { ensureDir } from "fs-extra";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
export default class PCSX2Integration implements PluginType
|
||||
{
|
||||
|
|
@ -38,13 +39,29 @@ export default class PCSX2Integration implements PluginType
|
|||
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 configFileContents = await Bun.file(controlsPath).text();
|
||||
const controlsFileContents = await Bun.file(controlsPath).text();
|
||||
ensureDir(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM'));
|
||||
await Bun.write(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM', 'ppsspp.ini'), Mustache.render(configFileContents, {}));
|
||||
await Bun.write(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM', 'controls.ini'), Mustache.render(controlsFileContents, {}));
|
||||
await Bun.write(path.join(ppssppPath, 'controls.ini'), Mustache.render(controlsFileContents, {}));
|
||||
}
|
||||
|
||||
return args;
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ export default class RommIntegration implements PluginType
|
|||
const file: DownloadFileEntry = {
|
||||
url: new URL(`${config.get('rommAddress')}/api/romsfiles/${f.id}/content/${f.file_name}`),
|
||||
file_name: f.file_name,
|
||||
file_path: path.join(config.get('downloadPath'), f.file_path),
|
||||
file_path: f.file_path,
|
||||
size: f.file_size_bytes,
|
||||
sha1: f.sha1_hash ?? undefined
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue