Skip to content

Commit 72d7e9e

Browse files
committed
refactor: Rewrite LAUNCH_GAME logic to use better structured content runners
fix: Hide all ! prefix views from browse header buttons fix: Explicit check in create and rename for ! prefix fix: Highlight non-Browse header buttons regardless of additional pathnames
1 parent 3f65e43 commit 72d7e9e

File tree

8 files changed

+474
-458
lines changed

8 files changed

+474
-458
lines changed

extensions/core-ruffle/src/extension.ts

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as flashpoint from 'flashpoint-launcher';
2-
import * as path from 'node:path';
32
import * as fs from 'node:fs';
4-
import { downloadFile, getGithubAsset, getPlatformRegex } from './util';
5-
import { AssetFile } from './types';
6-
import { RuffleStandaloneMiddleware } from './middleware/standalone';
3+
import * as path from 'node:path';
74
import { RuffleWebEmbedMiddleware } from './middleware/embed';
5+
import { RuffleStandaloneMiddleware } from './middleware/standalone';
6+
import { AssetFile } from './types';
7+
import { downloadFile, getGithubAsset, getPlatformRegex } from './util';
88

99
export async function activate(context: flashpoint.ExtensionContext): Promise<void> {
1010
// const registerSub = (d: flashpoint.Disposable) => { flashpoint.registerDisposable(context.subscriptions, d); };
@@ -66,14 +66,10 @@ export async function activate(context: flashpoint.ExtensionContext): Promise<vo
6666
const supportedEnabled = flashpoint.getExtConfigValue('com.ruffle.enabled');
6767
const unsupportedEnabled = flashpoint.getExtConfigValue('com.ruffle.enabled-all');
6868

69-
if (launchInfo.launchInfo.override === 'flash') {
70-
return;
71-
}
72-
73-
if (launchInfo.launchInfo.override === 'ruffle') {
74-
flashpoint.log.info('Using Standalone Ruffle for overriden game...');
69+
if (supportedEnabled || curation) {
70+
if (launchInfo.game.ruffleSupport.toLowerCase() === 'standalone') {
71+
flashpoint.log.info('Using Standalone Ruffle for supported game...');
7572
const defaultConfig = standaloneMiddleware.getDefaultConfig(launchInfo.game);
76-
defaultConfig.config.graphics = flashpoint.getExtConfigValue('com.ruffle.graphics-mode');
7773
standaloneMiddleware.execute(launchInfo, {
7874
middlewareId: '',
7975
name: '',
@@ -82,27 +78,6 @@ export async function activate(context: flashpoint.ExtensionContext): Promise<vo
8278
config: defaultConfig.config,
8379
});
8480
return;
85-
}
86-
87-
if (supportedEnabled || curation) {
88-
if (launchInfo.game.ruffleSupport.toLowerCase() === 'standalone') {
89-
const useLauncherEmbed = flashpoint.getExtConfigValue('com.ruffle.use-launcher-embed');
90-
if (useLauncherEmbed) {
91-
flashpoint.log.info('Using Launcher Embed Ruffle for supported game...');
92-
launchInfo.launchInfo.component = 'ruffle/LauncherEmbedPage';
93-
return;
94-
} else {
95-
flashpoint.log.info('Using Standalone Ruffle for supported game...');
96-
const defaultConfig = standaloneMiddleware.getDefaultConfig(launchInfo.game);
97-
standaloneMiddleware.execute(launchInfo, {
98-
middlewareId: '',
99-
name: '',
100-
enabled: true,
101-
version: defaultConfig.version,
102-
config: defaultConfig.config,
103-
});
104-
return;
105-
}
10681
} else if (launchInfo.game.ruffleSupport.toLowerCase() === 'webhosted') {
10782
flashpoint.log.info('Using Web Embed Ruffle for supported game...');
10883
const defaultConfig = webEmbedMiddleware.getDefaultConfig(launchInfo.game);

src/back/GameLauncher.ts

Lines changed: 63 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,10 @@ export namespace GameLauncher {
9898
await handleGameDataParams(opts, serverOverride, gameData || undefined);
9999
}
100100
const launchInfo: LaunchInfo = {
101-
override: opts.override,
102101
gamePath: gamePath,
103102
gameArgs: appArgs,
104103
useWine,
105-
env: getEnvironment(opts.fpPath, opts.proxy, opts.envPATH),
104+
env: getContentEnvironment(opts.fpPath, opts.proxy, process.platform, true, opts.envPATH),
106105
};
107106
const managedProc = opts.runAddApp(launchInfo);
108107
log.info(logSource, `Launch ${managedProc.name} (PID: ${managedProc.getPid()}) [\n`+
@@ -177,39 +176,14 @@ export namespace GameLauncher {
177176
await onWillEvent.fire(launchInfo)
178177
.then(async () => {
179178
// Handle middleware
180-
if (launchInfo.activeConfig) {
181-
log.info(logSource, `Using Game Configuration: ${launchInfo.activeConfig.name}`);
182-
for (const middlewareConfig of launchInfo.activeConfig.middleware) {
183-
// Find middleware in registry
184-
const middleware = opts.state.registry.middlewares.get(middlewareConfig.middlewareId);
185-
if (!middleware) {
186-
throw `Middleware not found (${middlewareConfig.middlewareId})`;
187-
}
188-
try {
189-
launchInfo = await Promise.resolve(middleware.execute(launchInfo, middlewareConfig));
190-
} catch (err) {
191-
throw `Failed to execute middleware (${middlewareConfig.middlewareId}) - ${err}`;
192-
}
193-
// @TODO - Validate launch info
194-
}
195-
log.info(logSource, 'Applied Game Configuration Successfully.');
196-
}
197179
await handleGameDataParams(opts, serverOverride, launchInfo.activeData ? launchInfo.activeData : undefined);
198180

199-
if (launchInfo.launchInfo.component) {
200-
opts.state.socketServer.broadcast(BackOut.OPEN_DYNAMIC_PAGE, launchInfo.launchInfo.component, launchInfo);
201-
log.info(logSource,`Launch Game "${opts.game.title}" [\n`+
202-
` applicationPath: "${launchInfo.launchInfo.gamePath}",\n`+
203-
` launchCommand: "${metadataLaunchCommand}",\n`+
204-
` launcher component: "${launchInfo.launchInfo.component}" ]`);
205-
} else {
206-
const command: string = createCommand(launchInfo.launchInfo);
207-
const managedProc = opts.runGame(launchInfo);
208-
log.info(logSource,`Launch Game "${opts.game.title}" (PID: ${managedProc.getPid()}) [\n`+
209-
` applicationPath: "${launchInfo.launchInfo.gamePath}",\n`+
210-
` launchCommand: "${metadataLaunchCommand}",\n`+
211-
` command: "${command}" ]`);
212-
}
181+
const command: string = createLaunchInfoCommand(launchInfo.launchInfo);
182+
const managedProc = opts.runGame(launchInfo);
183+
log.info(logSource,`Launch Game "${opts.game.title}" (PID: ${managedProc.getPid()}) [\n`+
184+
` applicationPath: "${launchInfo.launchInfo.gamePath}",\n`+
185+
` launchCommand: "${metadataLaunchCommand}",\n`+
186+
` command: "${command}" ]`);
213187
})
214188
.catch((error) => {
215189
log.info('Game Launcher', `Game Launch Aborted: ${error}`);
@@ -242,7 +216,7 @@ export namespace GameLauncher {
242216

243217
// Browser Mode Launch
244218
if (isBrowserOpts(res)) {
245-
const env = getEnvironment(opts.fpPath, opts.proxy, opts.envPATH);
219+
const env = getContentEnvironment(opts.fpPath, opts.proxy, process.platform, true, opts.envPATH);
246220
if ('ELECTRON_RUN_AS_NODE' in env) {
247221
delete env['ELECTRON_RUN_AS_NODE']; // If this flag is present, it will disable electron features from the process
248222
}
@@ -251,17 +225,16 @@ export namespace GameLauncher {
251225
browserLaunchArgs.push(`browser_url=${(res.url)}`);
252226
const gameLaunchInfo: GameLaunchInfo = {
253227
game: opts.game,
228+
isCuration: curation,
254229
activeData: gameData,
255230
launchInfo: {
256-
override: opts.override,
257231
gamePath: process.execPath,
258232
gameArgs: browserLaunchArgs,
259233
useWine: false,
260234
env,
261235
cwd: getCwd(opts.isDev, opts.exePath),
262236
noshell: true,
263237
},
264-
activeConfig: opts.activeConfig,
265238
};
266239
await onWillEvent.fire(gameLaunchInfo)
267240
.then(() => {
@@ -288,7 +261,7 @@ export namespace GameLauncher {
288261
const gamePath: string = path.isAbsolute(appPath) ? fixSlashes(appPath) : fixSlashes(path.join(opts.fpPath, appPath));
289262
const gameArgs: string[] = [...appArgs, metadataLaunchCommand];
290263
const useWine: boolean = process.platform != 'win32' && gamePath.endsWith('.exe');
291-
const env = getEnvironment(opts.fpPath, opts.proxy, opts.envPATH);
264+
const env = getContentEnvironment(opts.fpPath, opts.proxy, process.platform, true, opts.envPATH);
292265
try {
293266
// Double check game exists? Why are we doing this? TODO
294267
await fpDatabase.findGame(opts.game.id);
@@ -297,15 +270,14 @@ export namespace GameLauncher {
297270
}
298271
const gameLaunchInfo: GameLaunchInfo = {
299272
game: opts.game,
273+
isCuration: curation,
300274
activeData: gameData,
301275
launchInfo: {
302-
override: opts.override,
303276
gamePath,
304277
gameArgs,
305278
useWine,
306279
env,
307280
},
308-
activeConfig: opts.activeConfig
309281
};
310282
await launchCb(gameLaunchInfo);
311283
}
@@ -359,57 +331,25 @@ export namespace GameLauncher {
359331
// No Native exec found, return Windows/XML application path
360332
return filePath;
361333
}
334+
}
362335

363-
/**
364-
* Get an object containing the environment variables to use for the game / additional application.
365-
*
366-
* @param fpPath Path to Flashpoint Data Folder
367-
* @param proxy HTTP_PROXY environmental variable to add to env (For Linux / Mac)
368-
* @param path Override PATH environmental variable
369-
*/
370-
function getEnvironment(fpPath: string, proxy: string, path?: string): NodeJS.ProcessEnv {
371-
let newEnvVars: NodeJS.ProcessEnv = { 'FP_PATH': fpPath, 'PATH': path ?? process.env.PATH };
372-
// On Linux, we tell native applications to use Flashpoint's proxy using the HTTP_PROXY env var
373-
// On Windows, executables are patched to load the FlashpointProxy library
374-
// On Linux/Mac, WINE obeys the HTTP_PROXY env var so we can run unpatched Windows executables
375-
if (process.platform === 'linux' || process.platform === 'darwin') {
376-
// Add proxy env vars and prevent WINE from flooding the logs with debug messages
377-
newEnvVars = {
378-
...newEnvVars, 'WINEDEBUG': 'fixme-all',
379-
...(proxy !== '' ? { 'http_proxy': `http://${proxy}/`, 'HTTP_PROXY': `http://${proxy}/` } : null)
380-
};
381-
// If WINE's bin directory exists in FPSoftware, add it to the PATH
382-
if (fs.existsSync(`${fpPath}/FPSoftware/Wine/bin`)) {
383-
newEnvVars = {
384-
...newEnvVars, 'PATH': `${fpPath}/FPSoftware/Wine/bin:` + process.env.PATH
385-
};
336+
export function createLaunchInfoCommand(launchInfo: LaunchInfo): string {
337+
// This whole escaping thing is horribly broken. We probably want to switch
338+
// to an array representing the argv instead and not have a shell
339+
// in between.
340+
const { gamePath, gameArgs, useWine } = launchInfo;
341+
const args = typeof gameArgs === 'string' ? [gameArgs] : gameArgs;
342+
switch (process.platform) {
343+
case 'win32':
344+
return `"${gamePath}" ${args.join(' ')}`;
345+
case 'darwin':
346+
case 'linux':
347+
if (useWine) {
348+
return `wine start /wait /unix "${gamePath}" ${args.join(' ')}`;
386349
}
387-
}
388-
return {
389-
// Copy this processes environment variables
390-
...process.env,
391-
...newEnvVars
392-
};
393-
}
394-
395-
function createCommand(launchInfo: LaunchInfo): string {
396-
// This whole escaping thing is horribly broken. We probably want to switch
397-
// to an array representing the argv instead and not have a shell
398-
// in between.
399-
const { gamePath, gameArgs, useWine } = launchInfo;
400-
const args = typeof gameArgs === 'string' ? [gameArgs] : gameArgs;
401-
switch (process.platform) {
402-
case 'win32':
403-
return `"${gamePath}" ${args.join(' ')}`;
404-
case 'darwin':
405-
case 'linux':
406-
if (useWine) {
407-
return `wine start /wait /unix "${gamePath}" ${args.join(' ')}`;
408-
}
409-
return `"${gamePath}" ${args.join(' ')}`;
410-
default:
411-
throw Error('Unsupported platform');
412-
}
350+
return `"${gamePath}" ${args.join(' ')}`;
351+
default:
352+
throw Error('Unsupported platform');
413353
}
414354
}
415355

@@ -625,3 +565,38 @@ export async function checkAndInstallPlatform(platforms: Platform[], state: Back
625565
}
626566
}
627567
}
568+
569+
/**
570+
* Get an object containing the environment variables to use for the game / additional application.
571+
*
572+
* @param fpPath Path to Flashpoint Data Folder
573+
* @param proxy HTTP_PROXY environmental variable to add to env (For Linux / Mac)
574+
* @param path Override PATH environmental variable
575+
*/
576+
export function getContentEnvironment(fpPath: string, proxy: string, platform: NodeJS.Platform, inherit?: boolean, path?: string): NodeJS.ProcessEnv {
577+
let newEnvVars: NodeJS.ProcessEnv = { 'FP_PATH': fpPath, 'PATH': path ?? process.env.PATH };
578+
// On Linux, we tell native applications to use Flashpoint's proxy using the HTTP_PROXY env var
579+
// On Windows, executables are patched to load the FlashpointProxy library
580+
// On Linux/Mac, WINE obeys the HTTP_PROXY env var so we can run unpatched Windows executables
581+
if (platform === 'linux' || platform === 'darwin') {
582+
// Add proxy env vars and prevent WINE from flooding the logs with debug messages
583+
newEnvVars = {
584+
...newEnvVars, 'WINEDEBUG': 'fixme-all',
585+
...(proxy !== '' ? { 'http_proxy': `http://${proxy}/`, 'HTTP_PROXY': `http://${proxy}/` } : null)
586+
};
587+
// If WINE's bin directory exists in FPSoftware, add it to the PATH
588+
if (inherit && fs.existsSync(`${fpPath}/FPSoftware/Wine/bin`)) {
589+
newEnvVars = {
590+
...newEnvVars, 'PATH': `${fpPath}/FPSoftware/Wine/bin:` + process.env.PATH
591+
};
592+
}
593+
}
594+
if (inherit) {
595+
return {
596+
// Copy this processes environment variables
597+
...process.env,
598+
...newEnvVars
599+
};
600+
}
601+
return newEnvVars;
602+
}

src/back/extensions/ApiImplementation.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { ExtConfigFile } from '@back/ExtConfigFile';
2+
import { getContentEnvironment } from '@back/GameLauncher';
23
import { DisposableChildProcess, ManagedChildProcess } from '@back/ManagedChildProcess';
34
import { EXT_CONFIG_FILENAME, PREFERENCES_FILENAME } from '@back/constants';
45
import { loadCurationIndexImage } from '@back/curate/parse';
56
import { duplicateCuration, genCurationWarnings, makeCurationFromGame, refreshCurationContent } from '@back/curate/util';
67
import { saveCuration } from '@back/curate/write';
78
import { downloadGameData } from '@back/download';
89
import { installExtension as installExtensionUtil, uninstallExtension as uninstallExtensionUtil, unzipFile as unzipFileUtil } from '@back/extensions/util';
10+
import { ensureGameDataDownloaded, getApplicationPath } from '@back/flashpoint/WebgameContentRunner';
911
import { genContentTree } from '@back/rust';
1012
import { BackState, StatusState } from '@back/types';
1113
import { awaitDialog } from '@back/util/dialog';
@@ -30,6 +32,7 @@ import { langTemplate } from '@shared/lang';
3032
import { PreferencesFile } from '@shared/preferences/PreferencesFile';
3133
import { overwritePreferenceData } from '@shared/preferences/util';
3234
import { formatString } from '@shared/utils/StringFormatter';
35+
import { isGame } from '@shared/utils/misc';
3336
import * as flashpoint from 'flashpoint-launcher';
3437
import { Game, IExtensionManifest, Task } from 'flashpoint-launcher';
3538
import * as fsExtra from 'fs-extra';
@@ -100,9 +103,30 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
100103
return uninstallExtensionUtil(state, extId);
101104
};
102105

103-
const registerDataProvider = (provider: flashpoint.GameDataProvider): void => {
104-
console.log(`Registered ${provider.id}`);
106+
const registerContentRunner = (cr: flashpoint.ContentRunner): flashpoint.Disposable => {
107+
console.log(`Registered Content Runner ${cr.id}`);
108+
state.registry.contentRunners.set(cr.id, cr);
109+
return {
110+
toDispose: [],
111+
isDisposed: false,
112+
/** Callback to use when disposed */
113+
onDispose: () => {
114+
state.registry.contentRunners.delete(cr.id);
115+
}
116+
};
117+
};
118+
119+
const registerDataProvider = (provider: flashpoint.GameDataProvider): flashpoint.Disposable => {
120+
console.log(`Registered Data Provider ${provider.id}`);
105121
state.registry.dataSources.set(provider.id, provider);
122+
return {
123+
toDispose: [],
124+
isDisposed: false,
125+
/** Callback to use when disposed */
126+
onDispose: () => {
127+
state.registry.dataSources.delete(provider.id);
128+
}
129+
};
106130
};
107131

108132
const registerDataExtension = (extension: flashpoint.DataExtensionInfo): void => {
@@ -695,6 +719,13 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
695719
onExtConfigChange: state.apiEmitters.ext.onExtConfigChange.extEvent(extManifest.displayName || extManifest.name),
696720
focusWindow: focusWindow,
697721
langTemplate: langTemplate,
722+
isGame,
723+
getApplicationPath: (filePath, platform) => {
724+
return getApplicationPath(filePath, state.execMappings, state.preferences.nativePlatforms.some(p => p === platform));
725+
},
726+
getContentEnvironment,
727+
ensureGameDataDownloaded: (game) => ensureGameDataDownloaded(state, game),
728+
registerContentRunner,
698729

699730
// Namespaces
700731
sources: extSources,

0 commit comments

Comments
 (0)