fix: Made store downloads extract in their own folder
feat: Implemented cemu integration
This commit is contained in:
parent
09b8b9c6f8
commit
764691fc86
11 changed files with 156 additions and 52 deletions
|
|
@ -10,7 +10,7 @@ import path from "node:path";
|
|||
import { convertLocalToFrontend, convertStoreToFrontend, getLocalGameMatch, getSourceGameDetailed } from "./services/utils";
|
||||
import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/statusService";
|
||||
import { errorToResponse } from "elysia/adapter/bun/handler";
|
||||
import { getEmulatorsForSystem, launchCommand } from "./services/launchGameService";
|
||||
import { getEmulatorsForSystem, getRomFilePaths, launchCommand } from "./services/launchGameService";
|
||||
import { getErrorMessage, SeededRandom } from "@/bun/utils";
|
||||
import { defaultFormats, defaultPlugins } from 'jimp';
|
||||
import { createJimp } from "@jimp/core";
|
||||
|
|
@ -255,7 +255,8 @@ export default new Elysia()
|
|||
{
|
||||
const localGame = await db.query.games.findFirst({
|
||||
where: getLocalGameMatch(id, source),
|
||||
columns: { path_fs: true }
|
||||
columns: { path_fs: true },
|
||||
with: { platform: { columns: { es_slug: true } } }
|
||||
});
|
||||
|
||||
if (!localGame?.path_fs)
|
||||
|
|
@ -265,13 +266,15 @@ export default new Elysia()
|
|||
|
||||
const downloadPath = config.get('downloadPath');
|
||||
const path_fs = path.join(downloadPath, localGame.path_fs);
|
||||
const stats = await fs.stat(path_fs);
|
||||
if (stats.isDirectory())
|
||||
|
||||
const filesPaths = await getRomFilePaths(path_fs, localGame.platform.es_slug ?? undefined);
|
||||
|
||||
if (filesPaths.length <= 0)
|
||||
{
|
||||
return status("Not Found", "Rom is a folder");
|
||||
throw new Error("No Valid Roms Found");
|
||||
}
|
||||
|
||||
return Bun.file(path_fs);
|
||||
return Bun.file(filesPaths[0]);
|
||||
}, {
|
||||
params: z.object({ source: z.string(), id: z.string() })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -67,6 +67,70 @@ export async function getEmulatorsForSystem (systemSlug: string)
|
|||
return Array.from(emulators);
|
||||
}
|
||||
|
||||
export async function getRomFilePaths (gamePath: string, systemSlug?: string)
|
||||
{
|
||||
if (!existsSync(gamePath))
|
||||
{
|
||||
throw new Error(`Provided rom path is missing: '${gamePath}'`);
|
||||
}
|
||||
|
||||
const gamePathStat = await fs.stat(gamePath);
|
||||
const validFiles: string[] = [];
|
||||
|
||||
if (gamePathStat.isDirectory())
|
||||
{
|
||||
if (!systemSlug) throw new Error("Needs system to find valid file");
|
||||
|
||||
const system = await emulatorsDb.query.systems.findFirst({
|
||||
with: { commands: true },
|
||||
where: eq(schema.systems.name, systemSlug)
|
||||
});
|
||||
|
||||
if (!system)
|
||||
{
|
||||
throw new Error(`Could not find system '${systemSlug}'`);
|
||||
}
|
||||
|
||||
const extensionList = system.extension.join(',');
|
||||
|
||||
for await (const file of fs.glob(path.join(gamePath, `/**/*.{${extensionList}}`)))
|
||||
{
|
||||
validFiles.push(file);
|
||||
}
|
||||
|
||||
if (validFiles.length <= 0)
|
||||
{
|
||||
throw new Error(`Could not find valid rom file. Must be a file that ends in '${extensionList}'`);
|
||||
}
|
||||
} else if (systemSlug)
|
||||
{
|
||||
const system = await emulatorsDb.query.systems.findFirst({
|
||||
with: { commands: true },
|
||||
where: eq(schema.systems.name, systemSlug)
|
||||
});
|
||||
|
||||
if (!system)
|
||||
{
|
||||
throw new Error(`Could not find system '${systemSlug}'`);
|
||||
}
|
||||
|
||||
if (system.extension.some(e => gamePath.toLocaleLowerCase().endsWith(e.toLocaleLowerCase())))
|
||||
{
|
||||
validFiles.push(gamePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
const extensionList = system.extension.join(',');
|
||||
throw new Error(`Invalid Rom File. Must be a file that ends in '${extensionList}'`);
|
||||
}
|
||||
} else
|
||||
{
|
||||
validFiles.push(gamePath);
|
||||
}
|
||||
|
||||
return validFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data Uses es-de system slug
|
||||
|
|
@ -96,38 +160,7 @@ export async function getValidLaunchCommands (data: {
|
|||
const downloadPath = config.get('downloadPath');
|
||||
const gamePath = path.join(downloadPath, data.gamePath);
|
||||
|
||||
const validFiles: string[] = [];
|
||||
if (!existsSync(gamePath))
|
||||
{
|
||||
throw new Error(`Provided rom path is missing: '${gamePath}'`);
|
||||
}
|
||||
|
||||
const gamePathStat = await fs.stat(gamePath);
|
||||
|
||||
const extensionList = system.extension.join(',');
|
||||
|
||||
if (gamePathStat.isDirectory())
|
||||
{
|
||||
for await (const file of fs.glob(path.join(gamePath, `/**/*.{${extensionList}}`)))
|
||||
{
|
||||
validFiles.push(file);
|
||||
}
|
||||
|
||||
if (validFiles.length <= 0)
|
||||
{
|
||||
throw new Error(`Could not find valid rom file. Must be a file that ends in '${extensionList}'`);
|
||||
}
|
||||
} else
|
||||
{
|
||||
if (system.extension.some(e => gamePath.toLocaleLowerCase().endsWith(e.toLocaleLowerCase())))
|
||||
{
|
||||
validFiles.push(gamePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Error(`Invalid Rom File. Must be a file that ends in '${extensionList}'`);
|
||||
}
|
||||
}
|
||||
const validFiles: string[] = await getRomFilePaths(gamePath, data.systemSlug);
|
||||
|
||||
function escapeWindowsArg (arg: string): string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ export class InstallJob implements IJob<never, InstallJobStates>
|
|||
name: game.title,
|
||||
summary: game.description,
|
||||
system_slug: gameId.system,
|
||||
extract_path: path.join('roms', gameId.system),
|
||||
path_fs: path.join('roms', gameId.system, game.title),
|
||||
extract_path: path.join('roms', gameId.system, game.title),
|
||||
};
|
||||
|
||||
break;
|
||||
|
|
@ -218,7 +219,7 @@ export class InstallJob implements IJob<never, InstallJobStates>
|
|||
source_id: info.source_id,
|
||||
source: this.source,
|
||||
slug: info.slug,
|
||||
path_fs: info.path_fs,
|
||||
path_fs: info.path_fs ?? (info.extract_path ? path.join(downloadPath, info.extract_path) : undefined),
|
||||
last_played: info.last_played,
|
||||
platform_id: platformId,
|
||||
igdb_id: info.igdb_id,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import z from "zod";
|
||||
import { IJob, JobContext } from "../task-queue";
|
||||
import { ActiveGameSchema, ActiveGameType } from "@/bun/types/typesc.schema";
|
||||
import { db, events, plugins } from "../app";
|
||||
import { config, db, events, plugins } from "../app";
|
||||
import * as appSchema from "@schema/app";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from "node:path";
|
||||
|
||||
export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSchema>, "playing">
|
||||
{
|
||||
|
|
@ -60,7 +61,9 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
|
|||
const spawnGame = spawn(this.validCommand.command, {
|
||||
shell: true,
|
||||
cwd: this.validCommand.startDir,
|
||||
signal: context.abortSignal
|
||||
signal: context.abortSignal,
|
||||
env: {
|
||||
}
|
||||
});
|
||||
|
||||
spawnGame.stdout.on('data', data => console.log(data));
|
||||
|
|
@ -82,6 +85,8 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
|
|||
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], {
|
||||
cwd: this.validCommand.startDir,
|
||||
signal: context.abortSignal,
|
||||
env: {
|
||||
}
|
||||
});
|
||||
|
||||
context.abortSignal.addEventListener('abort', reject);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { PluginContextType, PluginType } from "@/bun/types/typesc.schema";
|
||||
import desc from './package.json';
|
||||
import path from 'node:path';
|
||||
import { config } from "@/bun/api/app";
|
||||
|
||||
export default class DOLPHINIntegration implements PluginType
|
||||
{
|
||||
emulator = 'CEMU';
|
||||
|
||||
load (ctx: PluginContextType)
|
||||
{
|
||||
ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) =>
|
||||
{
|
||||
return { id: desc.name, supportLevel: "full", capabilities: ["batch", "config", "fullscreen", "resolution", "saves", "states"] };
|
||||
});
|
||||
|
||||
ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
|
||||
{
|
||||
const args: string[] = [];
|
||||
|
||||
args.push(`--fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`);
|
||||
|
||||
const savesPath = path.join(config.get('downloadPath'), "saves", 'DOLPHIN');
|
||||
|
||||
args.push(`--mlc=${savesPath}`);
|
||||
|
||||
if (ctx.autoValidCommand.metadata.romPath)
|
||||
{
|
||||
args.push(`--game=${ctx.autoValidCommand.metadata.romPath}`);
|
||||
}
|
||||
|
||||
return args;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "com.simeonradivoev.gameflow.cemu",
|
||||
"displayName": "CEMU Integration",
|
||||
"version": "0.0.1",
|
||||
"description": "CEMU Emulator Integration",
|
||||
"main": "./cemu.ts",
|
||||
"icon": "https://upload.wikimedia.org/wikipedia/commons/9/9e/Cemu_Emulator_Official_Logo.png",
|
||||
"keywords": [
|
||||
"integration",
|
||||
"emulator",
|
||||
"wiiu",
|
||||
"cemu"
|
||||
]
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ export default class DOLPHINIntegration implements PluginType
|
|||
{
|
||||
const args: string[] = [];
|
||||
|
||||
const storageFolder = path.join(config.get('downloadPath'), "storage", 'DOLPHIN');
|
||||
const storageFolder = path.join(config.get('downloadPath'), "storage", this.emulator);
|
||||
args.push(`--user=${storageFolder}`);
|
||||
|
||||
args.push(`--config=Dolphin.Display.Fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`);
|
||||
|
|
@ -35,7 +35,7 @@ export default class DOLPHINIntegration implements PluginType
|
|||
args.push(`--config=Dolphin.Interface.SkipNKitWarning=True`);
|
||||
args.push(`--config=Dolphin.Analytics.PermissionAsked=True`);
|
||||
|
||||
const savesPath = path.join(config.get('downloadPath'), "saves", 'DOLPHIN');
|
||||
const savesPath = path.join(config.get('downloadPath'), "saves", this.emulator);
|
||||
|
||||
args.push(`--config=Dolphin.General.WiiSDCardPath=${path.join(savesPath, 'WiiSD.raw')}`);
|
||||
args.push(`--config=Dolphin.General.WiiSDCardSyncFolder=${path.join(savesPath, 'WiiSDSync')}`);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"keywords": [
|
||||
"integration",
|
||||
"emulator",
|
||||
"wiiu",
|
||||
"wii",
|
||||
"gc",
|
||||
"dolphin"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ export default class PCSX2Integration implements PluginType
|
|||
{
|
||||
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 biosFolder = path.join(config.get('downloadPath'), "bios", this.emulator);
|
||||
const storageFolder = path.join(config.get('downloadPath'), "storage", this.emulator);
|
||||
const savesFolder = path.join(config.get('downloadPath'), "saves", this.emulator);
|
||||
|
||||
const view = {
|
||||
BIOS_PATH: biosFolder,
|
||||
|
|
@ -70,7 +70,7 @@ export default class PCSX2Integration implements PluginType
|
|||
if (process.platform === 'win32')
|
||||
pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'inis');
|
||||
else
|
||||
pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, "PCSX2", 'inis');
|
||||
pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, this.emulator, 'inis');
|
||||
|
||||
await Bun.write(path.join(pscx2Path, 'PCSX2.ini'), Mustache.render(configFileContents, view));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,18 @@ export default class RommIntegration implements PluginType
|
|||
return file;
|
||||
}));
|
||||
|
||||
let extract_path: string | undefined = undefined;
|
||||
let path_fs = path.join(rom.fs_path, rom.fs_name);
|
||||
if (files.length === 1)
|
||||
{
|
||||
const name = files[0].file_name.toLocaleLowerCase();
|
||||
if (name.endsWith('.zip') || name.endsWith('.7z') || name.endsWith('.rar'))
|
||||
{
|
||||
extract_path = rom.name ?? path.parse(name).name;
|
||||
path_fs = path.join(rom.fs_path, extract_path);
|
||||
}
|
||||
}
|
||||
|
||||
const info: DownloadInfo = {
|
||||
platform: {
|
||||
slug: rommPlatform.slug,
|
||||
|
|
@ -204,13 +216,14 @@ export default class RommIntegration implements PluginType
|
|||
ra_id: rom.ra_id ?? undefined,
|
||||
summary: rom.summary ?? undefined,
|
||||
name: rom.name ?? "Unknown",
|
||||
path_fs: path.join(rom.fs_path, rom.fs_name),
|
||||
path_fs,
|
||||
source_id: String(rom.id),
|
||||
slug: rom.slug ?? undefined,
|
||||
system_slug: rommPlatform.slug,
|
||||
metadata: rom.metadatum,
|
||||
files,
|
||||
auth: await this.getAuthToken()
|
||||
auth: await this.getAuthToken(),
|
||||
extract_path
|
||||
};
|
||||
|
||||
return info;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue