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

[TECH] Move default save computation into the backend #1887

Merged
merged 29 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a01e9bc
Update legendary to 0.20.29
CommandMC Oct 8, 2022
40ae1c0
Misc: Minor IPC changes
CommandMC Oct 9, 2022
7441c6b
Add `save_path` to GameInfo
CommandMC Oct 9, 2022
ec574de
[Epic] Move default save computation into the backend & rely on Legen…
CommandMC Oct 9, 2022
901258f
Fixup: Actually refresh game info after running `sync-saves`
CommandMC Oct 9, 2022
6fd4765
Pass Wine env vars to Legendary
CommandMC Oct 9, 2022
80eb710
Move save computation into its own file
CommandMC Oct 9, 2022
a124647
Extract getShellPath into its own function
CommandMC Oct 11, 2022
29422b9
Add a bunch of new GOG interfaces/types
CommandMC Oct 11, 2022
14c3c8d
Use the new types in GOGLibrary
CommandMC Oct 11, 2022
1b35d93
Move GOG save path computation into the backend
CommandMC Oct 11, 2022
b689b4e
Remove unused `getRealPath` IPC call
CommandMC Oct 11, 2022
cbee1e4
Make sure the computed save folder exists before running `realpath` o…
CommandMC Oct 11, 2022
2052417
Make it possible to directly specify a `ProtonVerb` when running Prot…
CommandMC Oct 17, 2022
9c6166a
Add new `getWinePath` function and use it in save_sync.ts
CommandMC Oct 17, 2022
b952e52
Add a whole bunch of debug logging
CommandMC Oct 17, 2022
c84822d
Merge branch 'beta' into feat/dont-guess-save-path
CommandMC Oct 17, 2022
95051a0
Merge branch 'beta' into feat/dont-guess-save-path
CommandMC Oct 20, 2022
29916f5
Merge branch 'beta' into feat/dont-guess-save-path
CommandMC Oct 29, 2022
d9e5611
Fixup runWineCommand
CommandMC Oct 29, 2022
9c33b5d
Update translation files
CommandMC Oct 30, 2022
ec51eaa
Merge branch 'beta' into feat/dont-guess-save-path
CommandMC Oct 30, 2022
f1a2fe7
Use callAbortController instead of killing the child process
CommandMC Oct 30, 2022
7f048ca
Run `cmd /c echo path` in getWinePath to expand Windows env vars
CommandMC Nov 1, 2022
3f791a4
Just always run `cmd /c winepath path`
CommandMC Nov 1, 2022
c62f45a
Remove debug logging
CommandMC Nov 2, 2022
ef95e9a
Make `workingDir` and `arguments` optional for `FileTask`
CommandMC Nov 2, 2022
df403c3
New 'other' `TaskCategory`
CommandMC Nov 2, 2022
3789fd5
`overlaySupported` optional property of info file
CommandMC Nov 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/backend/api/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ConnectivityChangedCallback,
ConnectivityStatus
} from 'common/types'
import { GOGCloudSavesLocation } from 'common/types/gog'

export const notify = (notification: string[]) =>
ipcRenderer.send('Notify', notification)
Expand Down Expand Up @@ -34,6 +35,17 @@ export const getUserInfo = async () => ipcRenderer.invoke('getUserInfo')
export const syncSaves = async (
args: [arg: string | undefined, path: string, appName: string, runner: string]
) => ipcRenderer.invoke('syncSaves', args)
export const getDefaultSavePath = async (
appName: string,
runner: Runner,
alreadyDefinedGogSaves: GOGCloudSavesLocation[] = []
): Promise<string | GOGCloudSavesLocation[]> =>
ipcRenderer.invoke(
'getDefaultSavePath',
appName,
runner,
alreadyDefinedGogSaves
)
export const getGameInfo = async (appName: string, runner: Runner) =>
ipcRenderer.invoke('getGameInfo', appName, runner)
export const getGameSettings = async (appName: string, runner: Runner) =>
Expand Down
5 changes: 0 additions & 5 deletions src/backend/api/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,11 @@ export const getGOGGameClientId = async (appName: string) =>
ipcRenderer.invoke('getGOGGameClientId', appName)
export const getShellPath = async (saveLocation: string) =>
ipcRenderer.invoke('getShellPath', saveLocation)
export const getRealPath = async (actualPath: string) =>
ipcRenderer.invoke('getRealPath', actualPath)
export const callTool = async (toolArgs: Tools) =>
ipcRenderer.invoke('callTool', toolArgs)
export const getAnticheatInfo = async (namespace: string) =>
ipcRenderer.invoke('getAnticheatInfo', namespace)

export const requestSettingsRemoveListeners = () =>
ipcRenderer.removeAllListeners('requestSettings')

export const clipboardReadText = async () =>
ipcRenderer.invoke('clipboardReadText')

