feat: Added more ways to detect duplicates
feat: Added resolution and widescreen settings feat: Added Xenia and Xemu integration
This commit is contained in:
parent
764691fc86
commit
05fafced07
25 changed files with 407 additions and 49 deletions
6
bun.lock
6
bun.lock
|
|
@ -25,6 +25,8 @@
|
|||
"node-stream-zip": "^1.15.0",
|
||||
"open": "^11.0.0",
|
||||
"pathe": "^2.0.3",
|
||||
"slugify": "^1.6.9",
|
||||
"smol-toml": "^1.6.1",
|
||||
"systeminformation": "^5.31.5",
|
||||
"tapable": "^2.3.0",
|
||||
"tough-cookie": "^6.0.0",
|
||||
|
|
@ -1516,6 +1518,10 @@
|
|||
|
||||
"simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="],
|
||||
|
||||
"slugify": ["slugify@1.6.9", "", {}, "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg=="],
|
||||
|
||||
"smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
|
||||
|
||||
"socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="],
|
||||
|
||||
"socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="],
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@
|
|||
"node-stream-zip": "^1.15.0",
|
||||
"open": "^11.0.0",
|
||||
"pathe": "^2.0.3",
|
||||
"slugify": "^1.6.9",
|
||||
"smol-toml": "^1.6.1",
|
||||
"systeminformation": "^5.31.5",
|
||||
"tapable": "^2.3.0",
|
||||
"tough-cookie": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
games.push(...remoteGames.filter(g =>
|
||||
{
|
||||
const remoteGames: FrontEndGameType[] = [];
|
||||
await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e));
|
||||
games.push(...remoteGames.map(g =>
|
||||
if (localGameExistsPredicate(g))
|
||||
{
|
||||
if (localGamesSet?.has(`${g.id.source}@${g.id.id}`))
|
||||
{
|
||||
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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export class GameHooks
|
|||
*/
|
||||
fetchGames = new AsyncSeriesHook<[ctx: {
|
||||
query: GameListFilterType;
|
||||
games: FrontEndGameType[];
|
||||
games: FrontEndGameTypeWithIds[];
|
||||
}]>(['ctx']);
|
||||
fetchGame = new AsyncSeriesBailHook<[ctx: {
|
||||
source: string;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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')}`);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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') },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { oneShot } from "@/mainview/scripts/audio/audio";
|
|||
|
||||
export function OptionDropdown (data: {
|
||||
name: string;
|
||||
type: HTMLInputTypeAttribute;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
icon?: JSX.Element;
|
||||
|
|
|
|||
55
src/mainview/components/options/SettingsDropdown.tsx
Normal file
55
src/mainview/components/options/SettingsDropdown.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { HTMLInputTypeAttribute, JSX, useCallback, useEffect, useState } from "react";
|
||||
import { SettingsType } from "../../../shared/constants";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { OptionSpace } from "./OptionSpace";
|
||||
import { OptionInput } from "./OptionInput";
|
||||
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
||||
import { OptionDropdown } from "./OptionDropdown";
|
||||
|
||||
export function SettingsDropdown (data: {
|
||||
label: string;
|
||||
id: KeysWithValueAssignableTo<SettingsType, string>;
|
||||
values: string[];
|
||||
placeholder?: string;
|
||||
icon?: JSX.Element;
|
||||
children?: any;
|
||||
})
|
||||
{
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [localValue, setLocalValue] = useState<string | undefined>();
|
||||
const { data: serverValue } = useQuery(getSettingQuery(data.id));
|
||||
const setMutation = useMutation(setSettingMutation(data.id));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setLocalValue(serverValue as any);
|
||||
setDirty(false);
|
||||
}, [serverValue]);
|
||||
|
||||
const handleSave = useCallback(() =>
|
||||
{
|
||||
if (dirty)
|
||||
{
|
||||
setDirty(false);
|
||||
setMutation.mutate(localValue);
|
||||
}
|
||||
}, [dirty, setDirty, localValue]);
|
||||
|
||||
return (
|
||||
<OptionSpace id={`${data.id}-space`} label={data.label}>
|
||||
<OptionDropdown
|
||||
icon={data.icon}
|
||||
name={data.id ?? ""}
|
||||
placeholder={data.placeholder}
|
||||
onBlur={handleSave}
|
||||
onChange={(v) =>
|
||||
{
|
||||
setLocalValue(v);
|
||||
setMutation.mutate(v);
|
||||
}}
|
||||
value={localValue} values={data.values}
|
||||
/>
|
||||
{data.children}
|
||||
</OptionSpace>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { Check, ChevronDown, FileQuestion, FolderSearch, Plug, SearchAlert, Stor
|
|||
import { ContextDialog, ContextList, DialogEntry, OptionElement } from '../../components/ContextDialog';
|
||||
import classNames from 'classnames';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { RPC_URL } from '../../../shared/constants';
|
||||
import { RPC_URL, SettingsSchema } from '../../../shared/constants';
|
||||
import emulators from '@emulators';
|
||||
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||
|
|
@ -19,6 +19,7 @@ import Carousel from '@/mainview/components/Carousel';
|
|||
import { FOCUS_KEYS } from '@/mainview/scripts/types';
|
||||
import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils';
|
||||
import { SettingsOption } from '@/mainview/components/options/SettingsOption';
|
||||
import { SettingsDropdown } from '@/mainview/components/options/SettingsDropdown';
|
||||
|
||||
export const Route = createFileRoute('/settings/emulators')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -328,6 +329,8 @@ function RouteComponent ()
|
|||
<EmulatorBadges addOverride={addOverrideMutation.mutate} onFocus={scrollIntoViewHandler({ block: 'center' })} />
|
||||
<div className="divider text-base-content/40">Preferences</div>
|
||||
<SettingsOption label="Launch In Fullscreen" id="launchInFullscreen" type="checkbox" />
|
||||
<SettingsOption label="Widescreen" id="emulatorWidescreen" type="checkbox" />
|
||||
<SettingsDropdown label='Resolution' id='emulatorResolution' values={SettingsSchema.shape.emulatorResolution.unwrap().options} />
|
||||
<div className="divider text-base-content/40">Overrides</div>
|
||||
<NewEmulatorPath isAddingOverride={addOverrideMutation.isPending} addOverride={addOverrideMutation.mutate} />
|
||||
{!!customEmulators && customEmulators.map((key) => <EmulatorPath key={key} id={key} />)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
|
||||
import { emulators } from '@/bun/api/schema/emulators';
|
||||
import { FocusDetails } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { JSX } from 'react';
|
||||
import * as z from 'zod';
|
||||
|
|
@ -35,7 +36,9 @@ export const SettingsSchema = z.object({
|
|||
windowPosition: z.object({ x: z.number(), y: z.number() }).optional(),
|
||||
downloadPath: z.string(),
|
||||
launchInFullscreen: z.boolean().default(true),
|
||||
disabledPlugins: z.array(z.string()).default([])
|
||||
disabledPlugins: z.array(z.string()).default([]),
|
||||
emulatorResolution: z.enum(['720p', '1080p', '1440p', '4k']).default('720p'),
|
||||
emulatorWidescreen: z.boolean().default(true)
|
||||
});
|
||||
|
||||
export const LocalSettingsSchema = z.object({
|
||||
|
|
|
|||
6
src/shared/types..d.ts
vendored
6
src/shared/types..d.ts
vendored
|
|
@ -153,6 +153,12 @@ declare interface FrontEndPlatformType
|
|||
paths_screenshots: string[];
|
||||
}
|
||||
|
||||
declare interface FrontEndGameTypeWithIds extends FrontEndGameType
|
||||
{
|
||||
igdb_id: number | null;
|
||||
ra_id: number | null;
|
||||
}
|
||||
|
||||
declare interface FrontEndGameType
|
||||
{
|
||||
platform_display_name: string | null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue