Skip to content

Commit

Permalink
Merge branch 'master' of github.com:SteamGridDB/steam-rom-manager
Browse files Browse the repository at this point in the history
  • Loading branch information
cbartondock committed Jul 18, 2024
2 parents c9e419b + f5b5287 commit 104ec67
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 59 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ In addition to flexible importing of ROMS, SRM now has several *platform parsers
|[Amazon Games](https://gaming.amazon.com/amazon-games-app)||🟦|🟦|<ul><li>Launch via Amazon Games</li><li>Launch via executable</li>|
|[EA Desktop](https://www.ea.com/ea-app)||🟦|🟦|<ul><li>Launch via EA Desktop</li><li>Launch via executable</li>|
|[Epic](https://store.epicgames.com/en-US/)|||🟦|<ul><li>Launch via Epic</li><li>Launch via executable</li>|
|[GOG Galaxy](https://www.gog.com/galaxy)||||🟦|<ul><li>Launch via GOG Galaxy</li><li>Launch via executable</li>|
|[GOG Galaxy](https://www.gog.com/galaxy)|||🟦|<ul><li>Launch via GOG Galaxy</li><li>Launch via executable</li>|
|[Itch.io](https://itch.io/app)||||<ul><li>Launch via executable</li></ul>|
|[Legendary](https://github.com/derrod/legendary)||||<ul><li>Launch via executable</li></ul>|
|[Ubisoft Connect](https://ubisoftconnect.com/en-US/)|||🟦|<ul><li>Launch via Ubisoft Connect</li><li>Launch via executable</li>|
Expand Down
1 change: 1 addition & 0 deletions src/lang/en-US/langStrings.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
"galaxyExeOverridePlaceholderMac": "/path/to/GOG Galaxy.app/Contents/MacOS/GOG Galaxy",
"launcherModeInputTitle": "Launch games via GOG Galaxy",
"parseLinkedExecsTitle": "Parse linked executables from GOG Galaxy",
"parseRegistryEntries": "Parse using Registry instead of Galaxy DB",
"errors": {
"invalidGalaxyExeOverride": "> Galaxy Client Override is not a valid path.",
"fatalError__i": "> GOG Galaxy parser failed with fatal error:\n ${error}",
Expand Down
7 changes: 4 additions & 3 deletions src/lang/en-US/markdown/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ If you enjoy Steam ROM Manager and want it to continue to be useful also conside
* `lexarvn`{.noWrap} - Added the Amazon Games parser and the itch.io parser.
* `CarJem`{.noWrap} - Added the manual parser.
* `MattMckenzy`{.noWrap} - Added ability to import and export image choices.
* `Apalatn`{.noWrap} - Added an install drive redirect option to the itch.io parser.
* `OneMoreByte` - Made itch.io parser work on linux and mac.
* `UndarkAido` - Added shortcut passthrough for Linux's .desktop shortcuts.
* `Apalatn`{.noWrap} - Added an install drive redirect option to the itch.io parser.
* `OneMoreByte`{.noWrap} - Made itch.io parser work on linux and mac.
* `UndarkAido`{.noWrap} - Added shortcut passthrough for Linux's .desktop shortcuts.
* `HazardousBackup`{.noWrap} - Added option to GOG parser to parse Registry instead of GOG's DB.

### Community
* `HE Spoke`{.noWrap} - created a community around SRM. Creator of [Steam](https://steamcommunity.com/groups/steamrommanager) and [Discord](https://discord.gg/bnSVJrz) groups. Also helps users setup SRM in [Discord](https://discord.gg/bnSVJrz).
Expand Down
7 changes: 5 additions & 2 deletions src/lang/en-US/markdown/gog-parser-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ By default Steam ROM Manager assumes your GOG Galaxy executable is located at `C

Specifying the correct location of GOG Galaxy's executable is only necessary if you enable launch via GOG Galaxy (see below), as otherwise SRM has no need of the location of GOG Galaxy's executable.

## Launch Via GOG Galaxy `[Recommend disabled]`
## Launch via GOG Galaxy `[Recommend disabled]`

What it sounds like, this toggle determines whether games launch via GOG Galaxy or directly. For some games launching from GOG Galaxy may fail, and the Steam overlay will most likely not work.

## Parse Linked Executables from GOG Galaxy

If enabled, SRM will pull in not only GOG games aquired from GOG Galaxy's store, but also those you have manually linked executables for in GOG Galaxy. This is desirable if those games are not being parsed into SRM elsewhere.

A caveat is that because GOG Galaxy does not store the names linked executables in its database, SRM will use the directory name of the executable on Windows (e.g. `C:\\path\\to\\Hoa\\LaunchHoa.exe` would be assigned the title `Hoa`) and the executable name on Mac (e.g. `/Applications/Symphonia.app` would be assigned the title `Symphonia`).
A caveat is that because GOG Galaxy does not store the names linked executables in its database, SRM will use the directory name of the executable on Windows (e.g. `C:\\path\\to\\Hoa\\LaunchHoa.exe` would be assigned the title `Hoa`) and the executable name on Mac (e.g. `/Applications/Symphonia.app` would be assigned the title `Symphonia`).

## Parse using Registry instead of Galaxy DB `[Windows only] [Recommend disabled]`
This option will parse the Windows Registry for installed GOG games instead of GOG Galaxy's SQL database, allowing the parser to work even if GOG Galaxy is not installed. If this is enabled, `Parse Linked Executables` will of have no effect, but `Launch via GOG Galaxy` will work as normal.
2 changes: 1 addition & 1 deletion src/lib/parsers/available-parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const availableParserInputs: Record<ParserType, string[]> = {
'Amazon Games': ['amazonGamesExeOverride', 'amazonGamesLauncherMode'],
'Epic': ['epicManifests', 'epicLauncherMode'],
'Legendary': ['legendaryInstalledFile'],
'GOG Galaxy': ['galaxyExeOverride','gogLauncherMode','parseLinkedExecs'],
'GOG Galaxy': ['galaxyExeOverride','gogLauncherMode','parseLinkedExecs','parseRegistryEntries'],
'itch.io': ['itchIoAppDataOverride','itchIoWindowsOnLinuxInstallDriveRedirect'],
'Steam': ['appTypes','onlyInstalled'],
'UPlay': ['uplayDir','uplayLauncherMode'],
Expand Down
139 changes: 110 additions & 29 deletions src/lib/parsers/gog-galaxy.parser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ParserInfo, GenericParser, ParsedData } from '../../models';
import { ParserInfo, GenericParser, ParsedData, ParsedSuccess } from '../../models';
import { APP } from '../../variables';
import * as _ from "lodash";
import * as fs from "fs-extra";
import * as path from "path";
import * as os from "os";
import { SqliteWrapper } from "../helpers/sqlite";
import Registry from "winreg";

export class GOGParser implements GenericParser {

Expand Down Expand Up @@ -34,11 +35,82 @@ export class GOGParser implements GenericParser {
inputType: 'toggle',
validationFn: (input: any) => { return null },
info: this.lang.docs__md.input.join('')
},
'parseRegistryEntries': {
label: this.lang.parseRegistryEntries,
inputType: 'toggle',
hidden: os.type()!='Windows_NT',
validationFn: (input: any) => { return null },
info: this.lang.docs__md.input.join('')
}
}
};
}

private processRegKey(regkey: Registry.Registry){
return new Promise<{success: ParsedSuccess, failMessage: string}>((resolve, reject) => {
regkey.values((err: Error, values: Registry.RegistryItem[]) => {
if (err) {
return reject(err);
}
if (values) {
const transformed = Object.fromEntries(values.filter(entry => entry.name && entry.value)
.map(entry=>[entry.name, entry.value]));
if(transformed.gameName && transformed.gameID && transformed.launchCommand && transformed.workingDir) {
if(transformed.dependsOn) { // ignore DLC
resolve({success: null, failMessage: `Skipping DLC: ${transformed.gameName}`})
} else {
if(!fs.existsSync(transformed.launchCommand)) {
return resolve({success: null, failMessage: `Skipping Missing Executable: ${transformed.gameName}`})
}
return resolve({success: {
extractedTitle: transformed.gameName,
extractedAppId: transformed.gameID,
launchOptions: `/command=runGame /gameId=${transformed.gameID}`,
filePath: transformed.launchCommand,
fileLaunchOptions: transformed.launchParam,
startInDirectory: transformed.workingDir
}, failMessage: null})
}
} else {
resolve(null)
}
}
});
});
}

private getRegInstalled(executableLocation: string){
return new Promise<ParsedData>((resolve, reject) => {
const rootkey: string = "\\SOFTWARE\\WOW6432Node\\GOG.com\\Games"
const reg = new Registry({
hive: Registry.HKLM,
key: rootkey,
});

reg.keys((err: Error, regkeys: Registry.Registry[]) => {
if (err) {
return reject(err);
}
if (regkeys) {
const promiseArr = regkeys.map((regkey) => this.processRegKey(regkey))
Promise.all(promiseArr).then((parsedArray) => {
let parsedData: ParsedData = {
executableLocation: executableLocation,
success: parsedArray.filter(x=> x&&!x.failMessage).map(x=>x.success),
failed: parsedArray.filter(x=>x&&x.failMessage).map(x=>x.failMessage)
}
return resolve(parsedData);
}).catch((err) => {
return reject(err)
});
} else {
return resolve( {success: [], failed: [] });
}
});
});
}

execute(directories: string[], inputs: { [key: string]: any }, cache?: { [key: string]: any }) {
return new Promise<ParsedData>(async (resolve,reject)=>{
let dbPath, galaxyExePath: string;
Expand All @@ -54,38 +126,47 @@ export class GOGParser implements GenericParser {
if(inputs.galaxyExeOverride) {
galaxyExePath = inputs.galaxyExeOverride
}
if(!fs.existsSync(dbPath)) {
return reject(this.lang.errors.gogNotInstalled);
}
try {
const sqliteWrapper = new SqliteWrapper('gog-galaxy', dbPath, {externals: !!inputs.parseLinkedExecs});
const playtasks = await sqliteWrapper.callWorker() as any[];
let parsedData: ParsedData = {success: [], failed:[]};
parsedData.executableLocation = galaxyExePath;
for(let task of playtasks) {
if(task.params.executablePath) {
const productID = task.productId.toString();
const flag = os.type() == 'Windows_NT' ? '/' : '--';
let fallbackTitle;
if(os.type() == 'Windows_NT') {
fallbackTitle = path.dirname(task.params.executablePath).split(path.sep).pop();
} else {
fallbackTitle = task.params.executablePath.split(path.sep).pop().slice().replace(/\.[^.]*$/, '');
if(inputs.parseRegistryEntries && os.type()=='Windows_NT'){
this.getRegInstalled(galaxyExePath).then((parsedData) => {
parsedData.executableLocation = galaxyExePath;
resolve(parsedData);
}).catch((err)=>{
reject(this.lang.errors.fatalError__i.interpolate({error: err}));
});
} else {
if(!fs.existsSync(dbPath)) {
return reject(this.lang.errors.gogNotInstalled);
}
try {
const sqliteWrapper = new SqliteWrapper('gog-galaxy', dbPath, {externals: !!inputs.parseLinkedExecs});
const playtasks = await sqliteWrapper.callWorker() as any[];
let parsedData: ParsedData = {success: [], failed:[]};
parsedData.executableLocation = galaxyExePath;
for(let task of playtasks) {
if(task.params.executablePath) {
const productID = task.productId.toString();
const flag = os.type() == 'Windows_NT' ? '/' : '--';
let fallbackTitle;
if(os.type() == 'Windows_NT') {
fallbackTitle = path.dirname(task.params.executablePath).split(path.sep).pop();
} else {
fallbackTitle = task.params.executablePath.split(path.sep).pop().slice().replace(/\.[^.]*$/, '');
}
parsedData.success.push({
extractedTitle: task.title || fallbackTitle,
extractedAppId: productID,
launchOptions: `${flag}command=runGame ${flag}gameId=${productID}`,
filePath: task.params.executablePath,
fileLaunchOptions: task.params.commandLineArgs
})
}
parsedData.success.push({
extractedTitle: task.title || fallbackTitle,
extractedAppId: productID,
launchOptions: `${flag}command=runGame ${flag}gameId=${productID}`,
filePath: task.params.executablePath,
fileLaunchOptions: task.params.commandLineArgs
})
}
resolve(parsedData);
}
resolve(parsedData);
catch(err) {
reject(this.lang.errors.fatalError__i.interpolate({error: err}));
};
}
catch(err) {
reject(this.lang.errors.fatalError__i.interpolate({error: err}));
};
});
}
}
1 change: 1 addition & 0 deletions src/models/language.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ export interface languageStruct {
galaxyExeOverridePlaceholderMac: string,
launcherModeInputTitle: string,
parseLinkedExecsTitle: string,
parseRegistryEntries: string,
docs__md: {
self: string[],
input: string[]
Expand Down
1 change: 1 addition & 0 deletions src/models/parser.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface ParserInputField {
inputType: 'text' | 'path' | 'dir' | 'toggle' | 'multiselect',
allowedValues?: SelectItem[],
initialValue?: string[],
hidden?: boolean,
required?: boolean,
info?: string,
forcedInput?: string,
Expand Down
37 changes: 14 additions & 23 deletions src/renderer/components/parsers.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ export class ParsersComponent implements AfterViewInit, OnDestroy {
let inputFieldName: keyof typeof parser.inputs;
for (inputFieldName in parser.inputs) {
let input = parser.inputs[inputFieldName];
const isHidden = () => {
return concat(of(this.userForm.get('parserType').value), this.userForm.get('parserType').valueChanges).pipe(map((pType: string) => {
return input.hidden || (pType !== parsers[i]);
}));
}
const onInfoClick = (self: any, path: string[]) => {
this.currentDoc.activePath = path.join();
this.currentDoc.content = input.info;
}
if(['path','dir','text'].includes(input.inputType)) {
parserInputs[inputFieldName] = new NestedFormElement.Input({
path: ['path','dir'].includes(input.inputType) ? {
Expand All @@ -259,30 +268,19 @@ export class ParsersComponent implements AfterViewInit, OnDestroy {
initialValue: input.forcedInput !== undefined ? input.forcedInput : null,
highlight: this.highlight.bind(this),
label: input.label,
isHidden: () => {
return concat(of(this.userForm.get('parserType').value), this.userForm.get('parserType').valueChanges).pipe(map((pType: string) => {
return pType !== parsers[i];
}));
},
isHidden: isHidden,
onValidate: (self, path) => {
if (parserInfo.superTypesMap[parsers[i]] !== parserInfo.ArtworkOnlyType && this.userForm.get('parserType').value === parsers[i])
return this.parsersService.validate(path[0] as keyof UserConfiguration, { parser: parsers[i], input: inputFieldName, inputData: self.value });
else
return null;
},
onInfoClick: (self, path) => {
this.currentDoc.activePath = path.join();
this.currentDoc.content = input.info;
}
onInfoClick: onInfoClick
})
} else if (input.inputType == 'toggle') {
parserInputs[inputFieldName] = new NestedFormElement.Toggle({
text: input.label,
isHidden: () => {
return concat(of(this.userForm.get('parserType').value), this.userForm.get('parserType').valueChanges).pipe(map((pType: string) => {
return pType !== parsers[i];
}));
},
isHidden: isHidden,
});
} else if(input.inputType == 'multiselect') {
parserInputs[inputFieldName] = new NestedFormElement.Select({
Expand All @@ -291,15 +289,8 @@ export class ParsersComponent implements AfterViewInit, OnDestroy {
allowEmpty: false,
values: input.allowedValues,
initialValue: input.initialValue,
onInfoClick: (self, path) => {
this.currentDoc.activePath = path.join();
this.currentDoc.content = input.info;
},
isHidden: () => {
return concat(of(this.userForm.get('parserType').value), this.userForm.get('parserType').valueChanges).pipe(map((pType: string) => {
return pType !== parsers[i];
}));
},
onInfoClick: onInfoClick,
isHidden: isHidden,
})
}
}
Expand Down

0 comments on commit 104ec67

Please sign in to comment.