From 764691fc8610fafebc93a69ca24f74bcac42a898 Mon Sep 17 00:00:00 2001 From: Simeon Radivoev Date: Sun, 5 Apr 2026 12:46:50 +0300 Subject: [PATCH] fix: Made store downloads extract in their own folder feat: Implemented cemu integration --- src/bun/api/games/games.ts | 15 +-- .../api/games/services/launchGameService.ts | 97 +++++++++++++------ src/bun/api/jobs/install-job.ts | 5 +- src/bun/api/jobs/launch-game-job.ts | 9 +- .../com.simeonradivoev.gameflow.cemu/cemu.ts | 35 +++++++ .../package.json | 14 +++ .../dolphin.ts | 4 +- .../package.json | 2 +- .../pcsx2.ts | 8 +- .../com.simeonradivoev.gameflow.romm/romm.ts | 17 +++- src/mainview/emulatorjs/emulator.ts | 2 +- 11 files changed, 156 insertions(+), 52 deletions(-) create mode 100644 src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu.ts create mode 100644 src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json diff --git a/src/bun/api/games/games.ts b/src/bun/api/games/games.ts index 7c91954..1618c52 100644 --- a/src/bun/api/games/games.ts +++ b/src/bun/api/games/games.ts @@ -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() }) }) diff --git a/src/bun/api/games/services/launchGameService.ts b/src/bun/api/games/services/launchGameService.ts index 362ff41..4dde81b 100644 --- a/src/bun/api/games/services/launchGameService.ts +++ b/src/bun/api/games/services/launchGameService.ts @@ -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 { diff --git a/src/bun/api/jobs/install-job.ts b/src/bun/api/jobs/install-job.ts index a45fe7b..39c4687 100644 --- a/src/bun/api/jobs/install-job.ts +++ b/src/bun/api/jobs/install-job.ts @@ -70,7 +70,8 @@ export class InstallJob implements IJob 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 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, diff --git a/src/bun/api/jobs/launch-game-job.ts b/src/bun/api/jobs/launch-game-job.ts index 18b4fd1..c58119c 100644 --- a/src/bun/api/jobs/launch-game-job.ts +++ b/src/bun/api/jobs/launch-game-job.ts @@ -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, "playing"> { @@ -60,7 +61,9 @@ export class LaunchGameJob implements IJob console.log(data)); @@ -82,6 +85,8 @@ export class LaunchGameJob implements IJob + { + 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; + }); + } +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json new file mode 100644 index 0000000..bbabba6 --- /dev/null +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json @@ -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" + ] +} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts index 59cc505..d4ec3ca 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts @@ -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')}`); diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json index 07fe38d..146b910 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json @@ -8,7 +8,7 @@ "keywords": [ "integration", "emulator", - "wiiu", + "wii", "gc", "dolphin" ] diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts index 2e944d2..bd3b78f 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts @@ -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)); } diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts index 654fb2a..644a505 100644 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts @@ -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; diff --git a/src/mainview/emulatorjs/emulator.ts b/src/mainview/emulatorjs/emulator.ts index 61e570b..b5a730e 100644 --- a/src/mainview/emulatorjs/emulator.ts +++ b/src/mainview/emulatorjs/emulator.ts @@ -28,7 +28,7 @@ window.addEventListener('message', (e) => }); -window.EJS_threads = true; +window.EJS_threads = !__PUBLIC__; window.EJS_player = "#game"; window.EJS_lightgun = false; window.EJS_startOnLoaded = true;