feat: Added more ways to detect duplicates

feat: Added resolution and widescreen settings
feat: Added Xenia and Xemu integration
This commit is contained in:
Simeon Radivoev 2026-04-06 00:05:00 +03:00
parent 764691fc86
commit 05fafced07
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
25 changed files with 407 additions and 49 deletions

View file

@ -201,31 +201,68 @@ export default new Elysia()
.groupBy(schema.games.id)
.where(and(...where));
localGamesSet = new Set(localGames.filter(g => !!g.source_id && !!g.source).map(g => `${g.source}@${g.source_id}`));
localGamesSet = new Set(
localGames.filter(g => !!g.source_id && !!g.source).map(g => `${g.source}@${g.source_id}`)
.concat(localGames.filter(g => !!g.igdb_id).map(g => `igdb@${g.igdb_id}`))
);
if (!query.collection_id)
function localGameExistsPredicate (game: { id: FrontEndId, igdb_id?: number | null, ra_id?: number | null; })
{
if (localGamesSet?.has(`${game.id.source}@${game.id.id}`)) return true;
if (game.igdb_id && localGamesSet?.has(`igdb@${game.igdb_id}`)) return true;
if (game.ra_id && localGamesSet?.has(`ra@${game.ra_id}`)) return true;
return false;
}
if (query.collection_id)
{
// Collections are just a remote thing for now.
const remoteGames: FrontEndGameTypeWithIds[] = [];
await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e));
games.push(...remoteGames.map(g =>
{
if (localGameExistsPredicate(g))
{
return convertLocalToFrontend(localGames.find(g => localGameExistsPredicate({ id: { id: g.source_id ?? '', source: g.source ?? '' }, igdb_id: g.igdb_id, ra_id: g.ra_id }))!);
}
else
{
return g;
}
}));
} else
{
games.push(...localGames.slice(query.offset, query.limit ? query.offset ?? 0 + query.limit : undefined).map(g =>
{
return convertLocalToFrontend(g);
}));
const remoteGames: FrontEndGameType[] = [];
const remoteGames: FrontEndGameTypeWithIds[] = [];
const remoteGameSet = new Set<string>();
await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e));
games.push(...remoteGames.filter(g => !localGamesSet?.has(`${g.id.source}@${g.id.id}`)));
} else
{
const remoteGames: FrontEndGameType[] = [];
await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e));
games.push(...remoteGames.map(g =>
games.push(...remoteGames.filter(g =>
{
if (localGamesSet?.has(`${g.id.source}@${g.id.id}`))
if (localGameExistsPredicate(g))
{
return convertLocalToFrontend(localGames.find(l => l.source === g.id.source && l.source_id === g.id.id)!);
} else
{
return g;
return false;
}
if (g.igdb_id)
{
const igdbId = `igdb@${g.igdb_id}`;
if (remoteGameSet.has(igdbId)) return false;
remoteGameSet.add(igdbId);
}
if (g.ra_id)
{
const raId = `ra@${g.ra_id}`;
if (remoteGameSet.has(raId)) return false;
remoteGameSet.add(raId);
}
return true;
}));
}
}

View file

