Skip to content

Install legendary prerequisites #3364

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

6 changes: 3 additions & 3 deletions public/locales/en/gamepage.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,10 @@
"start": "Play Now",
"stop": "Playing (Stop)"
},
"prerequisites": "Installing Prerequisites",
"saves": {
"syncing": "Syncing Saves"
},
"ubisoft": "Installing Ubisoft Connect"
}
},
"launch": {
"options": "Launch Options..."
Expand Down Expand Up @@ -215,13 +215,13 @@
"notSupported": "Not supported",
"notSupportedGame": "Not Supported",
"playing": "Playing",
"prerequisites": "Installing Prerequisites",
"processing": "Processing files, please wait",
"queued": "Queued",
"reparing": "Repairing Game, please wait",
"syncingSaves": "Syncing Saves",
"this-game-uses-third-party": "This game uses third party launcher and it is not supported yet",
"totalDownloaded": "Total Downloaded",
"ubisoft": "Installing Ubisoft",
"uninstalling": "Uninstalling",
"updating": "Updating Game"
},
Expand Down
4 changes: 0 additions & 4 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@
},
"moving": "Error Moving Game {{error}}",
"title": "Error",
"ubisoft-connect": {
"message": "Installation of Ubisoft Connect in the game prefix failed. Check our wiki page at https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher/wiki/How-to-install-Ubisoft-Connect-on-Linux-and-Mac to install it manually.",
"title": "Ubisoft Connect"
},
"uncaught-exception": {
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}} Report the exception on our Github repository.",
"title": "Uncaught Exception occured!"
Expand Down
6 changes: 0 additions & 6 deletions src/backend/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,6 @@ const defaultWinePrefixDir = join(userHome, 'Games', 'Heroic', 'Prefixes')
const defaultWinePrefix = join(defaultWinePrefixDir, 'default')
const anticheatDataPath = join(appFolder, 'areweanticheatyet.json')
const imagesCachePath = join(appFolder, 'images-cache')
const cachedUbisoftInstallerPath = join(
appFolder,
'tools',
'UbisoftConnectInstaller.exe'
)

