Skip to content
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

[EPIC]: Support third-party managed applications #3763

Merged
merged 5 commits into from
Jun 9, 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
6 changes: 6 additions & 0 deletions public/locales/en/gamepage.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
"size": "Size",
"steamdeck-compatibility-info": "SteamDeck Compatibility",
"syncsaves": "Sync Saves",
"third-party-app": "Third-Party Manager",
"version": "Version"
},
"install": {
Expand Down Expand Up @@ -271,5 +272,10 @@
"settings": "Settings",
"store": "Store Page",
"verify": "Verify and Repair"
},
"third-party-managed": {
"header": "This game is managed by a third-party application",
"notice1": "This game is managed by a third-party application: \"{{application_name}}\"",
"notice2": "After clicking Install, Heroic will run the application in order to complete the installation process"
}
}
1 change: 1 addition & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@
"show_hidden": "Show Hidden",
"show_installed_only": "Show Installed only",
"show_support_offline_only": "Show offline-supported only",
"show_third_party_managed_only": "Show third-party managed only",
"uncategorized": "Uncategorized"
},
"help": {
Expand Down
9 changes: 8 additions & 1 deletion src/backend/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const nileConfigPath = join(appFolder, 'nile_config', 'nile')
const configPath = join(appFolder, 'config.json')
const gamesConfigPath = join(appFolder, 'GamesConfig')
const toolsPath = join(appFolder, 'tools')
const epicRedistPath = join(toolsPath, 'redist', 'legendary')
const gogRedistPath = join(toolsPath, 'redist', 'gog')
const heroicIconFolder = join(appFolder, 'icons')
const runtimePath = join(toolsPath, 'runtimes')
Expand Down Expand Up @@ -90,6 +91,10 @@ const icon = fixAsarPath(join(publicDir, 'icon.png'))
const iconDark = fixAsarPath(join(publicDir, 'icon-dark.png'))
const iconLight = fixAsarPath(join(publicDir, 'icon-light.png'))
const installed = join(legendaryConfigPath, 'installed.json')
const thirdPartyInstalled = join(
legendaryConfigPath,
'third-party-installed.json'
)
const legendaryMetadata = join(legendaryConfigPath, 'metadata')
const nileInstalled = join(nileConfigPath, 'installed.json')
const nileLibrary = join(nileConfigPath, 'library.json')
Expand Down Expand Up @@ -279,10 +284,12 @@ export {
gogdlConfigPath,
gogSupportPath,
gogRedistPath,
epicRedistPath,
vulkanHelperBin,
nileConfigPath,
nileInstalled,
nileLibrary,
nileUserData,
fixesPath
fixesPath,
thirdPartyInstalled
}
55 changes: 33 additions & 22 deletions src/backend/downloadmanager/downloadqueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,30 +126,41 @@ async function addToQueue(element: DMQueueElement) {
if (elementIndex >= 0) {
elements[elementIndex] = element
} else {
const installInfo = await libraryManagerMap[
element.params.runner
].getInstallInfo(element.params.appName, element.params.platformToInstall, {
branch: element.params.branch,
build: element.params.build
})
const gameInfo = libraryManagerMap[element.params.runner].getGameInfo(
element.params.appName
)
if (!gameInfo?.isEAManaged) {
const installInfo = await libraryManagerMap[
element.params.runner
].getInstallInfo(
element.params.appName,
element.params.platformToInstall,
{
branch: element.params.branch,
build: element.params.build
}
)

element.params.size = installInfo?.manifest?.download_size
? getFileSize(installInfo?.manifest?.download_size)
: '?? MB'

if (
element.params.runner === 'gog' &&
element.params.platformToInstall.toLowerCase() === 'windows' &&
installInfo &&
'dependencies' in installInfo.manifest
) {
const newDependencies = installInfo.manifest.dependencies || []
if (newDependencies?.length || !existsSync(gogRedistPath)) {
// create redist element
const redistElement = createRedistDMQueueElement()
redistElement.params.dependencies = newDependencies
elements.push(redistElement)
element.params.size = installInfo?.manifest?.download_size
? getFileSize(installInfo?.manifest?.download_size)
: '?? MB'

if (
element.params.runner === 'gog' &&
element.params.platformToInstall.toLowerCase() === 'windows' &&
installInfo &&
'dependencies' in installInfo.manifest
) {
const newDependencies = installInfo.manifest.dependencies || []
if (newDependencies?.length || !existsSync(gogRedistPath)) {
// create redist element
const redistElement = createRedistDMQueueElement()
redistElement.params.dependencies = newDependencies
elements.push(redistElement)
}
}
} else {
element.params.size = '?? MB'
}
elements.push(element)
}
Expand Down
86 changes: 80 additions & 6 deletions src/backend/storeManagers/legendary/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@ import {
} from './library'
import { LegendaryUser } from './user'
import {
downloadFile,
getLegendaryBin,
killPattern,
moveOnUnix,
moveOnWindows,
sendGameStatusUpdate,
sendProgressUpdate,
shutdownWine
shutdownWine,
spawnAsync
} from '../../utils'
import {
isMac,
isWindows,
installed,
configStore,
isCLINoGui,
isLinux
isLinux,
epicRedistPath
} from '../../constants'
import {
appendGamePlayLog,
Expand Down Expand Up @@ -83,7 +86,9 @@ import {
PositiveInteger
} from './commands/base'
import { LegendaryCommand } from './commands'
import thirdParty from './thirdParty'
import { Path } from 'backend/schemas'
import { mkdirSync } from 'fs'

/**
* Alias for `LegendaryLibrary.listUpdateableGames`
Expand Down Expand Up @@ -144,9 +149,13 @@ async function getProductSlug(namespace: string, title: string) {
}

try {
const result = await axios('https://www.epicgames.com/graphql', {
const result = await axios('https://launcher.store.epicgames.com/graphql', {
data: graphql,
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) EpicGamesLauncher'
},
method: 'POST'
})

Expand Down Expand Up @@ -233,9 +242,13 @@ async function getExtraFromGraphql(
}

try {
const result = await axios('https://www.epicgames.com/graphql', {
const result = await axios('https://launcher.store.epicgames.com/graphql', {
data: graphql,
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) EpicGamesLauncher'
},
method: 'POST'
})

Expand Down Expand Up @@ -289,6 +302,7 @@ const emptyExtraInfo = {
export async function getExtraInfo(appName: string): Promise<ExtraInfo> {
const { namespace, title } = getGameInfo(appName)
if (namespace === undefined) return emptyExtraInfo

const cachedExtraInfo = gameInfoStore.get(namespace)
if (cachedExtraInfo) {
return cachedExtraInfo
Expand Down Expand Up @@ -571,6 +585,18 @@ export async function install(
status: 'done' | 'error' | 'abort'
error?: string
}> {
const gameInfo = getGameInfo(appName)
if (gameInfo.thirdPartyManagedApp) {
if (!gameInfo.isEAManaged) {
logError(
['Third party app', gameInfo.thirdPartyManagedApp, 'not supported'],
LogPrefix.Legendary
)
return { status: 'error' }
}

return installEA(gameInfo, platformToInstall)
}
const { maxWorkers, downloadNoHttps } = GlobalConfig.get().getSettings()
const info = await getInstallInfo(appName, platformToInstall)

Expand Down Expand Up @@ -634,7 +660,54 @@ export async function install(
return { status: 'done' }
}

async function installEA(
gameInfo: GameInfo,
platformToInstall: string
): Promise<{
status: 'done' | 'error' | 'abort'
error?: string
}> {
logInfo('Getting EA App installer', LogPrefix.Legendary)
const installerPath = join(epicRedistPath, 'EAappInstaller.exe')

if (!existsSync(epicRedistPath)) {
mkdirSync(epicRedistPath, { recursive: true })
}

if (!existsSync(installerPath)) {
try {
await downloadFile({
url: 'https://origin-a.akamaihd.net/EA-Desktop-Client-Download/installer-releases/EAappInstaller.exe',
dest: installerPath
})
} catch (e) {
return { status: 'error', error: `${e}` }
}
}

if (isWindows) {
const process = await spawnAsync(installerPath, [
'EAX_LAUNCH_CLIENT=0',
'IGNORE_INSTALLED=1'
])

if (process.code !== null && process.code === 3) {
return { status: 'abort' }
}
}

await thirdParty.addInstalledGame(gameInfo.app_name, platformToInstall)

return { status: 'done' }
}

export async function uninstall({ appName }: RemoveArgs): Promise<ExecResult> {
const gameInfo = getGameInfo(appName)
if (gameInfo.thirdPartyManagedApp) {
await thirdParty.removeInstalledGame(appName)
return { stdout: '', stderr: '' }
}

const command: LegendaryCommand = {
subcommand: 'uninstall',
appName: LegendaryAppName.parse(appName),
Expand Down Expand Up @@ -867,6 +940,7 @@ export async function launch(
command['--override-exe'] = Path.parse(gameSettings.targetExe)
if (offlineMode) command['--offline'] = true
if (isCLINoGui) command['--skip-version-check'] = true
if (gameInfo.isEAManaged) command['--origin'] = true

const fullCommand = getRunnerCallWithoutCredentials(
command,
Expand Down
23 changes: 17 additions & 6 deletions src/backend/storeManagers/legendary/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { LegendaryCommand } from './commands'
import { LegendaryAppName, LegendaryPlatform } from './commands/base'
import { Path } from 'backend/schemas'
import shlex from 'shlex'
import thirdParty from './thirdParty'
import { Entries } from 'type-fest'

const allGames: Set<string> = new Set()
Expand Down Expand Up @@ -129,22 +130,29 @@ async function refreshLegendary(): Promise<ExecResult> {
*/
export function refreshInstalled() {
const installedJSON = join(legendaryConfigPath, 'installed.json')

let installedCache: [string, InstalledJsonMetadata][] = []
if (existsSync(installedJSON)) {
try {
installedGames = new Map(
Object.entries(JSON.parse(readFileSync(installedJSON, 'utf-8')))
)
installedCache = Object.entries(
JSON.parse(readFileSync(installedJSON, 'utf-8'))
) as [string, InstalledJsonMetadata][]
} catch (error) {
// disabling log here because its giving false positives on import command
logError(
['Corrupted installed.json file, cannot load installed games', error],
LogPrefix.Legendary
)
installedGames = new Map()
installedCache = []
}
} else {
installedGames = new Map()
installedCache = []
}

const thirdPartyGames = thirdParty.getInstalledGames()
installedCache.push(...thirdPartyGames)

installedGames = new Map(installedCache)
}

const defaultExecResult = {
Expand Down Expand Up @@ -623,6 +631,9 @@ function loadFile(app_name: string): boolean {
title,
canRunOffline,
thirdPartyManagedApp,
isEAManaged:
!!thirdPartyManagedApp &&
['origin', 'the ea app'].includes(thirdPartyManagedApp.toLowerCase()),
is_linux_native: false,
runner: 'legendary',
store_url: formatEpicStoreUrl(title)
Expand Down Expand Up @@ -867,7 +878,7 @@ export async function getLaunchOptions(
): Promise<LaunchOption[]> {
const gameInfo = getGameInfo(appName)
const installPlatform = gameInfo?.install.platform
if (!installPlatform) return []
if (!installPlatform || gameInfo.thirdPartyManagedApp) return []

const installInfo = await getInstallInfo(appName, installPlatform)
const launchOptions: LaunchOption[] = installInfo.game.launch_options
Expand Down
24 changes: 22 additions & 2 deletions src/backend/storeManagers/legendary/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { getInstallInfo } from './library'
import { sendGameStatusUpdate } from 'backend/utils'
import { enable, getStatus, isEnabled } from './eos_overlay/eos_overlay'
import { split } from 'shlex'
import { logError } from 'backend/logger/logger'
import { logError, LogPrefix } from 'backend/logger/logger'
import { runWineCommand } from 'backend/launcher'
import { GameConfig } from 'backend/game_config'
import { epicRedistPath } from 'backend/constants'

export const legendarySetup = async (appName: string) => {
const gameInfo = getGameInfo(appName)
Expand Down Expand Up @@ -39,7 +40,8 @@ export const legendarySetup = async (appName: string) => {
const winPlatforms = ['Windows', 'Win32', 'windows']
if (
gameInfo.install.platform &&
winPlatforms.includes(gameInfo.install.platform)
winPlatforms.includes(gameInfo.install.platform) &&
!gameInfo.isEAManaged
) {
try {
const info = await getInstallInfo(appName, gameInfo.install.platform)
Expand All @@ -65,6 +67,24 @@ export const legendarySetup = async (appName: string) => {
}
}

if (gameInfo.isEAManaged) {
const installerPath = join(epicRedistPath, 'EAappInstaller.exe')
try {
await runWineCommand({
gameSettings,
commandParts: [
installerPath,
'EAX_LAUNCH_CLIENT=0',
'IGNORE_INSTALLED=1'
],
wait: true,
protonVerb: 'run'
})
} catch (e) {
logError(`Failed to run EA App installer ${e}`, LogPrefix.Legendary)
}
}

const isOverlayEnabled = await isEnabled(appName)

if (getStatus().isInstalled && !isOverlayEnabled) {
Expand Down
Loading
Loading