@ -35,7 +35,7 @@ export class GameHooks
*/
fetchGames = new AsyncSeriesHook<[ctx: {
query: GameListFilterType;
games: FrontEndGameType[];
games: FrontEndGameTypeWithIds[];
}]>(['ctx']);
fetchGame = new AsyncSeriesBailHook<[ctx: {
source: string;

View file

@ -15,6 +15,7 @@ import z from "zod";
import { checkFiles } from "../games/services/utils";
import { ensureDir } from "fs-extra";
import { path7za } from "7zip-bin";
import slugify from 'slugify';
interface JobConfig
{
@ -70,8 +71,8 @@ export class InstallJob implements IJob<never, InstallJobStates>
name: game.title,
summary: game.description,
system_slug: gameId.system,
path_fs: path.join('roms', gameId.system, game.title),
extract_path: path.join('roms', gameId.system, game.title),
path_fs: path.join('roms', gameId.system, slugify(game.title)),
extract_path: '.',
};
break;
@ -104,13 +105,17 @@ export class InstallJob implements IJob<never, InstallJobStates>
});
const downloadedFiles = await downloader.start();
if (!downloadedFiles)
{
return;
}
if (info.extract_path && downloadedFiles)
{
let progress = 0;
const progressDelta = 1 / downloadedFiles.length;
for (const filePath of downloadedFiles)
{
const extractPath = path.join(config.get('downloadPath'), info.extract_path);
const extractPath = path.join(config.get('downloadPath'), info.path_fs ?? '', info.extract_path);
await new Promise((resolve, reject) =>
{
const seven = Seven.extractFull(filePath, extractPath, { $bin: process.env.ZIP7_PATH ?? path7za, $progress: true });
@ -119,7 +124,10 @@ export class InstallJob implements IJob<never, InstallJobStates>
cx.setProgress(progress + p.percent * progressDelta, "extract");
});
seven.on('error', e => reject(e));
seven.on('error', e =>
{
reject(e);
});
seven.on('end', async () =>
{
await fs.rm(filePath);

View file

@ -3,7 +3,7 @@ import desc from './package.json';
import path from 'node:path';
import { config } from "@/bun/api/app";
export default class DOLPHINIntegration implements PluginType
export default class CEMUIntegration implements PluginType
{
emulator = 'CEMU';
@ -11,7 +11,7 @@ export default class DOLPHINIntegration implements PluginType
{
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"] };
return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen", "saves", "states"] };
});
ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
@ -20,7 +20,7 @@ export default class DOLPHINIntegration implements PluginType
args.push(`--fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`);
const savesPath = path.join(config.get('downloadPath'), "saves", 'DOLPHIN');
const savesPath = path.join(config.get('downloadPath'), "saves", this.emulator);
args.push(`--mlc=${savesPath}`);

View file

@ -13,7 +13,7 @@ export default class DOLPHINIntegration implements PluginType
{
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"] };
return { id: desc.name, supportLevel: "full", capabilities: ["batch", "config", "resolution", "fullscreen", "states"] };
});
ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
@ -35,6 +35,17 @@ export default class DOLPHINIntegration implements PluginType
args.push(`--config=Dolphin.Interface.SkipNKitWarning=True`);
args.push(`--config=Dolphin.Analytics.PermissionAsked=True`);
const resolution = config.get('emulatorResolution');
const resolutionMapping = {
"720p": 2,
"1080p": 3,
"1440p": 4,
"4k": 6
};
args.push(`--config=GFX.Settings.InternalResolution=${resolutionMapping[resolution] ?? 1}`);
args.push(`--config=GFX.Settings.wideScreenHack=${config.get('emulatorWidescreen') ? "True" : "False"}`);
args.push(`--config=GFX.Settings.AspectRatio=${config.get('emulatorWidescreen') ? "1" : "0"}`);
const savesPath = path.join(config.get('downloadPath'), "saves", this.emulator);
args.push(`--config=Dolphin.General.WiiSDCardPath=${path.join(savesPath, 'WiiSD.raw')}`);

View file

@ -21,7 +21,7 @@ CdvdShareWrite = false
EnablePatches = true
EnableCheats = false
EnablePINE = false
EnableWideScreenPatches = false
EnableWideScreenPatches = {{ENABLE_WIDESCREEN}}
EnableNoInterlacingPatches = false
EnableRecordingTools = true
EnableGameFixes = true
@ -92,7 +92,7 @@ VsyncEnable = 0
FramerateNTSC = 59.94
FrameratePAL = 50
SyncToHostRefreshRate = false
AspectRatio = Auto 4:3/3:2
AspectRatio = {{ASPECT_RATIO}}
FMVAspectRatioSwitch = Off
ScreenshotSize = 0
ScreenshotFormat = 0
@ -168,7 +168,7 @@ linear_present_mode = 1
deinterlace_mode = 0
OsdScale = 100
Renderer = 14
upscale_multiplier = 1
upscale_multiplier = {{UPSCALE_MULTIPLIER}}
mipmap_hw = -1
accurate_blending_unit = 1
crc_hack_level = -1

View file

@ -22,7 +22,7 @@ export default class PCSX2Integration implements PluginType
return {
id: desc.name,
supportLevel: "full",
capabilities: [...baseCapabilities, "resolution", "config"]
capabilities: [...baseCapabilities, "config", "resolution"]
};
}
else
@ -52,6 +52,12 @@ export default class PCSX2Integration implements PluginType
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 resolutionMapping = {
"720p": 2,
"1080p": 3,
"1440p": 4,
"4k": 6,
};
const view = {
BIOS_PATH: biosFolder,
@ -62,6 +68,9 @@ export default class PCSX2Integration implements PluginType
COVERS_PATH: path.join(storageFolder, 'covers'),
TEXTURES_PATH: path.join(storageFolder, 'textures'),
RECURSIVE_PATHS: path.join(config.get('downloadPath'), 'roms', 'PS2'),
ENABLE_WIDESCREEN: config.get('emulatorWidescreen'),
ASPECT_RATIO: config.get('emulatorWidescreen') ? "16:9" : "Auto 4:3/3:2",
UPSCALE_MULTIPLIER: resolutionMapping[config.get('emulatorResolution')] ?? 1
};
await Promise.all(Object.values(view).map(p => ensureDir(p)));

View file

@ -96,7 +96,7 @@ HardwareTransform = True
SoftwareSkinning = True
TextureFiltering = 1
BufferFiltering = 1
InternalResolution = 3
InternalResolution = {{RESOLUTION}}
AndroidHwScale = 1
HighQualityDepth = 1
FrameSkip = 0
@ -109,7 +109,7 @@ AnisotropyLevel = 4
VertexDecCache = False
TextureBackoffCache = False
TextureSecondaryCache = False
FullScreen = True
FullScreen = {{FULLSCREEN}}
FullScreenMulti = False
SmallDisplayZoomType = 2
SmallDisplayOffsetX = 0.500000

View file

@ -10,12 +10,21 @@ import Mustache from "mustache";
import { ensureDir } from "fs-extra";
import { homedir } from "node:os";
export default class PCSX2Integration implements PluginType
export default class PPSSPPIntegration implements PluginType
{
emulator = "PPSSPP";
load (ctx: PluginContextType)
{
ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
{
await Bun.write(path.join(ctx.path, "portable.txt"), "");
if (process.platform === 'win32')
{
await Bun.write(path.join(ctx.path, "installed.txt"), path.join(config.get('downloadPath'), 'saves', this.emulator));
}
});
ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) =>
{
const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen", "saves", "states"];
@ -25,7 +34,7 @@ export default class PCSX2Integration implements PluginType
return {
id: desc.name,
supportLevel: "full",
capabilities: [...baseCapabilities, "resolution", "config"]
capabilities: [...baseCapabilities, "config", "resolution"]
};
}
else
@ -68,7 +77,7 @@ export default class PCSX2Integration implements PluginType
let ppssppPath = '';
if (process.platform === 'win32')
{
ppssppPath = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM');
ppssppPath = path.join(config.get('downloadPath'), 'saves', this.emulator, 'PSP', 'SYSTEM');
} else
{
//TODO: Use way to set custom memstick path when they support it
@ -80,8 +89,17 @@ export default class PCSX2Integration implements PluginType
if (confPath)
{
const resolutionMapping = {
"720p": "2",
"1080p": "4",
"1440p": "6",
"4k": "8"
};
const configFileContents = await Bun.file(confPath).text();
await Bun.write(path.join(ppssppPath, 'ppsspp.ini'), Mustache.render(configFileContents, {}));
await Bun.write(path.join(ppssppPath, 'ppsspp.ini'), Mustache.render(configFileContents, {
RESOLUTION: resolutionMapping[config.get('emulatorResolution')] ?? 0,
FULLSCREEN: config.get('launchInFullscreen') ? "True" : "False"
}));
}
if (controlsPath)

View file

@ -96,7 +96,7 @@ HardwareTransform = True
SoftwareSkinning = True
TextureFiltering = 1
BufferFiltering = 1
InternalResolution = 3
InternalResolution = {{RESOLUTION}}
AndroidHwScale = 1
HighQualityDepth = 1
FrameSkip = 0
@ -109,7 +109,7 @@ AnisotropyLevel = 4
VertexDecCache = False
TextureBackoffCache = False
TextureSecondaryCache = False
FullScreen = True
FullScreen = {{FULLSCREEN}}
FullScreenMulti = False
SmallDisplayZoomType = 2
SmallDisplayOffsetX = 0.500000

View file

@ -0,0 +1,14 @@
{
"name": "com.simeonradivoev.gameflow.xemu",
"displayName": "XEMU Integration",
"version": "0.0.1",
"description": "XEMU Emulator Integration",
"main": "./xemu.ts",
"icon": "https://upload.wikimedia.org/wikipedia/commons/8/8e/Xemu_logo_green.svg",
"keywords": [
"integration",
"emulator",
"xbox",
"xemu"
]
}

View file

@ -0,0 +1,76 @@
import { PluginContextType, PluginType } from "@/bun/types/typesc.schema";
import desc from './package.json';
import { GameflowHooks } from "@/bun/api/hooks/app";
import { config } from "@/bun/api/app";
import path from "node:path";
import { ensureDir } from "fs-extra";
import toml, { TomlTable } from 'smol-toml';
import fs from 'node:fs/promises';
import bin from './eeprom.bin' with { type: 'file' };
export default class XEMUIntegration implements PluginType
{
emulator = 'XEMU';
load (ctx: PluginContextType)
{
ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) =>
{
return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen", "saves", "states"] };
});
ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) =>
{
const args: string[] = [];
if (config.get('launchInFullscreen'))
{
args.push("-full-screen");
}
if (ctx.autoValidCommand.metadata.romPath)
{
args.push("-dvd_path");
args.push(ctx.autoValidCommand.metadata.romPath);
}
const configPath = path.join(config.get('downloadPath'), 'storage', this.emulator, 'xemu.toml');
let configFile: { general: TomlTable & { misc: TomlTable; }, sys: TomlTable & { files: TomlTable; }; } = { general: { misc: {} }, sys: { files: {} } };
if (await Bun.file(configPath).exists())
{
configFile = toml.parse(await Bun.file(configPath).text()) as any;
}
configFile.general.misc ??= {};
configFile.general.misc.skip_boot_anim = true;
configFile.general.show_welcome = false;
configFile.general.games_dir = path.join(config.get('downloadPath'), 'roms', 'xbox');
configFile.sys.mem_limit = '128';
const biosFolder = path.join(config.get('downloadPath'), "bios", this.emulator);
if (await fs.exists(biosFolder))
{
const biosPaths = (await fs.readdir(biosFolder));
const flash = biosPaths.find(f => f.endsWith('.bin') && !f.includes('mcpx'));
const bootrom = biosPaths.find(f => f.endsWith('.bin') && f.includes('mcpx'));
const hardDrive = biosPaths.find(f => f.endsWith('qcow2'));
if (flash) configFile.sys.files.flashrom_path = path.join(biosFolder, flash);
if (bootrom) configFile.sys.files.bootrom_path = path.join(biosFolder, bootrom);
if (hardDrive) configFile.sys.files.hdd_path = path.join(biosFolder, hardDrive);
}
if (!ctx.dryRun)
{
const eepromPath = path.join(config.get('downloadPath'), "storage", this.emulator, 'eeprom.bin');
await Bun.write(eepromPath, await Bun.file(bin).arrayBuffer());
configFile.sys.files.eeprom_path = eepromPath;
await Bun.write(configPath, toml.stringify(configFile));
args.push("-config_path");
args.push(configPath);
}
return args;
});
}
}

