Skip to content

[Feat] Support Epic's launch-able addons/DLCs #3317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/backend/api/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export const getGameInfo = async (appName: string, runner: Runner) =>
export const getExtraInfo = async (appName: string, runner: Runner) =>
ipcRenderer.invoke('getExtraInfo', appName, runner)

export const getGOGLaunchOptions = async (appName: string) =>
ipcRenderer.invoke('getGOGLaunchOptions', appName)
export const getLaunchOptions = async (appName: string, runner: Runner) =>
ipcRenderer.invoke('getLaunchOptions', appName, runner)

export const getGameSettings = async (
appName: string,
Expand Down
15 changes: 11 additions & 4 deletions src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,10 @@ let powerDisplayId: number | null
// get pid/tid on launch and inject
ipcMain.handle(
'launch',
async (event, { appName, launchArguments, runner }): StatusPromise => {
async (
event,
{ appName, launchArguments, runner, skipVersionCheck }
): StatusPromise => {
const game = gameManagerMap[runner].getGameInfo(appName)
const gameSettings = await gameManagerMap[runner].getSettings(appName)
const { autoSyncSaves, savesPath, gogSaves = [] } = gameSettings
Expand Down Expand Up @@ -1084,7 +1087,11 @@ ipcMain.handle(
status: 'playing'
})

const command = gameManagerMap[runner].launch(appName, launchArguments)
const command = gameManagerMap[runner].launch(
appName,
launchArguments,
skipVersionCheck
)

const launchResult = await command.catch((exception) => {
logError(exception, LogPrefix.Backend)
Expand Down Expand Up @@ -1460,8 +1467,8 @@ ipcMain.handle('syncGOGSaves', async (event, gogSaves, appName, arg) =>
gameManagerMap['gog'].syncSaves(appName, arg, '', gogSaves)
)

ipcMain.handle('getGOGLaunchOptions', async (event, appName: string) =>
GOGLibraryManager.getLaunchOptions(appName)
ipcMain.handle('getLaunchOptions', async (event, appName, runner) =>
libraryManagerMap[runner].getLaunchOptions(appName)
)

ipcMain.handle(
Expand Down
10 changes: 7 additions & 3 deletions src/backend/storeManagers/gog/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import {
InstallArgs,
InstalledInfo,
InstallPlatform,
InstallProgress
InstallProgress,
LaunchOption,
BaseLaunchOption
} from 'common/types'
import { appendFileSync, existsSync, rmSync } from 'graceful-fs'
import { gamesConfigPath, isWindows, isMac, isLinux } from '../../constants'
Expand Down Expand Up @@ -404,7 +406,7 @@ export async function removeShortcuts(appName: string) {

export async function launch(
appName: string,
launchArguments?: string
launchArguments?: LaunchOption
): Promise<boolean> {
const gameSettings = await getSettings(appName)
const gameInfo = getGameInfo(appName)
Expand Down Expand Up @@ -515,7 +517,9 @@ export async function launch(
...wineFlag,
'--platform',
gameInfo.install.platform.toLowerCase(),
...shlex.split(launchArguments ?? ''),
...shlex.split(
(launchArguments as BaseLaunchOption | undefined)?.parameters ?? ''
),
...shlex.split(gameSettings.launcherArgs ?? '')
]

Expand Down
17 changes: 13 additions & 4 deletions src/backend/storeManagers/legendary/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
InstallArgs,
InstallPlatform,
InstallProgress,
WineCommandArgs
WineCommandArgs,
LaunchOption
} from 'common/types'
import { GameConfig } from '../../game_config'
import { GlobalConfig } from '../../config'
Expand Down Expand Up @@ -752,7 +753,8 @@ export async function syncSaves(

export async function launch(
appName: string,
launchArguments?: string
launchArguments?: LaunchOption,
skipVersionCheck = false
): Promise<boolean> {
const gameSettings = await getSettings(appName)
const gameInfo = getGameInfo(appName)
Expand Down Expand Up @@ -838,14 +840,21 @@ export async function launch(
wineFlags = getWineFlags(wineBin, wineType, shlex.join(wrappers))
}

const appNameToLaunch =
launchArguments?.type === 'dlc' ? launchArguments.dlcAppName : appName

const command: LegendaryCommand = {
subcommand: 'launch',
appName: LegendaryAppName.parse(appName),
extraArguments: [launchArguments, gameSettings.launcherArgs]
appName: LegendaryAppName.parse(appNameToLaunch),
extraArguments: [
launchArguments?.type !== 'dlc' ? launchArguments?.parameters : undefined,
gameSettings.launcherArgs
]
.filter(Boolean)
.join(' '),
...wineFlags
}
if (skipVersionCheck) command['--skip-version-check'] = true
if (languageCode) command['--language'] = NonEmptyString.parse(languageCode)
if (gameSettings.targetExe)
command['--override-exe'] = Path.parse(gameSettings.targetExe)
Expand Down
89 changes: 70 additions & 19 deletions src/backend/storeManagers/legendary/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
InstalledInfo,
CallRunnerOptions,
ExecResult,
InstallPlatform
InstallPlatform,
LaunchOption
} from 'common/types'
import {
InstalledJsonMetadata,
Expand Down Expand Up @@ -204,7 +205,7 @@ export function getGameInfo(
}
// We have the game, but info wasn't loaded yet
if (!library.has(appName) || forceReload) {
loadFile(appName + '.json')
loadFile(appName)
}
return library.get(appName)
}
Expand Down Expand Up @@ -449,7 +450,7 @@ export function installState(appName: string, state: boolean) {
if (state) {
// This assumes that fileName and appName are same.
// If that changes, this will break.
loadFile(`${appName}.json`)
loadFile(appName)
} else {
// @ts-expect-error TODO: Make sure game info is loaded & appName is valid here
library.get(appName).is_installed = false
Expand All @@ -459,23 +460,24 @@ export function installState(appName: string, state: boolean) {
}
}

function loadGameMetadata(appName: string): GameMetadata {
const fullPath = join(legendaryMetadata, appName + '.json')
return JSON.parse(readFileSync(fullPath, 'utf-8'))
}

/**
* Load the file completely into our in-memory library.
* Largely derived from legacy code.
*
* @returns True/False, whether or not the file was loaded
*/
function loadFile(fileName: string): boolean {
const fullPath = join(legendaryMetadata, fileName)

let app_name: string
function loadFile(app_name: string): boolean {
let metadata
try {
const data: GameMetadata = JSON.parse(readFileSync(fullPath, 'utf-8'))
app_name = data.app_name
const data = loadGameMetadata(app_name)
metadata = data.metadata
} catch (error) {
logError(['Failed to parse', fileName], LogPrefix.Legendary)
logError(['Failed to parse metadata for', app_name], LogPrefix.Legendary)
return false
}
const { namespace } = metadata
Expand Down Expand Up @@ -503,10 +505,7 @@ function loadFile(fileName: string): boolean {
}

if (!customAttributes) {
logWarning(
['Incomplete metadata for', fileName, app_name],
LogPrefix.Legendary
)
logWarning(['Incomplete metadata for', app_name], LogPrefix.Legendary)
}

const dlcs: string[] = []
Expand Down Expand Up @@ -559,10 +558,7 @@ function loadFile(fileName: string): boolean {
const convertedSize = install_size ? getFileSize(Number(install_size)) : '0'

if (releaseInfo && !releaseInfo[0].platform) {
logWarning(
['No platforms info for', fileName, app_name],
LogPrefix.Legendary
)
logWarning(['No platforms info for', app_name], LogPrefix.Legendary)
}

let metadataPlatform: LegendaryInstallPlatform[] = []
Expand Down Expand Up @@ -624,7 +620,7 @@ async function loadAll(): Promise<string[]> {
if (existsSync(legendaryMetadata)) {
const loadedFiles: string[] = []
allGames.forEach((appName) => {
const wasLoaded = loadFile(appName + '.json')
const wasLoaded = loadFile(appName)
if (wasLoaded) {
loadedFiles.push(appName)
}
Expand Down Expand Up @@ -845,3 +841,58 @@ export function commandToArgsArray(command: LegendaryCommand): string[] {

return commandParts
}

export async function getLaunchOptions(
appName: string
): Promise<LaunchOption[]> {
const gameInfo = getGameInfo(appName)
const installPlatform = gameInfo?.install.platform
if (!installPlatform) return []

const installInfo = await getInstallInfo(appName, installPlatform)
const launchOptions: LaunchOption[] = installInfo.game.launch_options

// Some DLCs are also launch-able
for (const dlc of installInfo.game.owned_dlc) {
const installedInfo = installedGames.get(dlc.app_name)
if (!installedInfo) continue

// If the DLC itself is executable, push it onto the list
if (installedInfo.executable) {
launchOptions.push({
type: 'dlc',
dlcAppName: dlc.app_name,
dlcTitle: dlc.title
})
// The one example we've found using this (Unreal Editor for Fortnite)
// suggests that we should not look at the AdditionalCommandLine custom
// attribute (below) if this is set
continue
}

// Otherwise, if it specifies additional commandline parameters to pass to
// the main game, add it as a basic launch option
let metadata
try {
metadata = loadGameMetadata(dlc.app_name)
} catch (e) {
logWarning(
[
'Failed to load DLC metadata for',
dlc.app_name,
'(base game is',
`${appName})`
],
LogPrefix.Legendary
)
}
if (!metadata?.metadata.customAttributes?.AdditionalCommandLine) continue
launchOptions.push({
type: 'basic',
name: dlc.title,
parameters: metadata.metadata.customAttributes.AdditionalCommandLine.value
})
}

return launchOptions
}
10 changes: 7 additions & 3 deletions src/backend/storeManagers/nile/games.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
BaseLaunchOption,
ExecResult,
ExtraInfo,
GameInfo,
GameSettings,
InstallArgs,
InstallPlatform,
InstallProgress
InstallProgress,
LaunchOption
} from 'common/types'
import { InstallResult, RemoveArgs } from 'common/types/game_manager'
import {
Expand Down Expand Up @@ -302,7 +304,7 @@ export async function removeShortcuts(appName: string) {

export async function launch(
appName: string,
launchArguments?: string
launchArguments?: LaunchOption
): Promise<boolean> {
const gameSettings = await getSettings(appName)
const gameInfo = getGameInfo(appName)
Expand Down Expand Up @@ -397,7 +399,9 @@ export async function launch(
'launch',
...exeOverrideFlag, // Check if this works
...wineFlag,
...shlex.split(launchArguments ?? ''),
...shlex.split(
(launchArguments as BaseLaunchOption | undefined)?.parameters ?? ''
),
...shlex.split(gameSettings.launcherArgs ?? ''),
appName
]
Expand Down
2 changes: 2 additions & 0 deletions src/backend/storeManagers/nile/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,5 @@ export async function runRunnerCommand(
}
)
}

export const getLaunchOptions = () => []
5 changes: 3 additions & 2 deletions src/backend/storeManagers/sideload/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
GameInfo,
GameSettings,
InstallArgs,
InstallPlatform
InstallPlatform,
LaunchOption
} from 'common/types'
import { libraryStore } from './electronStores'
import { GameConfig } from '../../game_config'
Expand Down Expand Up @@ -69,7 +70,7 @@ export async function isGameAvailable(appName: string): Promise<boolean> {
export async function launch(
appName: string,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
launchArguments?: string
launchArguments?: LaunchOption
): Promise<boolean> {
return launchGame(appName, getGameInfo(appName), 'sideload')
}
Expand Down
2 changes: 2 additions & 0 deletions src/backend/storeManagers/sideload/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,5 @@ export async function getInstallInfo(
logWarning(`getInstallInfo not implemented on Sideload Library Manager`)
return undefined
}

export const getLaunchOptions = () => []
14 changes: 7 additions & 7 deletions src/common/typedefs/ipcBridge.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DownloadManagerState } from './../types'
import { EventEmitter } from 'node:events'
import {
IpcMainEvent,
Expand Down Expand Up @@ -34,12 +33,13 @@ import {
ConnectivityStatus,
GamepadActionArgs,
ExtraInfo,
LaunchOption
LaunchOption,
DownloadManagerState,
InstallInfo
} from 'common/types'
import { LegendaryInstallInfo, SelectiveDownload } from 'common/types/legendary'
import { GOGCloudSavesLocation, GogInstallInfo } from 'common/types/gog'
import { SelectiveDownload } from 'common/types/legendary'
import { GOGCloudSavesLocation } from 'common/types/gog'
import {
NileInstallInfo,
NileLoginData,
NileRegisterData,
NileUserData
Expand Down Expand Up @@ -161,7 +161,7 @@ interface AsyncIPCFunctions {
appName: string,
runner: Runner,
installPlatform: InstallPlatform
) => Promise<LegendaryInstallInfo | GogInstallInfo | NileInstallInfo | null>
) => Promise<InstallInfo | null>
getUserInfo: () => Promise<UserInfo | undefined>
getAmazonUserInfo: () => Promise<NileUserData | undefined>
isLoggedIn: () => boolean
Expand Down Expand Up @@ -277,7 +277,7 @@ interface AsyncIPCFunctions {
toggleVKD3D: (args: ToolArgs) => Promise<boolean>
toggleDXVKNVAPI: (args: ToolArgs) => Promise<boolean>
pathExists: (path: string) => Promise<boolean>
getGOGLaunchOptions: (appName: string) => Promise<LaunchOption[]>
getLaunchOptions: (appName: string, runner: Runner) => Promise<LaunchOption[]>
getGameOverride: () => Promise<GameOverride | null>
getGameSdl: (appName: string) => Promise<SelectiveDownload[]>
getPlaytimeFromRunner: (
Expand Down
Loading