const {
currentLogFile,
Expand Down Expand Up @@ -276,7 +271,6 @@ export {
GITHUB_API,
wineprefixFAQ,
customThemesWikiLink,
cachedUbisoftInstallerPath,
gogdlAuthConfig,
vulkanHelperBin,
nileConfigPath,
Expand Down
6 changes: 0 additions & 6 deletions src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1082,12 +1082,6 @@ ipcMain.handle(
}
}

sendGameStatusUpdate({
appName,
runner,
status: 'playing'
})

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

const launchResult = await command.catch((exception) => {
Expand Down
2 changes: 2 additions & 0 deletions src/backend/storeManagers/gog/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ export async function launch(
`Launch Command: ${fullCommand}\n\nGame Log:\n`
)

sendGameStatusUpdate({ appName, runner: 'gog', status: 'playing' })

const { error, abort } = await runGogdlCommand(commandParts, {
abortId: appName,
env: commandEnv,
Expand Down
9 changes: 8 additions & 1 deletion src/backend/storeManagers/gog/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
import { copySync } from 'fs-extra'
import path from 'node:path'
import { GameInfo, InstalledInfo } from 'common/types'
import { checkWineBeforeLaunch, getShellPath, spawnAsync } from '../../utils'
import {
checkWineBeforeLaunch,
getShellPath,
sendGameStatusUpdate,
spawnAsync
} from '../../utils'
import { GameConfig } from '../../game_config'
import { logError, logInfo, LogPrefix, logWarning } from '../../logger/logger'
import { isWindows } from '../../constants'
Expand Down Expand Up @@ -48,6 +53,8 @@ async function setup(
LogPrefix.Gog
)

sendGameStatusUpdate({ appName, runner: 'gog', status: 'prerequisites' })

const gameSettings = GameConfig.get(appName).config
if (!isWindows) {
const isWineOkToLaunch = await checkWineBeforeLaunch(
Expand Down
2 changes: 2 additions & 0 deletions src/backend/storeManagers/legendary/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,8 @@ export async function launch(
`Launch Command: ${fullCommand}\n\nGame Log:\n`
)

sendGameStatusUpdate({ appName, runner: 'legendary', status: 'playing' })

const { error } = await runLegendaryCommand(command, {
abortId: appName,
env: commandEnv,
Expand Down
149 changes: 36 additions & 113 deletions src/backend/storeManagers/legendary/setup.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { logInfo, LogPrefix } from '../../logger/logger'
import axios from 'axios'
import { cachedUbisoftInstallerPath } from 'backend/constants'
import { logWarning } from 'backend/logger/logger'
import { existsSync, createWriteStream, statSync } from 'graceful-fs'
import { showDialogBoxModalAuto } from 'backend/dialog/dialog'
import i18next from 'i18next'
import { getWinePath } from 'backend/launcher'
import { getGameInfo, getSettings, runWineCommandOnGame } from './games'
import { join } from 'path'
import { getGameInfo, runWineCommandOnGame } from './games'
import { getInstallInfo } from './library'
import { sendGameStatusUpdate } from 'backend/utils'

const UBISOFT_INSTALLER_URL =
'https://ubistatic3-a.akamaihd.net/orbit/launcher_installer/UbisoftConnectInstaller.exe'
import { split } from 'shlex'
import { logError } from 'backend/logger/logger'

export const legendarySetup = async (appName: string) => {
const gameInfo = getGameInfo(appName)
if (!gameInfo) {
return
}

sendGameStatusUpdate({
appName,
runner: 'legendary',
status: 'prerequisites'
})

// Fixes games like Fallout New Vegas and Dishonored: Death of the Outsider
await runWineCommandOnGame(appName, {
commandParts: [
Expand All @@ -30,107 +29,31 @@ export const legendarySetup = async (appName: string) => {
protonVerb: 'waitforexitandrun'
})

// if not a ubisoft game, do nothing
if (gameInfo.install.executable !== 'UplayLaunch.exe') {
return
}

if (await isUbisoftInstalled(appName)) {
// it's already installed, do nothing
return
}

sendGameStatusUpdate({
appName,
runner: 'legendary',
status: 'ubisoft'
})

await installUbisoftConnect(appName)
}

const installUbisoftConnect = async (appName: string) => {
try {
await downloadIfNotCached(cachedUbisoftInstallerPath, UBISOFT_INSTALLER_URL)

await runWineCommandOnGame(appName, {
commandParts: [cachedUbisoftInstallerPath, '/S']
})
} catch (error) {
logWarning(`Error installing Ubisoft Connect: ${error}`, LogPrefix.Backend)
}

if (await isUbisoftInstalled(appName)) {
return true
} else {
// it was not installed correctly, show an error
showDialogBoxModalAuto({
title: i18next.t('box.error.ubisoft-connect.title', 'Ubisoft Connect'),
message: i18next.t(
'box.error.ubisoft-connect.message',
'Installation of Ubisoft Connect in the game prefix failed. Check our wiki page at https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher/wiki/How-to-install-Ubisoft-Connect-on-Linux-and-Mac to install it maunally.'
),
type: 'ERROR'
})
return false
}
}

const downloadIfNotCached = async (cachePath: string, url: string) => {
if (existsSync(cachePath)) {
// if the cached file exist but it was cached more than a week ago, download a new one
// an outdated installer can make the installation fail according to some report
const cachedAt = statSync(cachePath).mtime
const oneWeekAgo = new Date()
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7)
if (cachedAt > oneWeekAgo) {
return true
if (
gameInfo.install?.platform === 'Windows' ||
gameInfo.install?.platform === 'Win32' ||
gameInfo.install?.platform === 'windows'
) {
try {
const info = await getInstallInfo(appName, gameInfo.install.platform)
if (
info.manifest.prerequisites &&
info.manifest.prerequisites.path.length > 0
) {
await runWineCommandOnGame(appName, {
commandParts: [
join(
gameInfo.install.install_path ?? '',
info.manifest.prerequisites.path
),
...split(info.manifest.prerequisites.args)
],
wait: true,
protonVerb: 'waitforexitandrun'
})
}
} catch (error) {
logError(`getInstallInfo failed with ${error}`)
}
}

try {
await download(cachePath, url)
return true
} catch {
logWarning(`Failed to download ${url}`, LogPrefix.Backend)
return false
}
}

// snippet took from https://stackoverflow.com/a/61269447/1430810, maybe can be improved
const download = async (cachePath: string, url: string) => {
logInfo('Downloading UbisoftConnectInstaller.exe', LogPrefix.Backend)
const writer = createWriteStream(cachePath)

return axios
.get(url, {
responseType: 'stream'
})
.then(async (response) => {
return new Promise((resolve, reject) => {
response.data.pipe(writer)
let error: unknown = null
writer.on('error', (err) => {
error = err
writer.close()
reject(err)
})
writer.on('close', () => {
if (!error) {
resolve(true)
}
})
})
})
}

const isUbisoftInstalled = async (appName: string) => {
const gameSettings = await getSettings(appName)

const ubisoftExecPath = await getWinePath({
path: 'C:/Program FIles (x86)/Ubisoft/Ubisoft Game Launcher/UbisoftConnect.exe',
gameSettings
})

return existsSync(ubisoftExecPath)
}
2 changes: 2 additions & 0 deletions src/backend/storeManagers/nile/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,8 @@ export async function launch(
`Launch Command: ${fullCommand}\n\nGame Log:\n`
)

sendGameStatusUpdate({ appName, runner: 'nile', status: 'playing' })

const { error } = await runNileCommand(commandParts, {
abortId: appName,
env: commandEnv,
Expand Down
7 changes: 6 additions & 1 deletion src/backend/storeManagers/nile/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
import { fetchFuelJSON, getGameInfo } from './library'
import { GameConfig } from 'backend/game_config'
import { isWindows } from 'backend/constants'
import { checkWineBeforeLaunch, spawnAsync } from 'backend/utils'
import {
checkWineBeforeLaunch,
sendGameStatusUpdate,
spawnAsync
} from 'backend/utils'
import { logFileLocation } from '../storeManagerCommon/games'
import { runWineCommand, verifyWinePrefix } from 'backend/launcher'

Expand Down Expand Up @@ -55,6 +59,7 @@ export default async function setup(
'Running setup instructions, if you notice issues with launching a game, please report it on our Discord server',
LogPrefix.Nile
)
sendGameStatusUpdate({ appName, runner: 'nile', status: 'prerequisites' })

const gameSettings = GameConfig.get(appName).config
if (!isWindows) {
Expand Down
7 changes: 7 additions & 0 deletions src/backend/storeManagers/storeManagerCommon/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '../../utils/aborthandler/aborthandler'
import { BrowserWindow, dialog, Menu } from 'electron'
import { gameManagerMap } from '../index'
import { sendGameStatusUpdate } from 'backend/utils'

async function getAppSettings(appName: string): Promise<GameSettings> {
return (
Expand Down Expand Up @@ -182,6 +183,12 @@ export async function launchGame(
return false
}

sendGameStatusUpdate({
appName,
runner,
status: 'playing'
})

// Native
if (isNative) {
logInfo(
Expand Down
2 changes: 1 addition & 1 deletion src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export type Status =
| 'notSupportedGame'
| 'notInstalled'
| 'installed'
| 'ubisoft'
| 'prerequisites'
| 'extracting'

export interface GameStatus {
Expand Down
5 changes: 4 additions & 1 deletion src/frontend/hooks/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export function getStatusLabel({
}`,
notInstalled: t('gamepage:status.notinstalled'),
launching: t('gamepage:status.launching', 'Launching'),
ubisoft: t('gamepage:status.ubisoft', 'Installing Ubisoft')
prerequisites: t(
'gamepage:status.prerequisites',
'Installing Prerequisites'
)
}

return statusMap[status] || t('gamepage:status.notinstalled')
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/screens/Game/GameContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const initialContext: GameContextType = {
gameInstallInfo: null,
is: {
installing: false,
installingUbisoft: false,
installingPrerequisites: false,
launching: false,
linux: false,
linuxNative: false,
Expand Down
6 changes: 3 additions & 3 deletions src/frontend/screens/Game/GamePage/components/MainButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const MainButton = ({ gameInfo, handlePlay, handleInstall }: Props) => {
</span>
)
}
if (is.installingUbisoft) {
return t('label.ubisoft', 'Installing Ubisoft Connect')
if (is.installingPrerequisites) {
return t('label.prerequisites', 'Installing Prerequisites')
}
if (is.launching) {
return t('label.launching', 'Launching')
Expand Down Expand Up @@ -136,7 +136,7 @@ const MainButton = ({ gameInfo, handlePlay, handleInstall }: Props) => {
is.uninstalling ||
is.syncing ||
is.launching ||
is.installingUbisoft
is.installingPrerequisites
}
autoFocus={true}
onClick={async () => handlePlay(gameInfo)}
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/screens/Game/GamePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export default React.memo(function GamePage(): JSX.Element | null {
const isUninstalling = status === 'uninstalling'
const isSyncing = status === 'syncing-saves'
const isLaunching = status === 'launching'
const isInstallingUbisoft = status === 'ubisoft'
const isInstallingPrerequisites = status === 'prerequisites'
const notAvailable = !gameAvailable && gameInfo.is_installed
const notInstallable =
gameInfo.installable !== undefined && !gameInfo.installable
Expand Down Expand Up @@ -280,7 +280,7 @@ export default React.memo(function GamePage(): JSX.Element | null {
gameExtraInfo: extraInfo,
is: {
installing: isInstalling,
installingUbisoft: isInstallingUbisoft,
installingPrerequisites: isInstallingPrerequisites,
launching: isLaunching,
linux: isLinux,
linuxNative: isLinuxNative,
Expand Down
Loading