Expand Down
114 changes: 77 additions & 37 deletions src/backend/gog/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ import {
ExecResult,
CallRunnerOptions
} from 'common/types'
import { GOGCloudSavesLocation, GogInstallInfo } from 'common/types/gog'
import { join } from 'node:path'
import {
GOGCloudSavesLocation,
GOGGameDotInfoFile,
GogInstallInfo,
GOGGameDotIdFile,
GOGClientsResponse
} from 'common/types/gog'
import { basename, join } from 'node:path'
import { existsSync, readFileSync } from 'graceful-fs'

import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger'
Expand Down Expand Up @@ -40,7 +46,7 @@ export class GOGLibrary {
appName: string,
install: InstalledInfo
): Promise<GOGCloudSavesLocation[] | undefined> {
let syncPlatform = 'Windows'
let syncPlatform: 'Windows' | 'MacOS' = 'Windows'
const platform = install.platform
switch (platform) {
case 'windows':
Expand All @@ -58,29 +64,26 @@ export class GOGLibrary {
)
return
}
const response = await axios
.get(
`https://remote-config.gog.com/components/galaxy_client/clients/${clientId}?component_version=2.0.45`
)
.catch((error) => {
logError(
['Failed to get remote config information for', appName, ':', error],
{ prefix: LogPrefix.Gog }

let response: GOGClientsResponse | undefined
try {
response = (
await axios.get(
`https://remote-config.gog.com/components/galaxy_client/clients/${clientId}?component_version=2.0.45`
)
return null
})
).data
} catch (error) {
logError(
['Failed to get remote config information for', appName, ':', error],
{ prefix: LogPrefix.Gog }
)
}
if (!response) {
return
}
const platformInfo = response.data.content[syncPlatform]
const platformInfo = response.content[syncPlatform]
const savesInfo = platformInfo.cloudStorage
if (!savesInfo.enabled) {
return
}

const locations = savesInfo.locations

return locations
return savesInfo.locations
}
/**
* Returns ids of games with requested features ids
Expand Down Expand Up @@ -718,16 +721,18 @@ export class GOGLibrary {
* Reads goggame-appName.info file and returns JSON object of it
* @param appName
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public readInfoFile(appName: string, installPath?: string): any {
public readInfoFile(
appName: string,
installPath?: string
): GOGGameDotInfoFile | undefined {
const gameInfo = this.getGameInfo(appName)
if (!gameInfo) {
return
}

installPath = installPath ?? gameInfo?.install.install_path
if (!installPath) {
return {}
return
}

const infoFileName = `goggame-${appName}.info`
Expand All @@ -738,29 +743,64 @@ export class GOGLibrary {
infoFilePath = join(installPath, 'Contents', 'Resources', infoFileName)
}

if (existsSync(infoFilePath)) {
const fileData = readFileSync(infoFilePath, { encoding: 'utf-8' })
if (!existsSync(infoFilePath)) {
return
}

let infoFileData: GOGGameDotInfoFile | undefined
try {
infoFileData = JSON.parse(readFileSync(infoFilePath, 'utf-8'))
} catch (error) {
logError(`Error reading ${infoFilePath}, could not complete operation`, {
prefix: LogPrefix.Gog
})
}
if (!infoFileData) {
return
}

try {
const jsonData = JSON.parse(fileData)
return jsonData
} catch (error) {
logError(`Error reading ${fileData}, could not complete operation`, {
prefix: LogPrefix.Gog
})
if (!infoFileData.buildId) {
const idFilePath = join(basename(infoFilePath), `goggame-${appName}.id`)
if (existsSync(idFilePath)) {
try {
const { buildId }: GOGGameDotIdFile = JSON.parse(
readFileSync(idFilePath, 'utf-8')
)
infoFileData.buildId = buildId
} catch (error) {
logError(
`Error reading ${idFilePath}, not adding buildId to game metadata`
)
}
}
}
return {}

return infoFileData
}

public getExecutable(appName: string): string {
const jsonData = this.readInfoFile(appName)
if (!jsonData) {
throw new Error('No game metadata, cannot get executable')
}
const playTasks = jsonData.playTasks

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const primary = playTasks.find((value: any) => value?.isPrimary)
let primary = playTasks.find((task) => task.isPrimary)

if (!primary) {
primary = playTasks[0]
if (!primary) {
throw new Error('No play tasks in game metadata')
}
}

if (primary.type === 'URLTask') {
throw new Error(
'Primary play task is an URL task, not sure what to do here'
)
}

const workingDir = primary?.workingDir
const workingDir = primary.workingDir

if (workingDir) {
return join(workingDir, primary.path)
Expand Down
37 changes: 36 additions & 1 deletion src/backend/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,40 @@ function getRunnerCallWithoutCredentials(
].join(' ')
}

/**
* Converts Unix paths to Windows ones or vice versa
* @param path The Windows/Unix path you have
* @param game Required for runWineCommand
* @param variant The path variant (Windows/Unix) that you'd like to get (passed to `winepath` as -u/-w)
* @returns The path returned by `winepath`
*/
async function getWinePath({
path,
gameSettings,
variant = 'unix'
}: {
path: string
gameSettings: GameSettings
variant?: 'win' | 'unix'
}): Promise<string> {
// TODO: Proton has a special verb for getting Unix paths, and another one for Windows ones. Use those instead
// Note that this would involve running `proton runinprefix cmd /c echo path` first to expand env vars
// https://github.com/ValveSoftware/Proton/blob/4221d9ef07cc38209ff93dbbbca9473581a38255/proton#L1526-L1533
const { stdout } = await runWineCommand({
gameSettings,
commandParts: [
'cmd',
'/c',
'winepath',
variant === 'unix' ? '-u' : '-w',
path
],
wait: false,
protonVerb: 'runinprefix'
})
return stdout.trim()
}

export {
prepareLaunch,
launchCleanup,
Expand All @@ -821,5 +855,6 @@ export {
setupWrappers,
runWineCommand,
callRunner,
getRunnerCallWithoutCredentials
getRunnerCallWithoutCredentials,
getWinePath
}
21 changes: 16 additions & 5 deletions src/backend/legendary/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,22 @@ export class LegendaryLibrary {
/**
* Get game info for a particular game.
*
* @param appName
* @param appName The AppName of the game you want the info of
* @param forceReload Discards game info in `this.library` and always reads info from metadata files
* @returns GameInfo
*/
public getGameInfo(appName: string): GameInfo | undefined {
public getGameInfo(
appName: string,
forceReload = false
): GameInfo | undefined {
if (!this.hasGame(appName)) {
logWarning(['Requested game', appName, 'was not found in library'], {
prefix: LogPrefix.Legendary
})
return
}
// We have the game, but info wasn't loaded yet
if (!this.library.has(appName)) {
if (!this.library.has(appName) || forceReload) {
this.loadFile(appName + '.json')
}
return this.library.get(appName)
Expand Down Expand Up @@ -513,8 +517,14 @@ export class LegendaryLibrary {
const art_square_front = gameBoxStore ? gameBoxStore.url : undefined

const info = this.installedGames.get(app_name)
const { executable, version, install_size, install_path, platform } =
info ?? {}
const {
executable,
version,
install_size,
install_path,
platform,
save_path
} = info ?? {}

const is_dlc = Boolean(metadata.mainGameItem)

Expand Down Expand Up @@ -549,6 +559,7 @@ export class LegendaryLibrary {
? platform === 'Mac'
: releaseInfo[0].platform.includes('Mac'),
save_folder: saveFolder,
save_path,
title,
canRunOffline,
is_linux_native: false,
Expand Down
37 changes: 17 additions & 20 deletions src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,14 @@ import {
rmSync,
unlinkSync,
watch,
realpathSync,
writeFileSync,
readdirSync,
readFileSync
} from 'graceful-fs'

import Backend from 'i18next-fs-backend'
import i18next from 'i18next'
import { join, normalize } from 'path'
import { join } from 'path'
import checkDiskSpace from 'check-disk-space'
import { DXVK, Winetricks } from './tools'
import { GameConfig } from './game_config'
Expand Down Expand Up @@ -77,7 +76,8 @@ import {
getGame,
getFirstExistingParentPath,
getLatestReleases,
notify
notify,
getShellPath
} from './utils'
import {
configStore,
Expand Down Expand Up @@ -140,6 +140,7 @@ import {
stop
} from './sideload/games'
import { callAbortController } from './utils/aborthandler/aborthandler'
import { getDefaultSavePath } from './save_sync'
import si from 'systeminformation'

const { showOpenDialog } = dialog
Expand Down Expand Up @@ -1449,6 +1450,17 @@ ipcMain.handle('syncSaves', async (event, args) => {
return `\n ${stdout} - ${stderr}`
})

ipcMain.handle(
'getDefaultSavePath',
async (
event,
appName: string,
runner: Runner,
alreadyDefinedGogSaves: GOGCloudSavesLocation[]
): Promise<string | GOGCloudSavesLocation[]> =>
getDefaultSavePath(appName, runner, alreadyDefinedGogSaves)
)

// Simulate keyboard and mouse actions as if the real input device is used
ipcMain.handle('gamepadAction', async (event, args) => {
const [action, metadata] = args
Expand Down Expand Up @@ -1585,27 +1597,12 @@ ipcMain.handle(
await setup(game.appName)
}

// FIXME: Why are we using `runinprefix` here?
return game.runWineCommand(commandParts, false, 'runinprefix')
}
)

ipcMain.handle('getShellPath', async (event, path) => {
return normalize((await execAsync(`echo "${path}"`)).stdout.trim())
})

ipcMain.handle('getRealPath', (event, path) => {
let resolvedPath = normalize(path)
try {
resolvedPath = realpathSync(path)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if (err?.path) {
resolvedPath = err.path // Reslove most accurate path (most likely followed symlinks)
}
}

return resolvedPath
})
ipcMain.handle('getShellPath', async (event, path) => getShellPath(path))

ipcMain.handle('clipboardReadText', () => {
return clipboard.readText()
Expand Down