parent
04d5856f7d
commit
09b8b9c6f8
20 changed files with 351 additions and 231 deletions
|
|
@ -389,7 +389,7 @@ export default new Elysia()
|
||||||
if (validCommand)
|
if (validCommand)
|
||||||
{
|
{
|
||||||
// launch command waits for the game to exit, we don't want that.
|
// 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 };
|
return { type: 'application', command: null };
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,20 @@ import { existsSync, readFileSync } from 'node:fs';
|
||||||
import * as schema from '@schema/emulators';
|
import * as schema from '@schema/emulators';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { config, customEmulators, emulatorsDb, taskQueue } from '../../app';
|
import { config, customEmulators, emulatorsDb, taskQueue } from '../../app';
|
||||||
import os, { platform } from 'node:os';
|
import os from 'node:os';
|
||||||
import { cores } from '../../emulatorjs/emulatorjs';
|
import { cores } from '../../emulatorjs/emulatorjs';
|
||||||
import { LaunchGameJob } from '../../jobs/launch-game-job';
|
import { LaunchGameJob } from '../../jobs/launch-game-job';
|
||||||
import { EmulatorPackageType } from '@/shared/constants';
|
import { getStoreEmulatorPackage } from '../../store/services/gamesService';
|
||||||
import { getStoreEmulatorPackage, getStoreFolder } from '../../store/services/gamesService';
|
|
||||||
import { getOrCached } from '../../cache';
|
|
||||||
import { getOrCachedScoopPackage } from '../../store/services/emulatorsService';
|
import { getOrCachedScoopPackage } from '../../store/services/emulatorsService';
|
||||||
|
|
||||||
export const varRegex = /%([^%]+)%/g;
|
export const varRegex = /%([^%]+)%/g;
|
||||||
export const assignRegex = /(%\w+%)=(\S+) /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))
|
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));
|
taskQueue.enqueue(LaunchGameJob.id, new LaunchGameJob(id, validCommand, source, sourceId));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { RPC_URL, } from "@shared/constants";
|
import { RPC_URL, } from "@shared/constants";
|
||||||
import { config, customEmulators, db, emulatorsDb, plugins, taskQueue } from "../../app";
|
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 * as emulatorSchema from '@schema/emulators';
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { getErrorMessage, hashFile } from "@/bun/utils";
|
import { getErrorMessage, hashFile } from "@/bun/utils";
|
||||||
|
|
@ -26,7 +26,7 @@ class CommandSearchError extends Error
|
||||||
export async function getLocalGame (source: string, id: string)
|
export async function getLocalGame (source: string, id: string)
|
||||||
{
|
{
|
||||||
const localGame = await db.query.games.findFirst({
|
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),
|
where: getLocalGameMatch(id, source),
|
||||||
with: {
|
with: {
|
||||||
platform: { columns: { slug: true } }
|
platform: { columns: { slug: true } }
|
||||||
|
|
@ -36,8 +36,27 @@ export async function getLocalGame (source: string, id: string)
|
||||||
return localGame;
|
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);
|
const localGame = await getLocalGame(source, id);
|
||||||
if (localGame)
|
if (localGame)
|
||||||
{
|
{
|
||||||
|
|
@ -70,7 +89,7 @@ export async function getValidLaunchCommandsForGame (source: string, id: string)
|
||||||
const validCommand = commands.find(c => c.valid);
|
const validCommand = commands.find(c => c.valid);
|
||||||
if (validCommand)
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
import { EmulatorDownloadInfoType, EmulatorPackageType } from "@/shared/constants";
|
import { EmulatorDownloadInfoType, EmulatorPackageType } from "@/shared/constants";
|
||||||
import { AsyncSeriesBailHook, AsyncSeriesHook } from "tapable";
|
import { AsyncSeriesBailHook, AsyncSeriesHook } from "tapable";
|
||||||
|
import { any } from "zod";
|
||||||
|
|
||||||
|
interface EmulatorPostInstallContext
|
||||||
|
{
|
||||||
|
emulator: string;
|
||||||
|
emulatorPackage?: EmulatorPackageType;
|
||||||
|
path: string;
|
||||||
|
update: boolean;
|
||||||
|
info: EmulatorDownloadInfoType;
|
||||||
|
}
|
||||||
|
|
||||||
export class EmulatorHooks
|
export class EmulatorHooks
|
||||||
{
|
{
|
||||||
|
|
@ -12,11 +22,24 @@ export class EmulatorHooks
|
||||||
/**
|
/**
|
||||||
* Triggered when emulator is downloaded or updated
|
* Triggered when emulator is downloaded or updated
|
||||||
*/
|
*/
|
||||||
emulatorPostInstall = new AsyncSeriesHook<[ctx: {
|
emulatorPostInstall = new AsyncSeriesHook<[ctx: EmulatorPostInstallContext], { emulator: string; }>(['ctx']);
|
||||||
emulator: string;
|
|
||||||
emulatorPackage?: EmulatorPackageType;
|
constructor()
|
||||||
path: string;
|
{
|
||||||
update: boolean;
|
this.emulatorPostInstall.intercept({
|
||||||
info: EmulatorDownloadInfoType;
|
register (tap)
|
||||||
}]>(['ctx']);
|
{
|
||||||
|
return {
|
||||||
|
...tap,
|
||||||
|
fn: async (ctx: EmulatorPostInstallContext, ...rest: any[]) =>
|
||||||
|
{
|
||||||
|
if (ctx.emulator === tap.emulator)
|
||||||
|
{
|
||||||
|
tap.fn(ctx, ...rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,10 +15,11 @@ export class GameHooks
|
||||||
autoValidCommand: CommandEntry;
|
autoValidCommand: CommandEntry;
|
||||||
dryRun: boolean,
|
dryRun: boolean,
|
||||||
game: {
|
game: {
|
||||||
source: string;
|
source?: string;
|
||||||
id: number;
|
sourceId?: string;
|
||||||
|
id: FrontEndId;
|
||||||
};
|
};
|
||||||
}], string[] | undefined>(['ctx']);
|
}], string[] | undefined, { emulator: string; }>(['ctx']);
|
||||||
/**
|
/**
|
||||||
* Is the given emulator for the given command supported
|
* 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.
|
* @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: {
|
emulatorLaunchSupport = new SyncBailHook<[ctx: {
|
||||||
emulator: string;
|
emulator: string;
|
||||||
source?: EmulatorSourceEntryType;
|
source?: EmulatorSourceEntryType;
|
||||||
}], EmulatorSupport | undefined>(['ctx']);
|
}], EmulatorSupport | undefined, { emulator: string; }>(['ctx']);
|
||||||
/**
|
/**
|
||||||
* Fetches and returns a list of games converted to frontend.
|
* Fetches and returns a list of games converted to frontend.
|
||||||
* @param ctx.localGameIds This is local game ids in the format '<source>@<sourceId>'
|
* @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"]);
|
updatePlayed = new AsyncSeriesWaterfallHook<[ctx: { source: string, id: string; }], boolean>(["ctx"]);
|
||||||
fetchCollections = new AsyncSeriesHook<[ctx: { collections: FrontEndCollection[]; }]>(['ctx']);
|
fetchCollections = new AsyncSeriesHook<[ctx: { collections: FrontEndCollection[]; }]>(['ctx']);
|
||||||
fetchCollection = new AsyncSeriesBailHook<[ctx: { source: string, id: string; }], FrontEndCollection | undefined>(['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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +32,7 @@ function registerJob<
|
||||||
data: _job.dataSchema
|
data: _job.dataSchema
|
||||||
}),
|
}),
|
||||||
z.object({ type: z.literal(['completed', 'ended']), 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() })
|
z.object({ type: z.literal('error'), error: z.string() })
|
||||||
]),
|
]),
|
||||||
open (ws)
|
open (ws)
|
||||||
|
|
@ -41,6 +42,9 @@ function registerJob<
|
||||||
if (job)
|
if (job)
|
||||||
{
|
{
|
||||||
ws.send({ type: 'data', state: job.state, progress: job.progress, data: job.job.exposeData?.() });
|
ws.send({ type: 'data', state: job.state, progress: job.progress, data: job.job.exposeData?.() });
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
ws.send({ type: 'waiting' });
|
||||||
}
|
}
|
||||||
|
|
||||||
(ws.data as any).cleanup = [
|
(ws.data as any).cleanup = [
|
||||||
|
|
@ -97,10 +101,10 @@ function registerJob<
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jobs = new Elysia({ prefix: '/api/jobs' })
|
export const jobs = new Elysia({ prefix: '/api/jobs' })
|
||||||
|
.use(registerJob(LaunchGameJob))
|
||||||
.use(registerJob(LoginJob))
|
.use(registerJob(LoginJob))
|
||||||
.use(registerJob(TwitchLoginJob))
|
.use(registerJob(TwitchLoginJob))
|
||||||
.use(registerJob(UpdateStoreJob))
|
.use(registerJob(UpdateStoreJob))
|
||||||
.use(registerJob(LaunchGameJob))
|
|
||||||
.use(registerJob(BiosDownloadJob))
|
.use(registerJob(BiosDownloadJob))
|
||||||
.use(registerJob(InstallJob))
|
.use(registerJob(InstallJob))
|
||||||
.use(registerJob(EmulatorDownloadJob));
|
.use(registerJob(EmulatorDownloadJob));
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,51 @@ import { ActiveGameSchema, ActiveGameType } from "@/bun/types/typesc.schema";
|
||||||
import { db, events, plugins } from "../app";
|
import { db, events, plugins } from "../app";
|
||||||
import * as appSchema from "@schema/app";
|
import * as appSchema from "@schema/app";
|
||||||
import { eq, sql } from "drizzle-orm";
|
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">
|
export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSchema>, "playing">
|
||||||
{
|
{
|
||||||
static id = "launch-game" as const;
|
static id = "launch-game" as const;
|
||||||
static dataSchema = z.optional(ActiveGameSchema);
|
static dataSchema = z.nullable(ActiveGameSchema);
|
||||||
group = "launch-game";
|
group = "launch-game";
|
||||||
activeGame?: ActiveGameType;
|
activeGame: ActiveGameType | null;
|
||||||
gameId: number;
|
gameId: FrontEndId;
|
||||||
validCommand: CommandEntry;
|
validCommand: CommandEntry;
|
||||||
gameSource: string;
|
gameSource?: string;
|
||||||
gameSourceId: 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.gameId = gameId;
|
||||||
this.validCommand = validCommand;
|
this.validCommand = validCommand;
|
||||||
this.gameSource = source;
|
this.gameSource = source;
|
||||||
this.gameSourceId = sourceId;
|
this.gameSourceId = sourceId;
|
||||||
|
this.activeGame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start (context: JobContext<IJob<z.infer<typeof LaunchGameJob.dataSchema>, "playing">, z.infer<typeof LaunchGameJob.dataSchema>, "playing">)
|
async start (context: JobContext<IJob<z.infer<typeof LaunchGameJob.dataSchema>, "playing">, z.infer<typeof LaunchGameJob.dataSchema>, "playing">)
|
||||||
{
|
{
|
||||||
const localGame = await db.query.games.findFirst({
|
let gameInfo: { name?: string, source_id?: string, source?: string; };
|
||||||
where: eq(appSchema.games.id, this.gameId), columns: {
|
if (this.gameId.source === 'emulator')
|
||||||
name: true,
|
{
|
||||||
source_id: true,
|
gameInfo = { name: this.gameId.id };
|
||||||
source: true
|
} 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({
|
const commandArgs = await plugins.hooks.games.emulatorLaunch.promise({
|
||||||
autoValidCommand: this.validCommand,
|
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) =>
|
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
|
// We have full control over launching integrated emulators better to use bun spawn
|
||||||
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], {
|
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], {
|
||||||
cwd: this.validCommand.startDir,
|
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);
|
console.error(e);
|
||||||
reject(e);
|
reject(e);
|
||||||
|
|
@ -87,28 +103,27 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
|
||||||
|
|
||||||
this.activeGame = {
|
this.activeGame = {
|
||||||
process: game,
|
process: game,
|
||||||
name: localGame?.name ?? "Unknown",
|
name: gameInfo?.name ?? "Unknown",
|
||||||
gameId: this.gameId,
|
gameId: this.gameId,
|
||||||
|
source: this.gameSource,
|
||||||
|
sourceId: this.gameSourceId,
|
||||||
command: this.validCommand
|
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));
|
if (this.gameId.source === 'local')
|
||||||
await plugins.hooks.games.updatePlayed.promise({ source, id }).then(v =>
|
{
|
||||||
|
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 (v) events.emit('notification', { message: "Updated Last Played", type: 'success' });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.gameSource !== 'local')
|
updatePlayed(this.gameId, this.gameSource, this.gameSourceId);
|
||||||
{
|
|
||||||
updatePlayed(this.gameSource, this.gameSourceId);
|
|
||||||
}
|
|
||||||
else if (localGame?.source && localGame?.source !== 'local' && localGame.source_id)
|
|
||||||
{
|
|
||||||
updatePlayed(localGame.source, localGame.source_id);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Old spawn lanching, cases issues, needs to be ran as shell
|
/* Old spawn lanching, cases issues, needs to be ran as shell
|
||||||
|
|
|
||||||
|
|
@ -6,37 +6,48 @@ import desc from './package.json';
|
||||||
|
|
||||||
export default class DOLPHINIntegration implements PluginType
|
export default class DOLPHINIntegration implements PluginType
|
||||||
{
|
{
|
||||||
|
emulator = 'DOLPHIN';
|
||||||
|
|
||||||
|
|
||||||
load (ctx: PluginContextType)
|
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"), "");
|
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"];
|
args.push("--batch");
|
||||||
|
args.push(`--exec=${ctx.autoValidCommand.metadata.romPath}`);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,72 +9,73 @@ import desc from './package.json';
|
||||||
|
|
||||||
export default class PCSX2Integration implements PluginType
|
export default class PCSX2Integration implements PluginType
|
||||||
{
|
{
|
||||||
|
emulator = "PCSX2";
|
||||||
|
|
||||||
load (ctx: PluginContextType)
|
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')
|
if (ctx.source?.type === 'store')
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
id: desc.name,
|
id: desc.name,
|
||||||
supportLevel: "full",
|
supportLevel: "full",
|
||||||
capabilities: [...baseCapabilities, "resolution", "config"]
|
capabilities: [...baseCapabilities, "resolution", "config"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] };
|
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"];
|
args.push(ctx.autoValidCommand.metadata.romPath);
|
||||||
if (config.get('launchInFullscreen'))
|
args.push("-batch");
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,85 +12,86 @@ import { homedir } from "node:os";
|
||||||
|
|
||||||
export default class PCSX2Integration implements PluginType
|
export default class PCSX2Integration implements PluginType
|
||||||
{
|
{
|
||||||
|
emulator = "PPSSPP";
|
||||||
|
|
||||||
load (ctx: PluginContextType)
|
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"];
|
return {
|
||||||
|
id: desc.name,
|
||||||
if (ctx.source?.type === 'store')
|
supportLevel: "full",
|
||||||
{
|
capabilities: [...baseCapabilities, "resolution", "config"]
|
||||||
return {
|
};
|
||||||
id: desc.name,
|
}
|
||||||
supportLevel: "full",
|
else
|
||||||
capabilities: [...baseCapabilities, "resolution", "config"]
|
{
|
||||||
};
|
return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] };
|
||||||
}
|
|
||||||
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"];
|
args.push(ctx.autoValidCommand.metadata.romPath);
|
||||||
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("--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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,8 +147,7 @@ export const store = new Elysia({ prefix: '/api/store' })
|
||||||
biosRequirement: emulatorPackage.bios,
|
biosRequirement: emulatorPackage.bios,
|
||||||
bios: biosFiles,
|
bios: biosFiles,
|
||||||
integrations: findEmulatorPluginIntegration(emulatorPackage.name, execPaths),
|
integrations: findEmulatorPluginIntegration(emulatorPackage.name, execPaths),
|
||||||
storeDownloadInfo: storeDownloadInfo,
|
storeDownloadInfo: storeDownloadInfo
|
||||||
hasUpdate: storeDownloadInfo?.hasUpdate ?? null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return emulator;
|
return emulator;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ export type PluginDescriptionType = z.infer<typeof PluginDescriptionSchema>;
|
||||||
|
|
||||||
export const ActiveGameSchema = z.object({
|
export const ActiveGameSchema = z.object({
|
||||||
process: z.any().optional(),
|
process: z.any().optional(),
|
||||||
gameId: z.number(),
|
gameId: z.object({ id: z.string(), source: z.string() }),
|
||||||
|
source: z.string().optional(),
|
||||||
|
sourceId: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
command: z.object({ command: z.string(), startDir: z.string().optional() })
|
command: z.object({ command: z.string(), startDir: z.string().optional() })
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ function Error (data: ErrorComponentProps)
|
||||||
{
|
{
|
||||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" });
|
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" });
|
||||||
|
|
||||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
const router = useRouter();
|
||||||
|
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router) }]);
|
||||||
const { shortcuts } = useShortcutContext();
|
const { shortcuts } = useShortcutContext();
|
||||||
|
|
||||||
return <AnimatedBackground ref={ref} backgroundKey="game-details">
|
return <AnimatedBackground ref={ref} backgroundKey="game-details">
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
||||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
import { createFileRoute, useBlocker, useRouter } from '@tanstack/react-router';
|
||||||
import DotsLoading from '../components/backgrounds/dots';
|
import DotsLoading from '../components/backgrounds/dots';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||||
import { useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import Shortcuts from '../components/Shortcuts';
|
import Shortcuts from '../components/Shortcuts';
|
||||||
import { gameQuery } from '@queries/romm';
|
import { useJobStatus } from '../scripts/utils';
|
||||||
import { rommApi } from '../scripts/clientApi';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/launcher/$source/$id')({
|
export const Route = createFileRoute('/launcher/$source/$id')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
@ -18,34 +15,33 @@ function RouteComponent ()
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
function HandleGoBack ()
|
function HandleGoBack ()
|
||||||
{
|
{
|
||||||
router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true });
|
if (router.history.canGoBack())
|
||||||
|
{
|
||||||
|
router.history.back();
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { source, id } = Route.useParams();
|
const { source, id } = Route.useParams();
|
||||||
const { ref, focusKey } = useFocusable({ focusKey: `launching-${source}-${id}` });
|
const { ref, focusKey } = useFocusable({ focusKey: `launching-${source}-${id}` });
|
||||||
const { data } = useQuery(gameQuery(source, id));
|
|
||||||
|
|
||||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||||
const { shortcuts } = useShortcutContext();
|
const { shortcuts } = useShortcutContext();
|
||||||
|
|
||||||
useEffect(() =>
|
const { data } = useJobStatus('launch-game', {
|
||||||
{
|
onEnded (data)
|
||||||
if (!data) return;
|
|
||||||
const sub = rommApi.api.romm.status({ source: data.id.source })({ id: data.id.id }).subscribe();
|
|
||||||
|
|
||||||
sub.subscribe((e) =>
|
|
||||||
{
|
{
|
||||||
if (e.data.status !== 'playing')
|
HandleGoBack();
|
||||||
{
|
},
|
||||||
HandleGoBack();
|
onWaiting ()
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () =>
|
|
||||||
{
|
{
|
||||||
sub.close();
|
HandleGoBack();
|
||||||
};
|
},
|
||||||
}, [data?.id]);
|
});
|
||||||
|
|
||||||
|
useBlocker({ shouldBlockFn: () => !!data });
|
||||||
|
|
||||||
return <AnimatedBackground ref={ref} backgroundKey='game-details'>
|
return <AnimatedBackground ref={ref} backgroundKey='game-details'>
|
||||||
<div className='flex shadow-2xs shadow-black flex-col absolute w-screen h-screen overflow-hidden justify-center items-center gap-4'>
|
<div className='flex shadow-2xs shadow-black flex-col absolute w-screen h-screen overflow-hidden justify-center items-center gap-4'>
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ function SettingsMenu (data: {})
|
||||||
const { ref, focusKey } = useFocusable({
|
const { ref, focusKey } = useFocusable({
|
||||||
focusable: true,
|
focusable: true,
|
||||||
focusKey: 'settings-menu',
|
focusKey: 'settings-menu',
|
||||||
preferredChildFocusKey: location.hash.replaceAll(/#|(\?.+)/g, '')
|
preferredChildFocusKey: `menu-item-${location.hash.replaceAll(/#|(\?.+)/g, '')}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return <ul
|
return <ul
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import
|
||||||
useFocusable,
|
useFocusable,
|
||||||
FocusContext,
|
FocusContext,
|
||||||
} from "@noriginmedia/norigin-spatial-navigation";
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
import Shortcuts from "@/mainview/components/Shortcuts";
|
import Shortcuts from "@/mainview/components/Shortcuts";
|
||||||
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
|
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
|
||||||
import { systemApi } from "@/mainview/scripts/clientApi";
|
import { rommApi, systemApi } from "@/mainview/scripts/clientApi";
|
||||||
import { Button } from "@/mainview/components/options/Button";
|
import { Button } from "@/mainview/components/options/Button";
|
||||||
import { ChevronDown, CircleFadingArrowUp, Cpu, Download, Gamepad2, Info, Puzzle, Settings, Trash2, TriangleAlert, WandSparkles } from "lucide-react";
|
import { ChevronDown, CircleFadingArrowUp, Cpu, Download, Gamepad2, Info, Puzzle, Settings, Trash2, TriangleAlert, WandSparkles } from "lucide-react";
|
||||||
import { ContextList, DialogEntry, useContextDialog } from "@/mainview/components/ContextDialog";
|
import { ContextList, DialogEntry, useContextDialog } from "@/mainview/components/ContextDialog";
|
||||||
|
|
@ -62,6 +62,7 @@ function TitleArea (data: {
|
||||||
onUpdate: (source: string) => void;
|
onUpdate: (source: string) => void;
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
const navigation = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
...storeEmulatorDeleteMutation,
|
...storeEmulatorDeleteMutation,
|
||||||
|
|
@ -202,6 +203,15 @@ function TitleArea (data: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.push(...data.emulator.validSources.filter(s => s.exists).map(s => ({
|
||||||
|
content: `Launch: ${s.type}`, type: 'primary', icon: emulatorStatusIcons[s.type], action (ctx)
|
||||||
|
{
|
||||||
|
if (!data.emulator) return;
|
||||||
|
rommApi.api.romm.game({ source: 'emulator' })({ id: data.emulator.name }).play.post({ command_id: s.type });
|
||||||
|
navigation({ to: '/launcher/$source/$id', params: { source: 'emulator', id: data.emulator.name } });
|
||||||
|
}, id: `open-${s.type}`
|
||||||
|
} satisfies DialogEntry)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ref, focusKey, hasFocusedChild } = useFocusable({
|
const { ref, focusKey, hasFocusedChild } = useFocusable({
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { RefObject, useEffect, useRef, useState } from "react";
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
import { jobsApi } from "./clientApi";
|
import { jobsApi } from "./clientApi";
|
||||||
import { JobsAPIType } from "@/bun/api/rpc";
|
import { JobsAPIType } from "@/bun/api/rpc";
|
||||||
import { AnyRouter, Router, useRouter } from "@tanstack/react-router";
|
import { AnyRouter, useRouter } from "@tanstack/react-router";
|
||||||
import { soundMap } from "./audio/audio";
|
import { soundMap } from "./audio/audio";
|
||||||
|
|
||||||
export type ScrollSaveParams = {
|
export type ScrollSaveParams = {
|
||||||
|
|
@ -267,6 +267,7 @@ export function useJobStatus<const JOB extends keyof JobsAPIType['~Routes']['api
|
||||||
init?: {
|
init?: {
|
||||||
query?: Record<string, any>,
|
query?: Record<string, any>,
|
||||||
onProgress?: (process: number, data: ExtractField<JobResponse<JOB>, "data" | "started" | "progress" | "completed" | "ended", 'data'>) => void,
|
onProgress?: (process: number, data: ExtractField<JobResponse<JOB>, "data" | "started" | "progress" | "completed" | "ended", 'data'>) => void,
|
||||||
|
onWaiting?: () => void,
|
||||||
onEnded?: (data: ExtractField<JobResponse<JOB>, "completed" | "ended", 'data'>) => void;
|
onEnded?: (data: ExtractField<JobResponse<JOB>, "completed" | "ended", 'data'>) => void;
|
||||||
onCompleted?: (data: ExtractField<JobResponse<JOB>, "completed" | "ended", 'data'>) => void;
|
onCompleted?: (data: ExtractField<JobResponse<JOB>, "completed" | "ended", 'data'>) => void;
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
|
|
@ -306,6 +307,11 @@ export function useJobStatus<const JOB extends keyof JobsAPIType['~Routes']['api
|
||||||
setData(undefined);
|
setData(undefined);
|
||||||
init?.onCompleted?.(data.data as any);
|
init?.onCompleted?.(data.data as any);
|
||||||
break;
|
break;
|
||||||
|
case 'waiting':
|
||||||
|
setState(undefined);
|
||||||
|
setData(undefined);
|
||||||
|
init?.onWaiting?.();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
setData(data.data as DataPayload);
|
setData(data.data as DataPayload);
|
||||||
setState(data.state);
|
setState(data.state);
|
||||||
|
|
|
||||||
2
src/shared/types..d.ts
vendored
2
src/shared/types..d.ts
vendored
|
|
@ -127,7 +127,7 @@ declare interface CommandEntry
|
||||||
emulatorSource?: EmulatorSourceType;
|
emulatorSource?: EmulatorSourceType;
|
||||||
/** Metadata for the command */
|
/** Metadata for the command */
|
||||||
metadata: {
|
metadata: {
|
||||||
romPath: string;
|
romPath?: string;
|
||||||
emulatorBin?: string;
|
emulatorBin?: string;
|
||||||
/** The root directory of the emulator */
|
/** The root directory of the emulator */
|
||||||
emulatorDir?: string;
|
emulatorDir?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { expect, test, describe, beforeEach, afterAll, beforeAll, jest } from 'bun:test';
|
import { expect, test, describe, afterAll, beforeAll, jest } from 'bun:test';
|
||||||
import { client } from './client';
|
import { client } from './client';
|
||||||
import * as app from '@/bun/api/app';
|
import * as app from '@/bun/api/app';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { afterAll, beforeAll, beforeEach, afterEach } from 'bun:test';
|
import { beforeAll, beforeEach, afterEach } from 'bun:test';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import * as app from '@/bun/api/app';
|
import * as app from '@/bun/api/app';
|
||||||
import { remove } from 'fs-extra';
|
import { remove } from 'fs-extra';
|
||||||
import { spawnSync } from "child_process";
|
|
||||||
|
|
||||||
export async function LoadApp ()
|
export async function LoadApp ()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue