Skip to content

Commit

Permalink
feat: Game Config basic functions
Browse files Browse the repository at this point in the history
  • Loading branch information
colin969 committed Oct 9, 2023
1 parent c9a80ae commit 64ee874
Show file tree
Hide file tree
Showing 12 changed files with 875 additions and 57 deletions.
8 changes: 4 additions & 4 deletions extensions/core-ruffle/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export async function activate(context: flashpoint.ExtensionContext): Promise<vo
const ruffleWebLatestDir = path.join(baseDataPath, 'webhosted', 'latest');
const ruffleStandaloneLatestDir = path.join(baseDataPath, 'standalone', 'latest');

// Register middleware
const standaloneMiddleware = new RuffleStandaloneMiddleware(path.join(baseDataPath, 'standalone'));
flashpoint.middleware.registerMiddleware(standaloneMiddleware);

// Check for Standalone updates
const logVoid = () => {};
const standaloneAssetFile = await getGithubAsset(getPlatformRegex(), logVoid);
Expand Down Expand Up @@ -43,10 +47,6 @@ export async function activate(context: flashpoint.ExtensionContext): Promise<vo
} else {
flashpoint.log.info('No Ruffle Web Assets found?');
}

// Register middleware
const standaloneMiddleware = new RuffleStandaloneMiddleware(path.join(baseDataPath, 'standalone'));
flashpoint.middleware.registerMiddleware(standaloneMiddleware);
}

async function downloadRuffleStandalone(ruffleStandaloneDir: string, assetFile: AssetFile, logDev: (text: string) => void) {
Expand Down
110 changes: 88 additions & 22 deletions extensions/core-ruffle/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ConfigSchema, Game, GameConfig, GameLaunchInfo, GameMiddlewareConfig, IGameMiddleware } from 'flashpoint-launcher';
import * as path from 'path';
import * as flashpoint from 'flashpoint-launcher';
import { ConfigSchema, Game, GameLaunchInfo, GameMiddlewareConfig, GameMiddlewareDefaultConfig, IGameMiddleware } from 'flashpoint-launcher';
import * as os from 'os';
import * as path from 'path';