View file

@ -0,0 +1,15 @@
{
"name": "com.simeonradivoev.gameflow.xenia",
"displayName": "XENIA Integration",
"version": "0.0.1",
"description": "XENIA Emulator Integration",
"main": "./xenia.ts",
"icon": "https://xenia.jp/images/logo-256x256.png",
"keywords": [
"integration",
"emulator",
"xbox360",
"xenia",
"xenia-edge"
]
}

View file

@ -0,0 +1,82 @@
import { PluginContextType, PluginType } from "@/bun/types/typesc.schema";
import desc from './package.json';
import { GameflowHooks } from "@/bun/api/hooks/app";
import { config } from "@/bun/api/app";
import path from "node:path";
import { ensureDir } from "fs-extra";
import toml, { TomlTable } from 'smol-toml';
import fs from 'node:fs/promises';
export default class XENIAIntegration implements PluginType
{
emulator = 'XENIA';
emulatorEdge = 'XENIA-EDGE';
async handlePostInstall (ctx: Parameters<typeof GameflowHooks.prototype.emulators.emulatorPostInstall.callAsync>['0'])
{
await Bun.write(path.join(ctx.path, "portable.txt"), "");
}
async handleLaunch (ctx: Parameters<typeof GameflowHooks.prototype.games.emulatorLaunch.callAsync>['0'])
{
const args: string[] = [];
if (ctx.autoValidCommand.metadata.romPath)
{
args.push(ctx.autoValidCommand.metadata.romPath);
}
const configPath = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, `${ctx.autoValidCommand.emulator}.toml`);
if (!ctx.dryRun)
{
await ensureDir(path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!));
let configFile: TomlTable & { Storage: TomlTable, GPU: TomlTable, Display: TomlTable; } = { Storage: {}, GPU: {}, Display: {} };
if (await fs.exists(configPath))
{
configFile = toml.parse(await Bun.file(configPath).text()) as any;
}
const resolutionMapping = {
"720p": 1,
"1080p": 2,
"1440p": 3,
"4k": 3
};
configFile.Display.fullscreen = config.get('launchInFullscreen');
configFile.GPU.draw_resolution_scale_x = resolutionMapping[config.get('emulatorResolution')] ?? 1;
configFile.GPU.draw_resolution_scale_y = resolutionMapping[config.get('emulatorResolution')] ?? 1;
await ensureDir(path.join(config.get('downloadPath'), 'saves', ctx.autoValidCommand.emulator!));
configFile.Storage.content_root = path.join(config.get('downloadPath'), 'saves', ctx.autoValidCommand.emulator!);
configFile.Storage.storage_root = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, 'config');
configFile.Storage.cache_root = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, 'cache');
await Bun.write(configPath, toml.stringify(configFile));
};
args.push(`--config`, configPath);
if (config.get('launchInFullscreen'))
{
args.push(`--fullscreen`);
}
return args;
}
handleEmulatorLaunchSupport (ctx: Parameters<typeof GameflowHooks.prototype.games.emulatorLaunchSupport.callAsync>['0']):
ReturnType<typeof GameflowHooks.prototype.games.emulatorLaunchSupport.call>
{
return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen", "saves", "states"] };
}
load (ctx: PluginContextType)
{
ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, this.handleEmulatorLaunchSupport);
ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulatorEdge }, this.handleEmulatorLaunchSupport);
ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, this.handleLaunch);
ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulatorEdge }, this.handleLaunch);
}
}

View file

@ -2,7 +2,7 @@
import { PluginContextType, PluginType } from "@/bun/types/typesc.schema";
import desc from './package.json';
import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomsApiRomsGet, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm";
import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomContentApiRomsIdContentFileNameGet, getRomsApiRomsGet, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm";
import { config } from "@/bun/api/app";
import path from 'node:path';
import fs from 'node:fs/promises';
@ -138,7 +138,9 @@ export default class RommIntegration implements PluginType
});
games.push(...rommGames.data.items.map(g =>
{
return this.convertRomToFrontend(g);
const game: FrontEndGameType & { igdb_id?: number; } = this.convertRomToFrontend(g);
game.igdb_id = g.igdb_id ?? undefined;
return game;
}));
}
});
@ -181,8 +183,9 @@ export default class RommIntegration implements PluginType
const files = await Promise.all(rom.files.map(async f =>
{
getRomContentApiRomsIdContentFileNameGet;
const file: DownloadFileEntry = {
url: new URL(`${config.get('rommAddress')}/api/romsfiles/${f.id}/content/${f.file_name}`),
url: new URL(`${config.get('rommAddress')}/api/roms/${f.id}/files/content/${f.file_name}`),
file_name: f.file_name,
file_path: f.file_path,
size: f.file_size_bytes,
@ -198,8 +201,8 @@ export default class RommIntegration implements PluginType
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);
extract_path = '.';
path_fs = path.join(rom.fs_path, rom.slug ?? rom.fs_name_no_ext);
}
}

View file

@ -3,6 +3,9 @@ import { PluginManager } from "./plugin-manager";
import pcsx2 from './builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json';
import ppsspp from './builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json';
import dolphin from './builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json';
import cemu from './builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json';
import xenia from './builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json';
import xemu from './builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json';
import romm from './builtin/sources/com.simeonradivoev.gameflow.romm/package.json';
import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@/bun/types/typesc.schema";
@ -13,6 +16,9 @@ export default async function register (pluginManager: PluginManager)
{ ...pcsx2, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2') },
{ ...ppsspp, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp') },
{ ...dolphin, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin') },
{ ...cemu, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu') },
{ ...xenia, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia') },
{ ...xemu, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu') },
{ ...romm, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.romm/romm') },
];

View file

@ -105,7 +105,7 @@ export class TaskQueue
{
this.queue = [];
this.activeQueue.forEach(c => c.abort());
return Promise.all(this.activeQueue.map(c => c.promise.promise));
return Promise.all(this.activeQueue.map(c => c.promise.promise.catch(e => console.error("Error During Task Queue Closing"))));
}
}
@ -212,10 +212,15 @@ export class JobContext<T extends IJob<TData, TState>, TData, TState extends str
{
this.events.emit('started', { id: this.m_id, job: this });
await this.m_job.start(this);
this.completed = true;
this.events.emit('completed', { id: this.m_id, job: this });
this.m_promise.resolve(this.m_job.exposeData?.());
if (!this.abortSignal.aborted)
{
this.completed = true;
this.events.emit('completed', { id: this.m_id, job: this });
this.m_promise.resolve(this.m_job.exposeData?.());
} else
{
this.m_promise.resolve(undefined);
}
} catch (error)
{
if (error !== 'cancel')
@ -225,7 +230,7 @@ export class JobContext<T extends IJob<TData, TState>, TData, TState extends str
this.events.emit('error', { id: this.m_id, job: this, error });
this.error = error;
this.m_promise.reject(error);
this.m_promise.resolve(undefined);
} finally
{
this.running = false;