fix: switched to node-7z

fix: switched to bun spawn but with windowsVerbatimArguments
feat: Added ppsspp integration
feat: Added focusing controls for windows
feat: Added shortcut to kill emulators
This commit is contained in:
Simeon Radivoev 2026-03-29 22:18:05 +03:00
parent a7eb655a48
commit 90d6711935
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
31 changed files with 1382 additions and 88 deletions

View file

@ -22,10 +22,12 @@ import UpdateStoreJob from "./jobs/update-store";
import { getStoreFolder } from "./store/services/gamesService";
import { PluginManager } from "./plugins/plugin-manager";
import registerPlugins from "./plugins/register-plugins";
import controls from '../controls';
export const config = new Conf<SettingsType>({
projectName: projectPackage.name,
projectSuffix: 'bun',
cwd: process.env.CONFIG_CWD,
schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any,
defaults: SettingsSchema.parse({
downloadPath: path.join(os.homedir(), "gameflow"),
@ -35,6 +37,7 @@ export const config = new Conf<SettingsType>({
export const customEmulators = new Conf<Record<string, string>>({
projectName: projectPackage.name,
projectSuffix: 'bun',
cwd: process.env.CONFIG_CWD,
configName: 'custom-emulators',
rootSchema: {
"type": "object",
@ -67,6 +70,7 @@ registerPlugins(plugins);
export const events = new EventEmitter<AppEventMap>();
config.onDidChange('downloadPath', () => reloadDatabase());
taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob());
await controls();
export async function cleanup ()
{

View file

@ -73,10 +73,6 @@ export async function getEmulatorsForSystem (systemSlug: string)
export async function getValidLaunchCommands (data: {
systemSlug: string;
gamePath: string;
customEmulatorConfig: {
get: (id: string) => string | undefined,
has: (id: string) => boolean,
};
}): Promise<CommandEntry[]>
{

View file

@ -6,7 +6,7 @@ import { Glob } from "bun";
import { config } from "../app";
import path from 'node:path';
import { getOrCachedGithubRelease } from "../cache";
import _7z from '7zip-min';
import Seven from 'node-7z';
import fs from "node:fs/promises";
import { Downloader } from "@/bun/utils/downloader";
import { move } from "fs-extra";
@ -85,7 +85,13 @@ export class EmulatorDownloadJob implements IJob<z.infer<typeof EmulatorDownload
if (await downloader.start() && destinationPaths[0])
{
let destinationPath = destinationPaths[0];
await _7z.unpack(destinationPath, emulatorsFolder);
await new Promise((resolve, reject) =>
{
const seven = Seven.extractFull(destinationPath, emulatorsFolder, { $bin: process.env.ZIP7_PATH, $progress: true });
seven.on('progress', p => context.setProgress(p.percent, "extract"));
seven.on('error', e => reject(e));
seven.on('end', () => resolve(true));
});
await fs.rm(destinationPath, { recursive: true });
// check if 1 root folder we need to get rid of

View file

@ -1,5 +1,4 @@
import { IJob, JobContext } from "../task-queue";
import { mkdir } from 'node:fs/promises';
import { and, eq, or } from 'drizzle-orm';
import fs from 'node:fs/promises';
import * as schema from "@schema/app";
@ -11,11 +10,10 @@ import * as igdb from 'ts-igdb-client';
import secrets from "../secrets";
import { simulateProgress } from "@/bun/utils";
import { Downloader } from "@/bun/utils/downloader";
import _7z from '7zip-min';
import Seven from 'node-7z';
import z from "zod";
import { checkFiles } from "../games/services/utils";
import { ensureDir } from "fs-extra";
import { getAuthToken } from "@/clients/romm/core/auth.gen";
interface JobConfig
{
@ -105,9 +103,19 @@ export class InstallJob implements IJob<never, InstallJobStates>
const downloadedFiles = await downloader.start();
if (info.extract_path && downloadedFiles)
{
let progress = 0;
const progressDelta = 1 / downloadedFiles.length;
for (const path of downloadedFiles)
{
await _7z.unpack(path, info.extract_path);
const extractPath = info.extract_path;
await new Promise((resolve, reject) =>
{
const seven = Seven.extractFull(path, extractPath, { $bin: process.env.ZIP7_PATH, $progress: true });
seven.on('progress', p => cx.setProgress(progress + p.percent * progressDelta, "extract"));
seven.on('error', e => reject(e));
seven.on('end', () => resolve(true));
});
progress += progressDelta * 100;
}
}
}

View file

@ -4,7 +4,8 @@ import { ActiveGameSchema, ActiveGameType } from "@/bun/types/typesc.schema";
import { db, events, plugins } from "../app";
import * as appSchema from "@schema/app";
import { eq, sql } from "drizzle-orm";
import { spawn } from 'node:child_process';
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
import { killBrowser } from "@/bun/utils/browser-spawner";
export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSchema>, "playing">
{
@ -39,26 +40,42 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
autoValidCommand: this.validCommand,
game: { source: this.gameSource, id: this.gameId }
});
const command = commandArgs ? this.validCommand.metadata.emulatorBin ?? this.validCommand.command : this.validCommand.command;
await new Promise((resolve, reject) =>
{
const game = spawn(command, commandArgs, {
shell: true,
cwd: this.validCommand.startDir,
signal: context.abortSignal
});
let game: Bun.Subprocess;
if (!commandArgs)
{
game = Bun.spawn(this.validCommand.command.split(' '), {
cwd: this.validCommand.startDir,
windowsVerbatimArguments: true,
signal: context.abortSignal
});
game.stdout.on('data', data => console.log(data));
game.on('close', (code) =>
game.exited.then(resolve).catch(e =>
{
console.error(e);
reject(e);
});
}
else if (this.validCommand.metadata.emulatorBin)
{
resolve(code);
});
game.on('error', e =>
game = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], {
cwd: this.validCommand.startDir,
windowsVerbatimArguments: true,
signal: context.abortSignal
});
game.exited.then(resolve).catch(e =>
{
console.error(e);
reject(e);
});
} else
{
console.error(e);
reject(e);
});
reject(new Error("No Emulator Bin"));
return;
}
this.activeGame = {
process: game,

View file

@ -0,0 +1,27 @@
[ControlMapping]
Up = 10-19
Down = 10-20
Left = 10-21
Right = 10-22
Circle = 10-190
Cross = 10-189
Square = 10-191
Triangle = 10-188
Start = 10-197
Select = 10-196
L = 10-193
R = 10-192
An.Up = 10-4003
An.Down = 10-4002
An.Left = 10-4001
An.Right = 10-4000
Fast-forward = 1-193:10-4010,1-135
Rewind = 10-196:10-4008
Save State = 10-196:10-192,1-132
Load State = 10-196:10-193,1-133
Previous Slot = 10-197:10-193,1-137
Next Slot = 10-197:10-192,1-136
Pause = 10-196:10-107,1-111
Screenshot = 10-196:10-190
Exit App = 10-196:10-197
SpeedToggle = 10-196:10-4010

View file

@ -0,0 +1,479 @@
[General]
FirstRun = False
RunCount = 4
Enable Logging = True
AutoRun = True
Browse = False
IgnoreBadMemAccess = True
CurrentDirectory = /home
ShowDebuggerOnLoad = False
CheckForNewVersion = True
Language = en_US
ForceLagSync2 = False
DiscordPresence = True
UISound = False
AutoLoadSaveState = 0
EnableCheats = True
CwCheatRefreshRate = 77
CwCheatScrollPosition = 0.000000
GameListScrollPosition = 0.000000
ScreenshotsAsPNG = False
UseFFV1 = False
DumpFrames = False
DumpVideoOutput = False
DumpAudio = False
SaveLoadResetsAVdumping = False
StateSlot = 0
EnableStateUndo = True
StateLoadUndoGame = NA
StateUndoLastSaveGame = NA
StateUndoLastSaveSlot = -5
RewindFlipFrequency = 0
ShowOnScreenMessage = True
ShowRegionOnGameIcon = False
ShowIDOnGameIcon = True
GameGridScale = 1.000000
GridView1 = True
GridView2 = False
GridView3 = False
RightAnalogUp = 0
RightAnalogDown = 0
RightAnalogLeft = 0
RightAnalogRight = 0
RightAnalogPress = 0
RightAnalogCustom = False
RightAnalogDisableDiagonal = False
SwipeUp = 0
SwipeDown = 0
SwipeLeft = 0
SwipeRight = 0
SwipeSensitivity = 1.000000
SwipeSmoothing = 0.300000
DoubleTapGesture = 0
GestureControlEnabled = False
ReportingHost = default
AutoSaveSymbolMap = False
CacheFullIsoInRam = False
RemoteISOPort = 0
LastRemoteISOServer =
LastRemoteISOPort = 0
RemoteISOManualConfig = False
RemoteShareOnStartup = False
RemoteISOSubdir = /
RemoteDebuggerOnStartup = False
InternalScreenRotation = 1
BackgroundAnimation = 1
PauseWhenMinimized = False
DumpDecryptedEboots = False
MemStickInserted = True
EnablePlugins = True
[CPU]
CPUCore = 1
SeparateSASThread = True
SeparateIOThread = True
IOTimingMethod = 0
FastMemoryAccess = True
FunctionReplacements = True
HideSlowWarnings = False
HideStateWarnings = False
PreloadFunctions = False
JitDisableFlags = 0x00000000
CPUSpeed = 0
[Graphics]
EnableCardboardVR = False
CardboardScreenSize = 50
CardboardXShift = 0
CardboardYShift = 0
ShowFPSCounter = 0
GraphicsBackend = 3 (VULKAN)
FailedGraphicsBackends =
DisabledGraphicsBackends =
VulkanDevice =
CameraDevice =
RenderingMode = 1
SoftwareRenderer = False
HardwareTransform = True
SoftwareSkinning = True
TextureFiltering = 1
BufferFiltering = 1
InternalResolution = 3
AndroidHwScale = 1
HighQualityDepth = 1
FrameSkip = 0
FrameSkipType = 0
AutoFrameSkip = False
FrameRate = 0
FrameRate2 = -1
UnthrottlingMode = CONTINUOUS
AnisotropyLevel = 4
VertexDecCache = False
TextureBackoffCache = False
TextureSecondaryCache = False
FullScreen = True
FullScreenMulti = False
SmallDisplayZoomType = 2
SmallDisplayOffsetX = 0.500000
SmallDisplayOffsetY = 0.500000
SmallDisplayZoomLevel = 1.000000
ImmersiveMode = True
SustainedPerformanceMode = False
IgnoreScreenInsets = True
ReplaceTextures = True
SaveNewTextures = False
IgnoreTextureFilenames = False
TexScalingLevel = 1
TexScalingType = 0
TexDeposterize = False
TexHardwareScaling = False
VSyncInterval = False
BloomHack = 0
SplineBezierQuality = 2
HardwareTessellation = False
TextureShader = Off
ShaderChainRequires60FPS = False
MemBlockTransferGPU = True
DisableSlowFramebufEffects = False
FragmentTestCache = True
LogFrameDrops = False
InflightFrames = 2
RenderDuplicateFrames = False
[Sound]
Enable = True
AudioBackend = 0
ExtraAudioBuffering = False
GlobalVolume = 10
ReverbVolume = 10
AltSpeedVolume = -1
AudioDevice =
AutoAudioDevice = False
[Control]
HapticFeedback = False
ShowTouchCross = True
ShowTouchCircle = True
ShowTouchSquare = True
ShowTouchTriangle = True
Custom0Mapping = 0x0000000000000000
Custom0Image = 0
Custom0Shape = 0
Custom0Toggle = False
Custom1Mapping = 0x0000000000000000
Custom1Image = 1
Custom1Shape = 0
Custom1Toggle = False
Custom2Mapping = 0x0000000000000000
Custom2Image = 2
Custom2Shape = 0
Custom2Toggle = False
Custom3Mapping = 0x0000000000000000
Custom3Image = 3
Custom3Shape = 0
Custom3Toggle = False
Custom4Mapping = 0x0000000000000000
Custom4Image = 4
Custom4Shape = 0
Custom4Toggle = False
Custom5Mapping = 0x0000000000000000
Custom5Image = 0
Custom5Shape = 1
Custom5Toggle = False
Custom6Mapping = 0x0000000000000000
Custom6Image = 1
Custom6Shape = 1
Custom6Toggle = False
Custom7Mapping = 0x0000000000000000
Custom7Image = 2
Custom7Shape = 1
Custom7Toggle = False
Custom8Mapping = 0x0000000000000000
Custom8Image = 3
Custom8Shape = 1
Custom8Toggle = False
Custom9Mapping = 0x0000000000000000
Custom9Image = 4
Custom9Shape = 1
Custom9Toggle = False
ShowTouchPause = False
ShowTouchControls = False
DisableDpadDiagonals = False
GamepadOnlyFocused = False
TouchButtonStyle = 1
TouchButtonOpacity = 65
TouchButtonHideSeconds = 20
AutoCenterTouchAnalog = False
AnalogAutoRotSpeed = 8.000000
TouchSnapToGrid = False
TouchSnapGridSize = 64
ActionButtonSpacing2 = 1.000000
ActionButtonCenterX = -1.000000
ActionButtonCenterY = -1.000000
ActionButtonScale = 1.150000
DPadX = -1.000000
DPadY = -1.000000
DPadScale = 1.150000
ShowTouchDpad = True
DPadSpacing = 1.000000
StartKeyX = -1.000000
StartKeyY = -1.000000
StartKeyScale = 1.150000
ShowTouchStart = True
SelectKeyX = -1.000000
SelectKeyY = -1.000000
SelectKeyScale = 1.150000
ShowTouchSelect = True
UnthrottleKeyX = -1.000000
UnthrottleKeyY = -1.000000
UnthrottleKeyScale = 1.150000
ShowTouchUnthrottle = True
LKeyX = -1.000000
LKeyY = -1.000000
LKeyScale = 1.150000
ShowTouchLTrigger = True
RKeyX = -1.000000
RKeyY = -1.000000
RKeyScale = 1.150000
ShowTouchRTrigger = True
AnalogStickX = -1.000000
AnalogStickY = -1.000000
AnalogStickScale = 1.150000
ShowAnalogStick = True
RightAnalogStickX = -1.000000
RightAnalogStickY = -1.000000
RightAnalogStickScale = 1.150000
ShowRightAnalogStick = False
fcombo0X = -1.000000
fcombo0Y = -1.000000
comboKeyScale0 = 1.150000
ShowComboKey0 = False
fcombo1X = -1.000000
fcombo1Y = -1.000000
comboKeyScale1 = 1.150000
ShowComboKey1 = False
fcombo2X = -1.000000
fcombo2Y = -1.000000
comboKeyScale2 = 1.150000
ShowComboKey2 = False
fcombo3X = -1.000000
fcombo3Y = -1.000000
comboKeyScale3 = 1.150000
ShowComboKey3 = False
fcombo4X = -1.000000
fcombo4Y = -1.000000
comboKeyScale4 = 1.150000
ShowComboKey4 = False
fcombo5X = -1.000000
fcombo5Y = -1.000000
comboKeyScale5 = 1.150000
ShowComboKey5 = False
fcombo6X = -1.000000
fcombo6Y = -1.000000
comboKeyScale6 = 1.150000
ShowComboKey6 = False
fcombo7X = -1.000000
fcombo7Y = -1.000000
comboKeyScale7 = 1.150000
ShowComboKey7 = False
fcombo8X = -1.000000
fcombo8Y = -1.000000
comboKeyScale8 = 1.150000
ShowComboKey8 = False
fcombo9X = -1.000000
fcombo9Y = -1.000000
comboKeyScale9 = 1.150000
ShowComboKey9 = False
AnalogDeadzone = 0.150000
AnalogInverseDeadzone = 0.000000
AnalogSensitivity = 1.100000
AnalogIsCircular = False
AnalogLimiterDeadzone = 0.600000
LeftStickHeadScale = 1.000000
RightStickHeadScale = 1.000000
HideStickBackground = False
UseMouse = False
MapMouse = False
ConfineMap = False
MouseSensitivity = 0.100000
MouseSmoothing = 0.900000
SystemControls = True
AllowMappingCombos = True
[Network]
EnableWlan = False
EnableAdhocServer = False
proAdhocServer = socom.cc
PortOffset = 10000
MinTimeout = 0
ForcedFirstConnect = False
EnableUPnP = False
UPnPUseOriginalPort = False
EnableNetworkChat = False
ChatButtonPosition = 0
ChatScreenPosition = 0
EnableQuickChat = True
QuickChat1 = Quick Chat 1
QuickChat2 = Quick Chat 2
QuickChat3 = Quick Chat 3
QuickChat4 = Quick Chat 4
QuickChat5 = Quick Chat 5
[SystemParam]
PSPModel = 1
PSPFirmwareVersion = 660
NickName = PPSSPP
MacAddress = ec:fd:62:d4:ec:73
Language = 1
ParamTimeFormat = 0
ParamDateFormat = 0
TimeZone = 0
DayLightSavings = False
ButtonPreference = 1
LockParentalLevel = 0
WlanAdhocChannel = 0
WlanPowerSave = False
EncryptSave = True
SavedataUpgradeVersion = True
MemStickSize = 16
[Debugger]
DisasmWindowX = -1
DisasmWindowY = -1
DisasmWindowW = -1
DisasmWindowH = -1
GEWindowX = -1
GEWindowY = -1
GEWindowW = -1
GEWindowH = -1
ConsoleWindowX = -1
ConsoleWindowY = -1
FontWidth = 8
FontHeight = 12
DisplayStatusBar = True
ShowBottomTabTitles = True
ShowDeveloperMenu = False
SkipDeadbeefFilling = False
FuncHashMap = False
MemInfoDetailed = False
DrawFrameGraph = False
[Upgrade]
UpgradeMessage =
UpgradeVersion =
DismissedVersion =
[Theme]
ItemStyleFg = 0xffffffff
ItemStyleBg = 0x55000000
ItemFocusedStyleFg = 0xffffffff
ItemFocusedStyleBg = 0xffedc24c
ItemDownStyleFg = 0xffffffff
ItemDownStyleBg = 0xffbd9939
ItemDisabledStyleFg = 0x80eeeeee
ItemDisabledStyleBg = 0x55e0d4af
ItemHighlightedStyleFg = 0xffffffff
ItemHighlightedStyleBg = 0x55bdbb39
ButtonStyleFg = 0xffffffff
ButtonStyleBg = 0x55000000
ButtonFocusedStyleFg = 0xffffffff
ButtonFocusedStyleBg = 0xffedc24c
ButtonDownStyleFg = 0xffffffff
ButtonDownStyleBg = 0xffbd9939
ButtonDisabledStyleFg = 0x80eeeeee
ButtonDisabledStyleBg = 0x55e0d4af
ButtonHighlightedStyleFg = 0xffffffff
ButtonHighlightedStyleBg = 0x55bdbb39
HeaderStyleFg = 0xffffffff
InfoStyleFg = 0xffffffff
InfoStyleBg = 0x00000000
PopupTitleStyleFg = 0xffe3be59
PopupStyleFg = 0xffffffff
PopupStyleBg = 0xff303030
[Recent]
MaxRecent = 60
[Log]
SYSTEMEnabled = True
SYSTEMLevel = 2
BOOTEnabled = True
BOOTLevel = 2
COMMONEnabled = True
COMMONLevel = 2
CPUEnabled = True
CPULevel = 2
FILESYSEnabled = True
FILESYSLevel = 2
G3DEnabled = True
G3DLevel = 2
HLEEnabled = True
HLELevel = 2
JITEnabled = True
JITLevel = 2
LOADEREnabled = True
LOADERLevel = 2
MEEnabled = True
MELevel = 2
MEMMAPEnabled = True
MEMMAPLevel = 2
SASMIXEnabled = True
SASMIXLevel = 2
SAVESTATEEnabled = True
SAVESTATELevel = 2
FRAMEBUFEnabled = True
FRAMEBUFLevel = 2
AUDIOEnabled = True
AUDIOLevel = 2
IOEnabled = True
IOLevel = 2
SCEAUDIOEnabled = True
SCEAUDIOLevel = 2
SCECTRLEnabled = True
SCECTRLLevel = 2
SCEDISPEnabled = True
SCEDISPLevel = 2
SCEFONTEnabled = True
SCEFONTLevel = 2
SCEGEEnabled = True
SCEGELevel = 2
SCEINTCEnabled = True
SCEINTCLevel = 2
SCEIOEnabled = True
SCEIOLevel = 2
SCEKERNELEnabled = True
SCEKERNELLevel = 2
SCEMODULEEnabled = True
SCEMODULELevel = 2
SCENETEnabled = True
SCENETLevel = 2
SCERTCEnabled = True
SCERTCLevel = 2
SCESASEnabled = True
SCESASLevel = 2
SCEUTILEnabled = True
SCEUTILLevel = 2
SCEMISCEnabled = True
SCEMISCLevel = 2
ACHIEVEMENTSEnabled = True
ACHIEVEMENTSLevel = 2
HTTPEnabled = True
HTTPLevel = 2
PRINTFEnabled = True
PRINTFLevel = 2
[PostShaderSetting]
BloomSettingValue1 = 0.600000
BloomSettingValue2 = 0.500000
CartoonSettingValue1 = 0.500000
ColorCorrectionSettingValue1 = 1.000000
ColorCorrectionSettingValue2 = 1.000000
ColorCorrectionSettingValue3 = 1.000000
ColorCorrectionSettingValue4 = 1.000000
ScanlinesSettingValue1 = 1.000000
ScanlinesSettingValue2 = 0.500000
SharpenSettingValue1 = 1.500000
[Achievements]
AchievementsEnable = False
AchievementsChallengeMode = False
AchievementsEncoreMode = False
AchievementsUnofficial = False
AchievementsLogBadMemReads = False
AchievementsUserName =
AchievementsSoundEffects = True
AchievementsUnlockAudioFile =
AchievementsLeaderboardSubmitAudioFile =
AchievementsLeaderboardTrackerPos = 3
AchievementsLeaderboardStartedOrFailedPos = 3
AchievementsLeaderboardSubmittedPos = 3
AchievementsProgressPos = 3
AchievementsChallengePos = 3
AchievementsUnlockedPos = 4

View file

@ -0,0 +1,14 @@
{
"name": "com.simeonradivoev.gameflow.ppsspp",
"displayName": "PPSSPP Integration",
"version": "0.0.1",
"description": "PPSSPP Emulator Integration",
"main": "./ppsspp.ts",
"icon": "https://www.ppsspp.org/static/img/platform/ppsspp-icon.png",
"keywords": [
"integration",
"emulator",
"psp",
"ppsspp"
]
}

View file

@ -0,0 +1,54 @@
import { PluginContextType, PluginType } from "@/bun/types/typesc.schema";
import desc from './package.json';
import { config } from "@/bun/api/app";
import configFilePathWin32 from './win32/ppsspp.ini' with { type: 'file' };
import configControlsFilePathWin32 from './win32/controls.ini' with { type: 'file' };
import configFilePathLinux from './linux/ppsspp.ini' with { type: 'file' };
import configControlsFilePathLinux from './linux/controls.ini' with { type: 'file' };
import path from "node:path";
import Mustache from "mustache";
import { ensureDir } from "fs-extra";
export default class PCSX2Integration implements PluginType
{
load (ctx: PluginContextType)
{
ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) =>
{
if (ctx.autoValidCommand.emulator === 'PPSSPP' && ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir)
{
const args = [ctx.autoValidCommand.metadata.romPath, "--escape-exit", "--pause-menu-exit"];
if (config.get('launchInFullscreen'))
{
args.push("--fullscreen");
}
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;
}
if (controlsPath)
{
const configFileContents = await Bun.file(controlsPath).text();
const controlsFileContents = await Bun.file(controlsPath).text();
ensureDir(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM'));
await Bun.write(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM', 'ppsspp.ini'), Mustache.render(configFileContents, {}));
await Bun.write(path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM', 'controls.ini'), Mustache.render(controlsFileContents, {}));
}
return args;
}
});
}
}

View file

@ -0,0 +1,23 @@
[ControlMapping]
Up = 20-19
Down = 20-20
Left = 20-21
Right = 20-22
Circle = 20-97
Cross = 20-96
Square = 20-100
Triangle = 20-99
Start = 20-108
Select = 20-109
L = 20-102
R = 20-103
An.Up = 20-4002
An.Down = 20-4003
An.Left = 20-4001
An.Right = 20-4000
Fast-forward = 20-109:20-4036
Rewind = 20-109:20-4034
Save State = 20-109:20-103
Load State = 20-109:20-102
Home = 20-108:20-109
Exit App = 20-3:20-108

View file

@ -0,0 +1,472 @@
[General]
FirstRun = False
RunCount = 4
Enable Logging = True
AutoRun = True
Browse = False
IgnoreBadMemAccess = True
CurrentDirectory = C:/Emulation/roms/psp
ShowDebuggerOnLoad = False
CheckForNewVersion = True
Language = en_US
ForceLagSync2 = False
DiscordPresence = True
UISound = False
AutoLoadSaveState = 0
EnableCheats = True
CwCheatRefreshRate = 77
CwCheatScrollPosition = 0.000000
GameListScrollPosition = 0.000000
ScreenshotsAsPNG = False
UseFFV1 = False
DumpFrames = False
DumpVideoOutput = False
DumpAudio = False
SaveLoadResetsAVdumping = False
StateSlot = 0
EnableStateUndo = True
StateLoadUndoGame = NA
StateUndoLastSaveGame = NA
StateUndoLastSaveSlot = -5
RewindFlipFrequency = 0
ShowOnScreenMessage = True
ShowRegionOnGameIcon = False
ShowIDOnGameIcon = False
GameGridScale = 1.000000
GridView1 = True
GridView2 = True
GridView3 = False
RightAnalogUp = 0
RightAnalogDown = 0
RightAnalogLeft = 0
RightAnalogRight = 0
RightAnalogPress = 0
RightAnalogCustom = False
RightAnalogDisableDiagonal = False
SwipeUp = 0
SwipeDown = 0
SwipeLeft = 0
SwipeRight = 0
SwipeSensitivity = 1.000000
SwipeSmoothing = 0.300000
DoubleTapGesture = 0
GestureControlEnabled = False
ReportingHost = default
AutoSaveSymbolMap = False
CacheFullIsoInRam = False
RemoteISOPort = 0
LastRemoteISOServer =
LastRemoteISOPort = 0
RemoteISOManualConfig = False
RemoteShareOnStartup = False
RemoteISOSubdir = /
RemoteDebuggerOnStartup = False
InternalScreenRotation = 1
BackgroundAnimation = 1
PauseWhenMinimized = False
DumpDecryptedEboots = False
MemStickInserted = True
EnablePlugins = True
[CPU]
CPUCore = 1
SeparateSASThread = True
SeparateIOThread = True
IOTimingMethod = 0
FastMemoryAccess = True
FunctionReplacements = True
HideSlowWarnings = False
HideStateWarnings = False
PreloadFunctions = False
JitDisableFlags = 0x00000000
CPUSpeed = 0
[Graphics]
EnableCardboardVR = False
CardboardScreenSize = 50
CardboardXShift = 0
CardboardYShift = 0
ShowFPSCounter = 0
GraphicsBackend = 3 (VULKAN)
FailedGraphicsBackends =
DisabledGraphicsBackends =
VulkanDevice =
CameraDevice =
RenderingMode = 1
SoftwareRenderer = False
HardwareTransform = True
SoftwareSkinning = True
TextureFiltering = 1
BufferFiltering = 1
InternalResolution = 3
AndroidHwScale = 1
HighQualityDepth = 1
FrameSkip = 0
FrameSkipType = 0
AutoFrameSkip = False
FrameRate = 0
FrameRate2 = -1
UnthrottlingMode = CONTINUOUS
AnisotropyLevel = 4
VertexDecCache = False
TextureBackoffCache = False
TextureSecondaryCache = False
FullScreen = True
FullScreenMulti = False
SmallDisplayZoomType = 2
SmallDisplayOffsetX = 0.500000
SmallDisplayOffsetY = 0.500000
SmallDisplayZoomLevel = 1.000000
ImmersiveMode = True
SustainedPerformanceMode = False
IgnoreScreenInsets = True
ReplaceTextures = True
SaveNewTextures = False
IgnoreTextureFilenames = False
TexScalingLevel = 1
TexScalingType = 0
TexDeposterize = False
TexHardwareScaling = False
VSyncInterval = False
BloomHack = 0
SplineBezierQuality = 2
HardwareTessellation = False
TextureShader = Off
ShaderChainRequires60FPS = False
MemBlockTransferGPU = True
DisableSlowFramebufEffects = False
FragmentTestCache = True
LogFrameDrops = False
InflightFrames = 2
RenderDuplicateFrames = False
[Sound]
Enable = True
AudioBackend = 0
ExtraAudioBuffering = False
GlobalVolume = 10
ReverbVolume = 10
AltSpeedVolume = -1
AudioDevice =
AutoAudioDevice = False
[Control]
HapticFeedback = False
ShowTouchCross = True
ShowTouchCircle = True
ShowTouchSquare = True
ShowTouchTriangle = True
Custom0Mapping = 0x0000000000000000
Custom0Image = 0
Custom0Shape = 0
Custom0Toggle = False
Custom1Mapping = 0x0000000000000000
Custom1Image = 1
Custom1Shape = 0
Custom1Toggle = False
Custom2Mapping = 0x0000000000000000
Custom2Image = 2
Custom2Shape = 0
Custom2Toggle = False
Custom3Mapping = 0x0000000000000000
Custom3Image = 3
Custom3Shape = 0
Custom3Toggle = False
Custom4Mapping = 0x0000000000000000
Custom4Image = 4
Custom4Shape = 0
Custom4Toggle = False
Custom5Mapping = 0x0000000000000000
Custom5Image = 0
Custom5Shape = 1
Custom5Toggle = False
Custom6Mapping = 0x0000000000000000
Custom6Image = 1
Custom6Shape = 1
Custom6Toggle = False
Custom7Mapping = 0x0000000000000000
Custom7Image = 2
Custom7Shape = 1
Custom7Toggle = False
Custom8Mapping = 0x0000000000000000
Custom8Image = 3
Custom8Shape = 1
Custom8Toggle = False
Custom9Mapping = 0x0000000000000000
Custom9Image = 4
Custom9Shape = 1
Custom9Toggle = False
ShowTouchPause = False
ShowTouchControls = False
DisableDpadDiagonals = False
GamepadOnlyFocused = False
TouchButtonStyle = 1
TouchButtonOpacity = 65
TouchButtonHideSeconds = 20
AutoCenterTouchAnalog = False
AnalogAutoRotSpeed = 8.000000
TouchSnapToGrid = False
TouchSnapGridSize = 64
ActionButtonSpacing2 = 1.000000
ActionButtonCenterX = -1.000000
ActionButtonCenterY = -1.000000
ActionButtonScale = 1.150000
DPadX = -1.000000
DPadY = -1.000000
DPadScale = 1.150000
ShowTouchDpad = True
DPadSpacing = 1.000000
StartKeyX = -1.000000
StartKeyY = -1.000000
StartKeyScale = 1.150000
ShowTouchStart = True
SelectKeyX = -1.000000
SelectKeyY = -1.000000
SelectKeyScale = 1.150000
ShowTouchSelect = True
UnthrottleKeyX = -1.000000
UnthrottleKeyY = -1.000000
UnthrottleKeyScale = 1.150000
ShowTouchUnthrottle = True
LKeyX = -1.000000
LKeyY = -1.000000
LKeyScale = 1.150000
ShowTouchLTrigger = True
RKeyX = -1.000000
RKeyY = -1.000000
RKeyScale = 1.150000
ShowTouchRTrigger = True
AnalogStickX = -1.000000
AnalogStickY = -1.000000
AnalogStickScale = 1.150000
ShowAnalogStick = True
RightAnalogStickX = -1.000000
RightAnalogStickY = -1.000000
RightAnalogStickScale = 1.150000
ShowRightAnalogStick = False
fcombo0X = -1.000000
fcombo0Y = -1.000000
comboKeyScale0 = 1.150000
ShowComboKey0 = False
fcombo1X = -1.000000
fcombo1Y = -1.000000
comboKeyScale1 = 1.150000
ShowComboKey1 = False
fcombo2X = -1.000000
fcombo2Y = -1.000000
comboKeyScale2 = 1.150000
ShowComboKey2 = False
fcombo3X = -1.000000
fcombo3Y = -1.000000
comboKeyScale3 = 1.150000
ShowComboKey3 = False
fcombo4X = -1.000000
fcombo4Y = -1.000000
comboKeyScale4 = 1.150000
ShowComboKey4 = False
fcombo5X = -1.000000
fcombo5Y = -1.000000
comboKeyScale5 = 1.150000
ShowComboKey5 = False
fcombo6X = -1.000000
fcombo6Y = -1.000000
comboKeyScale6 = 1.150000
ShowComboKey6 = False
fcombo7X = -1.000000
fcombo7Y = -1.000000
comboKeyScale7 = 1.150000
ShowComboKey7 = False
fcombo8X = -1.000000
fcombo8Y = -1.000000
comboKeyScale8 = 1.150000
ShowComboKey8 = False
fcombo9X = -1.000000
fcombo9Y = -1.000000
comboKeyScale9 = 1.150000
ShowComboKey9 = False
AnalogDeadzone = 0.150000
AnalogInverseDeadzone = 0.000000
AnalogSensitivity = 1.100000
AnalogIsCircular = False
AnalogLimiterDeadzone = 0.600000
LeftStickHeadScale = 1.000000
RightStickHeadScale = 1.000000
HideStickBackground = False
UseMouse = False
MapMouse = False
ConfineMap = False
MouseSensitivity = 0.100000
MouseSmoothing = 0.900000
SystemControls = True
AllowMappingCombos = True
[Network]
EnableWlan = False
EnableAdhocServer = False
proAdhocServer = socom.cc
PortOffset = 10000
MinTimeout = 0
ForcedFirstConnect = False
EnableUPnP = False
UPnPUseOriginalPort = False
EnableNetworkChat = False
ChatButtonPosition = 0
ChatScreenPosition = 0
EnableQuickChat = True
QuickChat1 = Quick Chat 1
QuickChat2 = Quick Chat 2
QuickChat3 = Quick Chat 3
QuickChat4 = Quick Chat 4
QuickChat5 = Quick Chat 5
[SystemParam]
PSPModel = 1
PSPFirmwareVersion = 660
NickName = PPSSPP
MacAddress = ec:fd:62:d4:ec:73
Language = 1
ParamTimeFormat = 0
ParamDateFormat = 0
TimeZone = 0
DayLightSavings = False
ButtonPreference = 1
LockParentalLevel = 0
WlanAdhocChannel = 0
WlanPowerSave = False
EncryptSave = True
SavedataUpgradeVersion = True
MemStickSize = 16
[Debugger]
DisasmWindowX = -1
DisasmWindowY = -1
DisasmWindowW = -1
DisasmWindowH = -1
GEWindowX = -1
GEWindowY = -1
GEWindowW = -1
GEWindowH = -1
ConsoleWindowX = -1
ConsoleWindowY = -1
FontWidth = 8
FontHeight = 12
DisplayStatusBar = True
ShowBottomTabTitles = True
ShowDeveloperMenu = False
SkipDeadbeefFilling = False
FuncHashMap = False
MemInfoDetailed = False
DrawFrameGraph = False
[Upgrade]
UpgradeMessage =
UpgradeVersion =
DismissedVersion =
[Theme]
ItemStyleFg = 0xffffffff
ItemStyleBg = 0x55000000
ItemFocusedStyleFg = 0xffffffff
ItemFocusedStyleBg = 0xffedc24c
ItemDownStyleFg = 0xffffffff
ItemDownStyleBg = 0xffbd9939
ItemDisabledStyleFg = 0x80eeeeee
ItemDisabledStyleBg = 0x55e0d4af
ItemHighlightedStyleFg = 0xffffffff
ItemHighlightedStyleBg = 0x55bdbb39
ButtonStyleFg = 0xffffffff
ButtonStyleBg = 0x55000000
ButtonFocusedStyleFg = 0xffffffff
ButtonFocusedStyleBg = 0xffedc24c
ButtonDownStyleFg = 0xffffffff
ButtonDownStyleBg = 0xffbd9939
ButtonDisabledStyleFg = 0x80eeeeee
ButtonDisabledStyleBg = 0x55e0d4af
ButtonHighlightedStyleFg = 0xffffffff
ButtonHighlightedStyleBg = 0x55bdbb39
HeaderStyleFg = 0xffffffff
InfoStyleFg = 0xffffffff
InfoStyleBg = 0x00000000
PopupTitleStyleFg = 0xffe3be59
PopupStyleFg = 0xffffffff
PopupStyleBg = 0xff303030
[Recent]
MaxRecent = 60
[Log]
SYSTEMEnabled = True
SYSTEMLevel = 2
BOOTEnabled = True
BOOTLevel = 2
COMMONEnabled = True
COMMONLevel = 2
CPUEnabled = True
CPULevel = 2
FILESYSEnabled = True
FILESYSLevel = 2
G3DEnabled = True
G3DLevel = 2
HLEEnabled = True
HLELevel = 2
JITEnabled = True
JITLevel = 2
LOADEREnabled = True
LOADERLevel = 2
MEEnabled = True
MELevel = 2
MEMMAPEnabled = True
MEMMAPLevel = 2
SASMIXEnabled = True
SASMIXLevel = 2
SAVESTATEEnabled = True
SAVESTATELevel = 2
FRAMEBUFEnabled = True
FRAMEBUFLevel = 2
AUDIOEnabled = True
AUDIOLevel = 2
IOEnabled = True
IOLevel = 2
SCEAUDIOEnabled = True
SCEAUDIOLevel = 2
SCECTRLEnabled = True
SCECTRLLevel = 2
SCEDISPEnabled = True
SCEDISPLevel = 2
SCEFONTEnabled = True
SCEFONTLevel = 2
SCEGEEnabled = True
SCEGELevel = 2
SCEINTCEnabled = True
SCEINTCLevel = 2
SCEIOEnabled = True
SCEIOLevel = 2
SCEKERNELEnabled = True
SCEKERNELLevel = 2
SCEMODULEEnabled = True
SCEMODULELevel = 2
SCENETEnabled = True
SCENETLevel = 2
SCERTCEnabled = True
SCERTCLevel = 2
SCESASEnabled = True
SCESASLevel = 2
SCEUTILEnabled = True
SCEUTILLevel = 2
SCEMISCEnabled = True
SCEMISCLevel = 2
[PostShaderSetting]
BloomSettingValue1 = 0.600000
BloomSettingValue2 = 0.500000
CartoonSettingValue1 = 0.500000
ColorCorrectionSettingValue1 = 1.000000
ColorCorrectionSettingValue2 = 1.000000
ColorCorrectionSettingValue3 = 1.000000
ColorCorrectionSettingValue4 = 1.000000
ScanlinesSettingValue1 = 1.000000
ScanlinesSettingValue2 = 0.500000
SharpenSettingValue1 = 1.500000
[Achievements]
AchievementsEnable = False
AchievementsChallengeMode = False
AchievementsEncoreMode = False
AchievementsUnofficial = False
AchievementsLogBadMemReads = False
AchievementsSoundEffects = True
AchievementsUnlockAudioFile =
AchievementsLeaderboardSubmitAudioFile =
AchievementsLeaderboardTrackerPos = 3
AchievementsLeaderboardStartedOrFailedPos = 3
AchievementsLeaderboardSubmittedPos = 3
AchievementsProgressPos = 3
AchievementsChallengePos = 3
AchievementsUnlockedPos = 4

View file

@ -1,6 +1,7 @@
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 romm from './builtin/sources/com.simeonradivoev.gameflow.romm/package.json';
import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@/bun/types/typesc.schema";
@ -9,6 +10,7 @@ export default async function register (pluginManager: PluginManager)
const plugins: (PluginDescriptionType & { main: string; load: () => Promise<any>; })[] = [
{ ...pcsx2, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2') },
{ ...ppsspp, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp') },
{ ...romm, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.romm/romm') },
];

View file

@ -62,33 +62,46 @@ export const system = new Elysia({ prefix: '/api/system' })
return new Response(buildNotificationsStream());
})
.ws('/info/system', {
response: SystemInfoSchema,
response: z.discriminatedUnion('type', [
z.object({ type: z.literal('info'), data: SystemInfoSchema }),
z.object({ type: z.literal('focus') })
]),
async open (ws)
{
const battery = await si.battery();
const wifi = await si.wifiConnections();
const bluetooth = await si.bluetoothDevices();
ws.send({
battery: battery,
wifiConnections: wifi,
bluetoothDevices: bluetooth
type: 'info',
data: {
battery: battery,
wifiConnections: wifi,
bluetoothDevices: bluetooth
}
}, true);
const handleFocus = () => ws.send({ type: 'focus' });
events.on('focus', handleFocus);
(ws.data as any).dispose = [() => events.removeListener('focus', handleFocus)];
(ws.data as any).observer = setInterval(async () =>
{
const battery = await si.battery();
const wifi = await si.wifiConnections();
const bluetooth = await si.bluetoothDevices();
ws.send({
battery: battery,
wifiConnections: wifi,
bluetoothDevices: bluetooth
type: 'info',
data: {
battery: battery,
wifiConnections: wifi,
bluetoothDevices: bluetooth
}
}, true);
}, 1000 * 30);
},
close (ws)
{
clearInterval((ws.data as any).observer);
(ws.data as any).dispose.forEach((dispose: any) => dispose());
}
})
.get('/drives', async () =>

View file

@ -2,6 +2,7 @@ import { killBrowser, spawnBrowser } from './utils/browser-spawner';
import { BrowserParams, BuildParams } from './utils/browser-params';
import os from 'node:os';
import { EventEmitter } from 'node:stream';
import { dlopen, FFIType } from "bun:ffi";
export default async function init (events: EventEmitter, forceBrowser: boolean, params: BrowserParams)
{
@ -31,8 +32,6 @@ async function runWebview (events: EventEmitter, params: BrowserParams)
config.WINDOW_HEIGHT = String(params.windowSize?.height);
}
const webviewWorker = new Worker(webviewPath, {
smol: true,
ref: false,
env: {
...config,
...process.env as any
@ -41,7 +40,6 @@ async function runWebview (events: EventEmitter, params: BrowserParams)
return new Promise((resolve, reject) =>
{
const handleExit = () =>
{
resolve(true);
@ -49,6 +47,8 @@ async function runWebview (events: EventEmitter, params: BrowserParams)
webviewWorker.terminate();
};
let pointer: any = undefined;
webviewWorker.addEventListener('error', e =>
{
console.error(e.message);
@ -64,10 +64,35 @@ async function runWebview (events: EventEmitter, params: BrowserParams)
{
console.log("Webview Destroyed");
resolve(true);
} else if (e.data.type === 'pointer')
{
pointer = e.data.data;
}
});
events.on('exitapp', handleExit);
events.on('focus', () =>
{
if (process.platform === 'win32')
{
const user32 = dlopen("user32.dll", {
SetForegroundWindow: { args: [FFIType.ptr], returns: FFIType.bool },
ShowWindow: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.bool },
BringWindowToTop: { args: [FFIType.ptr], returns: FFIType.bool },
keybd_event: { args: [FFIType.u8, FFIType.u8, FFIType.u32, FFIType.ptr], returns: FFIType.void },
});
const SW_RESTORE = 9;
if (pointer)
{
user32.symbols.ShowWindow(pointer, SW_RESTORE);
user32.symbols.keybd_event(0, 0, 0, null); // fake input event
user32.symbols.BringWindowToTop(pointer);
user32.symbols.SetForegroundWindow(pointer);
}
}
});
});
}

53
src/bun/controls.ts Normal file
View file

@ -0,0 +1,53 @@
import { LaunchGameJob } from './api/jobs/launch-game-job';
import { events, taskQueue } from './api/app';
process.env.SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS = "1";
process.env.SDL_JOYSTICK_THREAD = "1";
export default async function Initialize ()
{
const { default: sdl } = await import('@kmamal/sdl');
const launcherWin = sdl.video.createWindow({ title: "Launcher", visible: false });
sdl.controller.devices.forEach(d => connectToController(d));
sdl.controller.on('deviceAdd', e =>
{
connectToController(e.device);
});
function connectToController (device: any)
{
let selectHeld = false;
const ctrl = sdl.controller.openDevice(device);
console.log("Connected to", device.name);
ctrl.on("buttonDown", ({ button }) =>
{
if (button === "back") selectHeld = true;
if (button === "start" && selectHeld)
{
const launchGameTask = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob);
if (launchGameTask)
{
launchGameTask.abort('exit');
taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300));
} else
{
events.emit('focus');
}
}
if (button === 'guide')
{
events.emit('focus');
}
});
ctrl.on("buttonUp", ({ button }) =>
{
if (button === "back") selectHeld = false;
});
}
}

View file

@ -38,12 +38,16 @@ if (process.env.HEADLESS)
}
});
// Called by user
// Using stdout for communication as ipc doesn't seem to work with dev.ts script
app.events.on('exitapp', () =>
{
process.send?.({ type: 'exitapp' });
process.stdout.write('exitapp\n');
cleanup();
});
app.events.on('focus', () =>
{
process.stdout.write("focus\n");
});
} else
{
await init(app.events, Bun.env.FORCE_BROWSER === "true", {

View file

@ -28,4 +28,5 @@ declare interface AppEventMap
{
exitapp: [];
notification: [FrontendNotification];
focus: [];
}

View file

@ -27,7 +27,7 @@ export type PluginContextType = z.infer<typeof PluginContextSchema>;
export type PluginDescriptionType = z.infer<typeof PluginDescriptionSchema>;
export const ActiveGameSchema = z.object({
process: z.instanceof(ChildProcess).optional(),
process: z.any().optional(),
gameId: z.number(),
name: z.string(),
command: z.object({ command: z.string(), startDir: z.string().optional() })

View file

@ -1,4 +1,5 @@
import { $, type Subprocess } from "bun";
import { ChildProcessWithoutNullStreams } from "node:child_process";
import os from 'node:os';
export type RunBrowserType = "chrome" | "chromium" | "firefox" | "edge";
@ -163,7 +164,7 @@ export async function spawnBrowser ({
return processSub;
}
export async function killBrowser (browser: Subprocess)
export async function killBrowser (browser: Subprocess | ChildProcessWithoutNullStreams)
{
if (os.platform() === 'linux')
{

View file

@ -6,4 +6,5 @@ let size: Size | undefined = undefined;
if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT)
size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE };
const webview = new Webview(process.env.NODE_ENV === 'development', size);
self.postMessage({ type: 'pointer', data: webview.unsafeWindowHandle });
webviewWorkerBase(webview);

View file

@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { SystemInfoContext } from "../scripts/contexts";
import { systemApi } from "../scripts/clientApi";
import { SystemInfoType } from "@/shared/constants";
export default function AppCommunication (data: { children: any; })
{
const [systemInfo, setSystemInfo] = useState<SystemInfoType | undefined>();
useEffect(() =>
{
const sub = systemApi.api.system.info.system.subscribe();
sub.subscribe(({ data }) =>
{
switch (data.type)
{
case "info":
setSystemInfo(data.data);
break;
case "focus":
window.focus();
break;
}
});
document.documentElement.dataset.loaded = "true";
}, []);
return <SystemInfoContext value={systemInfo}>
{data.children}
</SystemInfoContext>;
}

View file

@ -464,7 +464,7 @@ const assets = new Set<string>([
]);
// Store basePath resolved from Vite config
const BASE_PATH = "/";
const BASE_PATH = "./";
/**

View file

@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { SystemInfoContext } from "../scripts/contexts";
import { SystemInfoType } from "@/shared/constants";
import { systemApi } from "../scripts/clientApi";
import AppCommunication from "../components/AppCommunication";
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
@ -34,23 +35,11 @@ function RootComponent ()
}, [theme]);
const [systemInfo, setSystemInfo] = useState<SystemInfoType | undefined>();
useEffect(() =>
{
const sub = systemApi.api.system.info.system.subscribe();
sub.subscribe(({ data }) =>
{
setSystemInfo(data);
});
document.documentElement.dataset.loaded = "true";
}, []);
return (
<div data-device={isMobile ? 'mobile' : ''} data-active-control={control} className="w-screen h-screen overflow-hidden">
<SystemInfoContext value={systemInfo}>
<AppCommunication>
<Outlet />
</SystemInfoContext>
</AppCommunication>
<Notifications />
<Toaster containerStyle={{ viewTimelineName: 'toasters', viewTransitionName: 'notifications' }} />
{/*import.meta.env.DEV && !isMobile &&

View file

@ -1,19 +1,25 @@
import { expect, test } from 'bun:test';
import { resolve } from 'node:path';
import './preload';
test("uses custom emulator", async () =>
{
const { getValidLaunchCommands: getLaunchCommands } = await import('../bun/api/games/services/launchGameService');
const { customEmulators } = await import('@/bun/api/app');
customEmulators.set('PCSX2', resolve("./src/tests/mock-roms/mock-emulator.exe"));
const { getValidLaunchCommands: getLaunchCommands } = await import('@/bun/api/games/services/launchGameService');
const commands = await getLaunchCommands({
systemSlug: 'ps2',
gamePath: './src/tests/mock-roms/mock-rom.iso',
customEmulatorConfig: new Map([['PCSX2', "./src/tests/mock-roms/pcsx2.exe"]])
gamePath: './mock-rom.iso'
});
expect(commands)
.toSatisfy((d) =>
!!d?.find(c =>
c?.command.includes("./src/tests/mock-roms/mock-rom.iso") &&
c.command.includes("./src/tests/mock-roms/pcsx2.exe")
)
);
{
const validCommand = d.find(c =>
c?.command.includes("mock-rom.iso") &&
c.command.includes("mock-emulator.exe")
);
return !!validCommand;
});
});

View file

View file

@ -1,2 +1,11 @@
import { mock } from 'bun:test';
import { beforeAll } from 'bun:test';
import { resolve } from 'node:path';
beforeAll(async () =>
{
process.env.CUSTOM_STORE_PATH = resolve('./src/tests/mock-store');
process.env.CONFIG_CWD = resolve('./src/tests/mock-config');
const { config } = await import('@/bun/api/app');
config.set('downloadPath', resolve('./src/tests/mock-roms'));
});