// Config Schema used to configure the middleware per game
const schema: ConfigSchema = [
{
type: 'label',
key: 'none',
title: 'Graphical Options'
},
// --fullscreen
{
type: 'boolean',
title: 'Full Screen',
key: 'fullscreen',
optional: true,
default: false
},
// --quality
{
type: 'string',
title: 'Quality',
key: 'quality',
options: ['low', 'medium', 'high', 'best', 'high8x8', 'high8x8-linear', 'high16x16', 'high16x16-linear'],
default: 'high',
},
// --graphics
{
type: 'string',
Expand All @@ -14,36 +35,52 @@ const schema: ConfigSchema = [
options: ['default', 'vulkan', 'metal', 'dx12', 'gl'],
default: 'default',
},
// --no-gui
{
type: 'boolean',
title: 'Hide GUI',
key: 'noGui',
optional: true,
default: false
type: 'label',
key: 'none',
title: 'Other Options'
},
// --frame-rate
{
type: 'number',
title: 'Player Version',
key: 'playerVersion',
description: 'The version of the player to emulate',
default: 32,
minimum: 1,
maximum: 32,
integer: true
},
// -P
{
type: 'string',
title: 'Flash Vars',
key: 'flashVars',
description: 'Format as pairs e.g `foo=bar speed=fast mode="Full Screen"',
optional: true
},
// --no-gui
{
type: 'boolean',
title: 'Hide GUI',
key: 'noGui',
optional: true,
validate: (input: string) => {
// Must be a set of key value pairs
const regex = /^(\w+="[^"]+"\s*)+$/;
return regex.test(input);
}
default: false
},
];

// Stored config value map
type RuffleConfig = {
flashVars?: string; // -P
flashVars: string; // -P
quality: 'low' | 'medium' | 'high' | 'high8x8' | 'high8x8-linear' | 'high16x16' | 'high16x16-linear';
graphics: 'default' | 'vulkan' | 'metal' | 'dx12' | 'gl'; // --graphics
noGui?: boolean; // --no-gui
noGui: boolean; // --no-gui
playerVersion: number; // --player-version
fullscreen: boolean; // --fullscreen
};

const DEFAULT_CONFIG: Partial<RuffleConfig> = {};

type FlashVar = {
key: string;
value: string;
Expand All @@ -68,19 +105,43 @@ export class RuffleStandaloneMiddleware implements IGameMiddleware {

execute(gameLaunchInfo: GameLaunchInfo, middlewareConfig: GameMiddlewareConfig): GameLaunchInfo {
// Cast our config values to the correct type
const config = middlewareConfig.config as RuffleConfig;
const config = {
...DEFAULT_CONFIG,
...(middlewareConfig.config as Partial<RuffleConfig>)
};

// Replace application path with ruffle standalone executable (<base>/<version>/<executable>)
const executable = os.platform() === 'win32' ? 'ruffle.exe' : 'ruffle';
const execPath = path.join(this.ruffleStandaloneRoot, middlewareConfig.version, executable);

// Add any configured ruffle params to the launch args
const launchArgs = coerceToStringArray(gameLaunchInfo.launchInfo.gameArgs);
// --quality
if (config.quality) {
launchArgs.unshift(config.quality);
launchArgs.unshift('--quality');
}
// --graphics
launchArgs.unshift(config.graphics);
launchArgs.unshift('--graphics');
if (config.graphics) {
launchArgs.unshift(config.graphics);
launchArgs.unshift('--graphics');
}
// --player-version
if (config.playerVersion) {
let version = Math.floor(config.playerVersion);
if (version > 32) {
version = 32;
}
if (version < 1) {
version = 1;
}
launchArgs.unshift(config.playerVersion + '');
launchArgs.unshift('--player-version');
}
// --no-gui
if (config.noGui) { launchArgs.unshift('--no-gui'); }
// --fullscreen
if (config.fullscreen) { launchArgs.unshift('--fullscreen'); }
// -P (flashvars) (Stored as pairs e.g `foo=bar speed=fast mode="Full Screen"`)
if (config.flashVars) {
const keyValuePairs: FlashVar[] = [];
Expand All @@ -89,11 +150,9 @@ export class RuffleStandaloneMiddleware implements IGameMiddleware {
// Map config.flashVars to key value pairs
while ((match = regex.exec(config.flashVars))) {
const [_, key, value] = match;
// Remove quotes from the value if it's wrapped in quotes
const cleanedValue = value.replace(/^"(.*)"$/, '$1');
keyValuePairs.push({
key,
value: cleanedValue
value,
});
}
// Add all pairs to args
Expand All @@ -114,7 +173,14 @@ export class RuffleStandaloneMiddleware implements IGameMiddleware {
return gameLaunchInfo;
}

getConfigSchema(version: string, game: Game, config: GameConfig): ConfigSchema {
getDefaultConfig(game: Game): GameMiddlewareDefaultConfig {
return {
version: 'latest',
config: this.getConfigSchema('latest', game),
};
}

getConfigSchema(version: string, game: Game): ConfigSchema {
// Only 1 kind of schema for now
return schema;
}
Expand Down
7 changes: 6 additions & 1 deletion src/back/game/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { chunkArray } from '@shared/utils/misc';
import { GameConfig, GameOrderBy, GameOrderReverse, IGameMiddleware, ParsedSearch, Playlist, PlaylistGame } from 'flashpoint-launcher';
import * as fs from 'fs';
import * as path from 'path';
import { Brackets, EntityTarget, FindOneOptions, In, MoreThan, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
import { Brackets, EntityTarget, FindOneOptions, In, MoreThan, Not, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
import { AppDataSource } from '..';
import * as GameDataManager from './GameDataManager';
import * as TagManager from './TagManager';
Expand Down Expand Up @@ -375,6 +375,11 @@ export async function saveGameConfig(config: GameConfig, registry: Map<string, I
return loadGameConfig(savedRaw, registry);
}

export async function cleanupConfigs(game: Game, validConfigIds: number[]) {
const gameConfigRepository = AppDataSource.getRepository(RawGameConfig);
return gameConfigRepository.delete({ id: Not(In(validConfigIds)), gameId: game.id });
}

export async function removeGameAndAddApps(gameId: string, dataPacksFolderPath: string, imageFolderPath: string, htdocsFolderPath: string): Promise<Game | null> {
const gameRepository = AppDataSource.getRepository(Game);
const addAppRepository = AppDataSource.getRepository(AdditionalApp);
Expand Down
68 changes: 64 additions & 4 deletions src/back/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { throttle } from '@shared/utils/throttle';
import * as axiosImport from 'axios';
import * as child_process from 'child_process';
import { execSync } from 'child_process';
import { CurationState, GameLaunchInfo, GameMetadataSource, Platform } from 'flashpoint-launcher';
import { ConfigSchema, CurationState, GameLaunchInfo, GameMetadataSource, GameMiddlewareInfo, Platform } from 'flashpoint-launcher';
import * as fs from 'fs-extra';
import * as fs_extra from 'fs-extra';
import * as https from 'https';
Expand Down Expand Up @@ -608,18 +608,22 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
state.socketServer.register(BackIn.SAVE_GAME, async (event, info) => {
try {
// Save configs
for (const config of info.configs) {
await GameManager.saveGameConfig(config, state.registry.middlewares);
for (let i = 0; i < info.configs.length; i++) {
const config = info.configs[i];
info.configs[i] = await GameManager.saveGameConfig(config, state.registry.middlewares);
}
const validConfigIds = info.configs.map(c => c.id);
// Save game
if (info.activeConfig) {
if (info.activeConfig && validConfigIds.includes(info.activeConfig.id)) {
info.game.activeGameConfigId = info.activeConfig.id;
info.game.activeGameConfigOwner = info.activeConfig.owner;
} else {
info.game.activeGameConfigId = null;
info.game.activeGameConfigOwner = null;
}
const game = await GameManager.save(info.game);
// Clean up removed configs
await GameManager.cleanupConfigs(info.game, validConfigIds);
// Save up to date ids
state.queries = {}; // Clear entire cache
return {
Expand Down Expand Up @@ -745,6 +749,19 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
}
});

state.socketServer.register(BackIn.GET_VALID_MIDDLEWARE, (event, game) => {
const list: GameMiddlewareInfo[] = [];
for (const middleware of state.registry.middlewares.entries()) {
if (middleware[1].isValid(game)) {
list.push({
middlewareId: middleware[0],
name: middleware[1].name,
});
}
}
return list;
});

state.socketServer.register(BackIn.GET_GAME, async (event, id) => {
const game = await GameManager.findGame(id);
if (game === null) {
Expand All @@ -759,6 +776,49 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
};
});

state.socketServer.register(BackIn.GET_MIDDLEWARE_CONFIG_SCHEMAS, async (event, game, mIds) => {
const schemas: Record<string, ConfigSchema> = {};

for (const pair of mIds) {
// Find middleware in reg
const { id, version } = pair;
const middleware = state.registry.middlewares.get(id);
if (middleware) {
try {
schemas[`${id}-${version}`] = middleware.getConfigSchema(version, game);
} catch (err) {
log.error('Launcher', `Failed to load config schema for ${id} - ${err}`);
}
}
}

return schemas;
});

state.socketServer.register(BackIn.GET_MIDDLEWARE_DEFAULT_CONFIG, async (event, mId, game) => {
const middleware = state.registry.middlewares.get(mId);
if (middleware) {
try {
const defaultConfig = middleware.getDefaultConfig(game);
const schema = middleware.getConfigSchema(defaultConfig.version, game);
return {
config: {
...defaultConfig,
enabled: true,
middlewareId: middleware.id,
name: middleware.name
},
schema
};
} catch (err) {
log.error('Launcher', `Failed to get default config for ${mId} - ${err}`);
throw `Failed to get default config for ${mId} - ${err}`;
}
} else {
throw `Failed to find middleware for ${mId}`;
}
});

state.socketServer.register(BackIn.GET_GAME_DATA, async (event, id) => {
const gameData = await GameDataManager.findOne(id);
// Verify it's still on disk
Expand Down
11 changes: 9 additions & 2 deletions src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1684,8 +1684,15 @@ export class App extends React.Component<AppProps> {
this.props.setMainState({
currentGameInfo: info
});
// Save without frontend update
window.Shared.back.send(BackIn.SAVE_GAME, info);
// Save without immediate frontend update
window.Shared.back.request(BackIn.SAVE_GAME, info)
.then((newInfo) => {
if (newInfo.fetchedInfo) {
this.props.setMainState({
currentGameInfo: newInfo.fetchedInfo
});
}
});
}
};

Expand Down
14 changes: 9 additions & 5 deletions src/renderer/components/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type DropdownProps = {
children: React.ReactNode;
/** Text to show in the text field (always visible). */
text: string;
form?: boolean;
};

// A text element, with a drop-down element that can be shown/hidden.
Expand All @@ -35,20 +36,23 @@ export function Dropdown(props: DropdownProps) {
setExpanded(!expanded);
}
}, [expanded]);

const baseClass = props.form ? 'simple-dropdown-form' : 'simple-dropdown';

// Render
return (
<div className={`simple-dropdown ${props.className}`}>
<div className={`${baseClass} ${props.className}`}>
<div
className={`simple-dropdown__select-box ${props.headerClassName}`}
className={`${baseClass}__select-box ${props.headerClassName}`}
onMouseDown={onMouseDown}
tabIndex={0}>
<div className='simple-dropdown__select-text'>
<div className={`${baseClass}__select-text`}>
{ props.text }
</div>
<div className='simple-dropdown__select-icon' />
<div className={`${baseClass}__select-icon`} />
</div>
<div
className={'simple-dropdown__content' + (expanded ? '' : ' simple-dropdown__content--hidden')}
className={`${baseClass}__content` + (expanded ? '' : ` ${baseClass}__content--hidden`)}
onMouseUp={() => setExpanded(false)}
ref={contentRef}>
{ props.children }
Expand Down
Loading

0 comments on commit 64ee874

Please sign in to comment.