mirror of
https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher.git
synced 2026-10-28 09:45:03 +00:00
[Tech] Switch from react-scripts to Vite & clean up everything to work with strict mode (#1633)
* Change complete project structure - Rename "src" -> "src/frontend" - Rename "electron" -> "src/backend" - Move types.ts to "common/types.ts" * Switch from react-scripts + foreman + webpack + ts-loader -> Vite * Set GH_TOKEN and GITHUB_TOKEN in tests directly This just cleans up package.json a bit * Get SVG & JPG importing working in VSCode * Move type definitions into common/typedefs * Fixup tests * Rewrite large chunks of the backend to be compatible with strict mode * Modify frontend to reflect changes to type definition structure * Remove unused files These weren't necessary, at least as far as I can tell * Update translations * Fixup: Re-add `--filesystem` parameter for Proton Turns out refactoring a lot of stuff can introduce issues, who would've thought * Remove `build` script You never have to run this yourself, so it was a bit confusing to have. If you really need its functionality, `vite build` isn't that hard to type in on your own :^) * Fully remove all remnants of UE support * Add new 'codecheck' command This command can be used to make sure that no files in the project violate TS rules, since Vite itself doesn't check them It would probably be a good idea to add them to GH Workflows and/or pre-commit hooks, but I'm not too sure on how to do that * Update translation files * Fix translation files Not sure how this happens, but it seems they've gotten screwed up in the merge * Remove some more unnecessary files * Always get game updates from all libraries This fixes an issue where refreshing the library while only showing one library made the update icons of all the others disappear * Some misc code style changes
This commit is contained in:
1 parent
a9849928b0
commit
2ba6604f70
376 files changed
+3367
-8678
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
public/start-react.js
|
||||
i18next-parser.config.js
|
||||
flatpak/build/
|
||||
flatpak/.flatpak-builder/
|
||||
vite.config.ts
|
||||
**/__tests__/**
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module",
|
||||
"project": ["./tsconfig.json", "./electron/tsconfig.json"]
|
||||
"project": ["./tsconfig.json"]
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint"],
|
||||
"settings": {
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
|
||||
@@ -23,6 +23,9 @@ jobs:
|
||||
run: yarn
|
||||
- name: Build artifacts.
|
||||
run: yarn dist:linux appimage --publish=never
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Upload AppImage.
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
@@ -23,6 +23,9 @@ jobs:
|
||||
run: yarn
|
||||
- name: Build artifacts.
|
||||
run: yarn dist:mac --publish=never
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Upload DMG.
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
@@ -23,6 +23,9 @@ jobs:
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Build artifacts.
|
||||
run: npm run dist:win portable
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
- name: Upload EXE.
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
@@ -19,3 +19,4 @@ jobs:
|
||||
- run: npm run release:linux
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
@@ -18,3 +18,4 @@ jobs:
|
||||
- run: npm run release:mac
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
@@ -33,4 +33,6 @@ downloads*.json
|
||||
|
||||
flatpak/build
|
||||
.flatpak-builder
|
||||
|
||||
vite-plugin-electron.log
|
||||
.tool-versions
|
||||
@@ -2,6 +2,5 @@ node_modules
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
public/index.html
|
||||
public/locales/
|
||||
flatpak
|
||||
@@ -1,587 +0,0 @@
|
||||
import { CallRunnerOptions, ExecResult } from './../types'
|
||||
import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
writeFileSync
|
||||
} from 'graceful-fs'
|
||||
|
||||
import { GameConfig } from '../game_config'
|
||||
import {
|
||||
GameInfo,
|
||||
InstalledInfo,
|
||||
InstallInfo,
|
||||
KeyImage,
|
||||
RawGameJSON
|
||||
} from '../types'
|
||||
import { LegendaryGame } from './games'
|
||||
import { LegendaryUser } from './user'
|
||||
import {
|
||||
formatEpicStoreUrl,
|
||||
getLegendaryBin,
|
||||
isEpicServiceOffline,
|
||||
isOnline,
|
||||
getFileSize
|
||||
} from '../utils'
|
||||
import {
|
||||
fallBackImage,
|
||||
installed,
|
||||
legendaryConfigPath,
|
||||
libraryPath
|
||||
} from '../constants'
|
||||
import {
|
||||
logDebug,
|
||||
logError,
|
||||
logInfo,
|
||||
LogPrefix,
|
||||
logWarning
|
||||
} from '../logger/logger'
|
||||
import { GlobalConfig } from '../config'
|
||||
import { installStore, libraryStore } from './electronStores'
|
||||
import { callRunner } from '../launcher'
|
||||
|
||||
/**
|
||||
* Legendary LegendaryLibrary.
|
||||
*
|
||||
* For multi-account support, the single global instance will need to become a instance map.
|
||||
* @see GameConfig
|
||||
*/
|
||||
export class LegendaryLibrary {
|
||||
private static globalInstance: LegendaryLibrary = null
|
||||
|
||||
private library: Map<string, null | GameInfo> = new Map()
|
||||
|
||||
private installedGames: Map<string, RawGameJSON>
|
||||
|
||||
/**
|
||||
* Private constructor for LegendaryLibrary since we don't really want it to be constructible from outside.
|
||||
*
|
||||
* @param lazy_load Whether the library loads data lazily or in advance.
|
||||
*/
|
||||
private constructor(lazy_load: boolean) {
|
||||
this.refreshInstalled()
|
||||
if (!lazy_load) {
|
||||
this.loadAll()
|
||||
} else {
|
||||
this.loadAsStubs()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global library instance, and if it doesn't exist, create one.
|
||||
*
|
||||
* @param lazy_load Whether the library loads data lazily or in advance. Default: TRUE.
|
||||
* @returns LegendaryLibrary instance.
|
||||
*/
|
||||
public static get(lazy_load = true) {
|
||||
if (this.globalInstance === null) {
|
||||
LegendaryLibrary.globalInstance = new LegendaryLibrary(lazy_load)
|
||||
}
|
||||
return this.globalInstance
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh library.
|
||||
*/
|
||||
public async refresh(): Promise<ExecResult> {
|
||||
logInfo('Refreshing Epic Games...', LogPrefix.Legendary)
|
||||
const { showUnrealMarket } = await GlobalConfig.get().getSettings()
|
||||
const epicOffline = await isEpicServiceOffline()
|
||||
if (epicOffline) {
|
||||
logWarning(
|
||||
'Epic is Offline right now, cannot update game list!',
|
||||
LogPrefix.Backend
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const includeUEFlag = showUnrealMarket ? '--include-ue' : ''
|
||||
const res = await runLegendaryCommand(['list', includeUEFlag])
|
||||
|
||||
if (res.error) {
|
||||
logError(['Failed to refresh library:', res.error], LogPrefix.Legendary)
|
||||
}
|
||||
this.refreshInstalled()
|
||||
this.loadAll()
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh `this.installedGames` from file.
|
||||
*/
|
||||
public refreshInstalled() {
|
||||
const installedJSON = `${legendaryConfigPath}/installed.json`
|
||||
if (existsSync(installedJSON)) {
|
||||
try {
|
||||
this.installedGames = new Map(
|
||||
Object.entries(JSON.parse(readFileSync(installedJSON, 'utf-8')))
|
||||
)
|
||||
} catch (error) {
|
||||
// disabling log here because its giving false positives on import command
|
||||
logError(
|
||||
[
|
||||
'Corrupted intalled.json file, cannot load installed games',
|
||||
`${error}`
|
||||
],
|
||||
LogPrefix.Legendary,
|
||||
false
|
||||
)
|
||||
this.installedGames = new Map()
|
||||
}
|
||||
} else {
|
||||
this.installedGames = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all games in the library.
|
||||
* Please note this loads all library data, thus making lazy loading kind of pointless.
|
||||
*
|
||||
* @param format Return format. 'info' -> GameInfo, 'class' (default) -> LegendaryGame
|
||||
* @param fullRefresh Reload from Legendary.
|
||||
* @returns Array of objects.
|
||||
*/
|
||||
public async getGames(
|
||||
format: 'info' | 'class' = 'class',
|
||||
fullRefresh?: boolean
|
||||
): Promise<(LegendaryGame | GameInfo)[]> {
|
||||
logInfo('Refreshing library...', LogPrefix.Legendary)
|
||||
const isLoggedIn = await LegendaryUser.isLoggedIn()
|
||||
if (!isLoggedIn) {
|
||||
return
|
||||
}
|
||||
|
||||
if (fullRefresh) {
|
||||
try {
|
||||
await this.refresh()
|
||||
} catch (error) {
|
||||
logError(`${error}`, LogPrefix.Legendary)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.refreshInstalled()
|
||||
await this.loadAll()
|
||||
} catch (error) {
|
||||
logError(`${error}`, LogPrefix.Legendary)
|
||||
}
|
||||
const arr = Array.from(this.library.values())
|
||||
|
||||
if (format === 'info') {
|
||||
if (libraryStore.has('library')) {
|
||||
libraryStore.delete('library')
|
||||
}
|
||||
logInfo('Updating game list', LogPrefix.Legendary)
|
||||
libraryStore.set('library', arr)
|
||||
logInfo('Game List Updated', LogPrefix.Legendary)
|
||||
return arr
|
||||
}
|
||||
if (format === 'class') {
|
||||
return arr.map(({ app_name }) => LegendaryGame.get(app_name))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get game info for a particular game.
|
||||
*
|
||||
* @param appName
|
||||
* @returns GameInfo
|
||||
*/
|
||||
public getGameInfo(appName: string) {
|
||||
const info = this.library.get(appName)
|
||||
if (info === undefined) {
|
||||
return null
|
||||
}
|
||||
if (info === null) {
|
||||
// This assumes that fileName and appName are same.
|
||||
// If that changes, this will break.
|
||||
this.loadFile(`${appName}.json`)
|
||||
}
|
||||
return this.library.get(appName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get game info for a particular game.
|
||||
*/
|
||||
public async getInstallInfo(
|
||||
appName: string,
|
||||
installPlatform = 'Windows'
|
||||
): Promise<InstallInfo> {
|
||||
const cache = installStore.get(appName) as InstallInfo
|
||||
if (cache) {
|
||||
logDebug('Using cached install info', LogPrefix.Legendary)
|
||||
return cache
|
||||
}
|
||||
|
||||
logInfo(`Getting more details with 'legendary info'`, LogPrefix.Legendary)
|
||||
const res = await runLegendaryCommand([
|
||||
'--pretty-json',
|
||||
'info',
|
||||
appName,
|
||||
'--platform',
|
||||
installPlatform,
|
||||
'--json',
|
||||
(await isEpicServiceOffline()) ? '--offline' : ''
|
||||
])
|
||||
|
||||
if (res.error) {
|
||||
logError(
|
||||
['Failed to get more details:', res.error],
|
||||
LogPrefix.Legendary,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
const info: InstallInfo = JSON.parse(res.stdout)
|
||||
installStore.set(appName, info)
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a list of updateable games.
|
||||
*
|
||||
* @returns App names of updateable games.
|
||||
*/
|
||||
public async listUpdateableGames() {
|
||||
const isLoggedIn = await LegendaryUser.isLoggedIn()
|
||||
|
||||
if (!isLoggedIn || !isOnline()) {
|
||||
return []
|
||||
}
|
||||
const epicOffline = await isEpicServiceOffline()
|
||||
if (epicOffline) {
|
||||
logWarning(
|
||||
'Epic servers are offline, cannot check for game updates',
|
||||
LogPrefix.Backend
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const commandParts = ['list-installed', '--check-updates', '--tsv']
|
||||
|
||||
logInfo('Checking for game updates.', LogPrefix.Legendary)
|
||||
const res = await runLegendaryCommand(commandParts)
|
||||
|
||||
if (res.error) {
|
||||
logError(
|
||||
['Failed to check for game updates:', res.error],
|
||||
LogPrefix.Legendary,
|
||||
false
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const updates = res.stdout
|
||||
.split('\n')
|
||||
.filter((item) => item.split('\t')[4] === 'True')
|
||||
.map((item) => item.split('\t')[0])
|
||||
.filter((item) => item.length > 1)
|
||||
logInfo(`Found ${updates.length} game(s) to update`, LogPrefix.Legendary)
|
||||
return updates
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all updateable games.
|
||||
* Uses `listUpdateableGames` along with `LegendaryGame.update`
|
||||
*
|
||||
* @returns Array of results of `Game.update`.
|
||||
*/
|
||||
public async updateAllGames() {
|
||||
return (
|
||||
await Promise.allSettled(
|
||||
(await this.listUpdateableGames())
|
||||
.map(LegendaryGame.get)
|
||||
.map(async (game) => game.update())
|
||||
)
|
||||
).map((res) => {
|
||||
if (res.status === 'fulfilled') {
|
||||
return res.value
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the install path for a given game.
|
||||
*
|
||||
* DOES NOT MOVE FILES. Use `LegendaryGame.moveInstall` instead.
|
||||
*
|
||||
* @param appName
|
||||
* @param newPath
|
||||
*/
|
||||
public changeGameInstallPath(appName: string, newPath: string) {
|
||||
this.library.get(appName).install.install_path = newPath
|
||||
this.installedGames.get(appName).install_path = newPath
|
||||
|
||||
// Modify Legendary installed.json file:
|
||||
try {
|
||||
const file = JSON.parse(readFileSync(installed, 'utf8'))
|
||||
const game = { ...file[appName], install_path: newPath }
|
||||
const modifiedInstall = { ...file, [appName]: game }
|
||||
writeFileSync(installed, JSON.stringify(modifiedInstall, null, 2))
|
||||
} catch (error) {
|
||||
logError(
|
||||
`Error reading ${installed}, could not complete operation`,
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the install state of a game without a complete library reload.
|
||||
*
|
||||
* @param appName
|
||||
* @param state true if its installed, false otherwise.
|
||||
*/
|
||||
public installState(appName: string, state: boolean) {
|
||||
if (state) {
|
||||
// This assumes that fileName and appName are same.
|
||||
// If that changes, this will break.
|
||||
this.loadFile(`${appName}.json`)
|
||||
} else {
|
||||
this.library.get(appName).is_installed = false
|
||||
this.library.get(appName).install = {} as InstalledInfo
|
||||
this.installedGames.delete(appName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configs for installed games into memory.
|
||||
*/
|
||||
public loadGameConfigs() {
|
||||
for (const appName of this.installedGames.keys()) {
|
||||
GameConfig.get(appName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the file completely into our in-memory library.
|
||||
* Largely derived from legacy code.
|
||||
*
|
||||
* @returns App name of loaded file.
|
||||
*/
|
||||
private loadFile(fileName: string): string {
|
||||
fileName = `${libraryPath}/${fileName}`
|
||||
|
||||
try {
|
||||
const { app_name, metadata } = JSON.parse(readFileSync(fileName, 'utf-8'))
|
||||
const { namespace } = metadata
|
||||
const is_game = namespace !== 'ue' ? true : false
|
||||
const {
|
||||
description,
|
||||
shortDescription = '',
|
||||
keyImages = [],
|
||||
title,
|
||||
developer,
|
||||
dlcItemList,
|
||||
releaseInfo,
|
||||
categories,
|
||||
customAttributes
|
||||
} = metadata
|
||||
|
||||
const dlcs: string[] = []
|
||||
const CloudSaveFolder = customAttributes?.CloudSaveFolder
|
||||
const FolderName = customAttributes?.FolderName
|
||||
const canRunOffline = customAttributes?.CanRunOffline?.value === 'true'
|
||||
|
||||
if (dlcItemList) {
|
||||
dlcItemList.forEach(
|
||||
(v: { releaseInfo: { [x: number]: { appId: string } } }) => {
|
||||
if (v.releaseInfo && v.releaseInfo[0]) {
|
||||
dlcs.push(v.releaseInfo[0].appId)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let is_ue_asset = false
|
||||
let is_ue_project = false
|
||||
let is_ue_plugin = false
|
||||
if (categories) {
|
||||
categories.forEach((c: { path: string }) => {
|
||||
if (c.path === 'projects') {
|
||||
is_ue_project = true
|
||||
} else if (c.path === 'assets') {
|
||||
is_ue_asset = true
|
||||
} else if (c.path === 'plugins') {
|
||||
is_ue_plugin = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let compatible_apps: string[] = []
|
||||
releaseInfo.forEach((rI: { appId: string; compatibleApps: string[] }) => {
|
||||
if (rI.appId === app_name) {
|
||||
compatible_apps = rI.compatibleApps
|
||||
}
|
||||
})
|
||||
|
||||
const cloud_save_enabled = is_game && Boolean(CloudSaveFolder?.value)
|
||||
const saveFolder = cloud_save_enabled ? CloudSaveFolder.value : ''
|
||||
const installFolder = FolderName ? FolderName.value : app_name
|
||||
|
||||
const gameBox = is_game
|
||||
? keyImages.filter(({ type }: KeyImage) => type === 'DieselGameBox')[0]
|
||||
: keyImages.filter(({ type }: KeyImage) => type === 'Screenshot')[0]
|
||||
|
||||
const gameBoxTall = is_game
|
||||
? keyImages.filter(
|
||||
({ type }: KeyImage) => type === 'DieselGameBoxTall'
|
||||
)[0]
|
||||
: gameBox
|
||||
|
||||
const gameBoxStore = is_game
|
||||
? keyImages.filter(
|
||||
({ type }: KeyImage) => type === 'DieselStoreFrontTall'
|
||||
)[0]
|
||||
: gameBox
|
||||
|
||||
const logo = is_game
|
||||
? keyImages.filter(
|
||||
({ type }: KeyImage) => type === 'DieselGameBoxLogo'
|
||||
)[0]
|
||||
: keyImages.filter(({ type }: KeyImage) => type === 'Thumbnail')[0]
|
||||
|
||||
const art_cover = gameBox ? gameBox.url : null
|
||||
const art_logo = logo ? logo.url : null
|
||||
const art_square = gameBoxTall ? gameBoxTall.url : null
|
||||
const art_square_front = gameBoxStore ? gameBoxStore.url : null
|
||||
|
||||
const info = this.installedGames.get(app_name)
|
||||
const {
|
||||
executable = null,
|
||||
version = null,
|
||||
install_size = null,
|
||||
install_path = null,
|
||||
platform,
|
||||
is_dlc = metadata.categories.filter(
|
||||
({ path }: { path: string }) => path === 'dlc'
|
||||
).length || dlcs.includes(app_name)
|
||||
} = (info === undefined ? {} : info) as InstalledInfo
|
||||
|
||||
const convertedSize = install_size && getFileSize(Number(install_size))
|
||||
|
||||
this.library.set(app_name, {
|
||||
app_name,
|
||||
art_cover: art_cover || art_square || fallBackImage,
|
||||
art_logo,
|
||||
art_square:
|
||||
art_square || art_square_front || art_cover || fallBackImage,
|
||||
cloud_save_enabled,
|
||||
compatible_apps,
|
||||
developer,
|
||||
extra: {
|
||||
about: {
|
||||
description,
|
||||
shortDescription
|
||||
},
|
||||
reqs: {}
|
||||
},
|
||||
folder_name: installFolder,
|
||||
install: {
|
||||
executable,
|
||||
install_path,
|
||||
install_size: convertedSize,
|
||||
is_dlc,
|
||||
version,
|
||||
platform
|
||||
},
|
||||
is_game,
|
||||
is_installed: info !== undefined,
|
||||
is_ue_asset,
|
||||
is_ue_plugin,
|
||||
is_ue_project,
|
||||
namespace,
|
||||
is_mac_native: info
|
||||
? platform === 'Mac'
|
||||
: releaseInfo[0]?.platform.includes('Mac'),
|
||||
save_folder: saveFolder,
|
||||
title,
|
||||
canRunOffline,
|
||||
is_linux_native: false,
|
||||
runner: 'legendary',
|
||||
store_url: formatEpicStoreUrl(title)
|
||||
} as GameInfo)
|
||||
|
||||
return app_name
|
||||
} catch (error) {
|
||||
logError(
|
||||
`Library file corrupted, please check ${fileName}`,
|
||||
LogPrefix.Legendary,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the file partially, signalling that yes this is part of the library,
|
||||
* but we don't have it loaded fully. Saves on memory.
|
||||
*
|
||||
* @returns App name of loaded file.
|
||||
*/
|
||||
private loadFileStub(fileName: string): string {
|
||||
fileName = `${libraryPath}/${fileName}`
|
||||
try {
|
||||
const { app_name } = JSON.parse(readFileSync(fileName, 'utf-8'))
|
||||
this.library.set(app_name, null)
|
||||
return app_name
|
||||
} catch (error) {
|
||||
logError(`Metadata for ${fileName} corrupted`, LogPrefix.Legendary)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully loads all files in library into memory.
|
||||
*
|
||||
* @returns App names of loaded files.
|
||||
*/
|
||||
private async loadAll(): Promise<string[]> {
|
||||
if (existsSync(libraryPath)) {
|
||||
return readdirSync(libraryPath).map((filename) => this.loadFile(filename))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully loads all stubbed files in library into memory.
|
||||
* Currently unused.
|
||||
*
|
||||
* @returns App names of loaded files.
|
||||
*/
|
||||
public async loadAllStubs(): Promise<string[]> {
|
||||
if (existsSync(libraryPath)) {
|
||||
return readdirSync(libraryPath)
|
||||
.filter((fileName) => {
|
||||
const app_name = fileName.split('.json')[0]
|
||||
return this.library.get(app_name) === null
|
||||
})
|
||||
.map((filename) => this.loadFile(filename))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub loads all files in library into memory.
|
||||
*
|
||||
* @returns App names of loaded files.
|
||||
*/
|
||||
private async loadAsStubs(): Promise<string[]> {
|
||||
if (existsSync(libraryPath)) {
|
||||
return readdirSync(libraryPath).map((filename) =>
|
||||
this.loadFileStub(filename)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runLegendaryCommand(
|
||||
commandParts: string[],
|
||||
options?: CallRunnerOptions
|
||||
): Promise<ExecResult> {
|
||||
const { dir, bin } = getLegendaryBin()
|
||||
return callRunner(
|
||||
commandParts,
|
||||
{ name: 'legendary', logPrefix: LogPrefix.Legendary, bin, dir },
|
||||
options
|
||||
)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { existsSync, readFile } from 'graceful-fs'
|
||||
|
||||
import { UserInfo } from '../types'
|
||||
import { clearCache } from '../utils'
|
||||
import { userInfo, configStore } from '../constants'
|
||||
import { logError, logInfo, LogPrefix } from '../logger/logger'
|
||||
import { userInfo as user } from 'os'
|
||||
import { session } from 'electron'
|
||||
import { runLegendaryCommand } from './library'
|
||||
|
||||
export class LegendaryUser {
|
||||
public static async login(sid: string) {
|
||||
const commandParts = ['auth', '--sid', sid]
|
||||
|
||||
logInfo('Logging in with Legendary.', LogPrefix.Legendary)
|
||||
|
||||
try {
|
||||
await runLegendaryCommand(commandParts)
|
||||
const userInfo = await this.getUserInfo()
|
||||
return { status: 'done', data: userInfo }
|
||||
} catch (error) {
|
||||
logError(
|
||||
['Failed to login with Legendary:', `${error}`],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
|
||||
return { status: 'failed' }
|
||||
}
|
||||
}
|
||||
|
||||
public static async logout() {
|
||||
const commandParts = ['auth', '--delete']
|
||||
|
||||
logInfo('Logging out.', LogPrefix.Legendary)
|
||||
|
||||
const res = await runLegendaryCommand(commandParts)
|
||||
|
||||
if (res.error) {
|
||||
logError(['Failed to logout:', res.error], LogPrefix.Legendary)
|
||||
}
|
||||
|
||||
const ses = session.fromPartition('persist:epicstore')
|
||||
await ses.clearStorageData()
|
||||
await ses.clearCache()
|
||||
await ses.clearAuthCache()
|
||||
await ses.clearHostResolverCache()
|
||||
configStore.clear()
|
||||
clearCache()
|
||||
}
|
||||
|
||||
public static isLoggedIn() {
|
||||
return existsSync(userInfo)
|
||||
}
|
||||
|
||||
public static async getUserInfo(): Promise<UserInfo> {
|
||||
if (LegendaryUser.isLoggedIn()) {
|
||||
const userInfoContent = await new Promise<string>((resolve, reject) => {
|
||||
readFile(userInfo, 'utf-8', (err, content) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(content)
|
||||
}
|
||||
})
|
||||
})
|
||||
try {
|
||||
const userInfoObject = JSON.parse(userInfoContent)
|
||||
const info: UserInfo = {
|
||||
account_id: userInfoObject.account_id,
|
||||
displayName: userInfoObject.displayName,
|
||||
user: user().username
|
||||
}
|
||||
configStore.set('userInfo', info)
|
||||
return info
|
||||
} catch (error) {
|
||||
logError(
|
||||
`User info file corrupted, check ${userInfo}`,
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
const info: UserInfo = {
|
||||
account_id: '',
|
||||
displayName: '',
|
||||
user: ''
|
||||
}
|
||||
return info
|
||||
}
|
||||
}
|
||||
configStore.delete('userInfo')
|
||||
return { account_id: '', displayName: null }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
interface ShortcutsResult {
|
||||
success: boolean
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export { ShortcutsResult }
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"sourceMap": false,
|
||||
"outDir": "../public",
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "esnext"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["__tests__"]
|
||||
}
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
indentation: 4,
|
||||
// Indentation of the catalog files
|
||||
|
||||
input: ['src/**/*.{ts,tsx}', 'electron/**/*.{ts,tsx}'],
|
||||
input: ['src/**/*.{ts,tsx}'],
|
||||
// An array of globs that describe where to look for source files
|
||||
// relative to the location of the configuration file
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Heroic Games Launcher</title>
|
||||
<script type="module" src="/src/frontend/index.tsx"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div class="simple-keyboard"></div>
|
||||
</body>
|
||||
</html>
|
||||
+8
-2
@@ -1,16 +1,22 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
globals: {
|
||||
'ts-jest': {}
|
||||
},
|
||||
|
||||
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.config.js'],
|
||||
coverageDirectory: '<rootDir>/coverage',
|
||||
coveragePathIgnorePatterns: [
|
||||
'<rootDir>/node_modules',
|
||||
'<rootDir>/public',
|
||||
'<rootDir>/electron',
|
||||
'<rootDir>/dist',
|
||||
'<rootDir>/build',
|
||||
'<rootDir>/coverage'
|
||||
],
|
||||
coverageReporters: ['text', 'html'],
|
||||
projects: ['<rootDir>/electron'],
|
||||
projects: ['<rootDir>/src/backend'],
|
||||
|
||||
rootDir: '.'
|
||||
}
|
||||
+19
-35
@@ -2,7 +2,7 @@
|
||||
"name": "heroic",
|
||||
"version": "2.4.0",
|
||||
"private": true,
|
||||
"main": "public/main.js",
|
||||
"main": "build/electron/main.js",
|
||||
"homepage": "./",
|
||||
"license": "GPL-3.0-only",
|
||||
"description": "An Open Source Launcher for GOG and Epic Games",
|
||||
@@ -30,9 +30,6 @@
|
||||
"build/icon-dark.png",
|
||||
"build/icon-light.png"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "public"
|
||||
},
|
||||
"protocols": [
|
||||
{
|
||||
"name": "heroic",
|
||||
@@ -157,7 +154,6 @@
|
||||
"react-dom": "17",
|
||||
"react-i18next": "^11.16.7",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"shlex": "^2.1.2",
|
||||
"simple-keyboard": "^3.4.136",
|
||||
"source-map-support": "^0.5.21",
|
||||
@@ -166,26 +162,19 @@
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "DISABLE_ESLINT_PLUGIN=true nf start -p 3000 && electron-start",
|
||||
"electron-start": "ELECTRON_IS_DEV=1 node public/start-react",
|
||||
"electron": "yarn build-electron && electron . --trace-warnings",
|
||||
"react-start": "HOST=localhost BROWSER=none react-scripts start",
|
||||
"react-start:win": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"dev": "vite",
|
||||
"codecheck": "tsc --noEmit",
|
||||
"test": "jest --noStackTrace",
|
||||
"test-watch": "jest --watch --maxWorkers=25%",
|
||||
"test:ci": "jest --runInBand --silent",
|
||||
"eject": "react-scripts eject",
|
||||
"release:linux": "GH_TOKEN='${{ secrets.WORKFLOW_TOKEN }}' yarn build-electron && yarn build && electron-builder -c.extraMetadata.main=build/main.js -p always --linux deb AppImage rpm pacman tar.xz",
|
||||
"release:mac": "GH_TOKEN='${{ secrets.WORKFLOW_TOKEN }}' yarn build-electron && yarn build && electron-builder -c.extraMetadata.main=build/main.js -p always --mac",
|
||||
"release:win": "yarn build-electron && yarn build && electron-builder -c.extraMetadata.main=build/main.js -p always --win nsis portable",
|
||||
"dist:linux": "GH_TOKEN='${{ secrets.WORKFLOW_TOKEN }}' yarn build-electron && yarn build && electron-builder -c.extraMetadata.main=build/main.js --linux",
|
||||
"dist:mac": "GH_TOKEN='${{ secrets.WORKFLOW_TOKEN }}' yarn build-electron && yarn build && electron-builder -c.extraMetadata.main=build/main.js --mac",
|
||||
"dist:win": "yarn build-electron && yarn build && electron-builder -c.extraMetadata.main=build/main.js --win",
|
||||
"lint": "eslint -c .eslintrc --ext .tsx,ts ./src && eslint -c .eslintrc --ext .ts ./electron",
|
||||
"lint-fix": "eslint --fix -c .eslintrc --ext .tsx,ts ./src && eslint --fix -c .eslintrc --ext .ts ./electron",
|
||||
"build-electron": "tsc --project electron/tsconfig.json",
|
||||
"watch-electron": "tsc --watch --project electron/tsconfig.json",
|
||||
"release:linux": "vite build && electron-builder -p always --linux deb AppImage rpm pacman tar.xz",
|
||||
"release:mac": "vite build && electron-builder -p always --mac",
|
||||
"release:win": "vite build && electron-builder -p always --win nsis portable",
|
||||
"dist:linux": "vite build && electron-builder --linux",
|
||||
"dist:mac": "vite build && electron-builder --mac",
|
||||
"dist:win": "vite build && electron-builder --win",
|
||||
"lint": "eslint -c .eslintrc --ext .tsx,ts ./src",
|
||||
"lint-fix": "eslint --fix -c .eslintrc --ext .tsx,ts ./src",
|
||||
"dist-flatpak": "cd flatpak && flatpak-builder build local.heroic.yml --install --force-clean --user",
|
||||
"flatpak-test": "yarn dist:linux appimage && cd flatpak && flatpak-builder build local.heroic.yml --install --force-clean --user",
|
||||
"i18n": "i18next",
|
||||
@@ -216,29 +205,24 @@
|
||||
"@types/tmp": "^0.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.20.0",
|
||||
"@typescript-eslint/parser": "^5.20.0",
|
||||
"@vitejs/plugin-react": "^2.0.0",
|
||||
"electron": "^20.0.1",
|
||||
"electron-builder": "^23.1.0",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"foreman": "^3.0.1",
|
||||
"husky": "^7.0.4",
|
||||
"i18next-parser": "^6.3.0",
|
||||
"jest": "^28.1.3",
|
||||
"prettier": "^2.6.2",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version"
|
||||
]
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^3.0.3",
|
||||
"vite-plugin-electron": "^0.8.3",
|
||||
"vite-plugin-svgr": "^2.2.1",
|
||||
"vite-tsconfig-paths": "^3.5.0"
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Heroic Games Launcher</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "Тази игра включва софтуер против мамене"
|
||||
},
|
||||
"Assets": "Ресурси",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Кешът на Heroic беше изчистен.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Пускането е прекратено! Играта изисква връзка с Интернет, за да бъде пусната.",
|
||||
"title": "Не се поддържа режим извън линия."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Грешка",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Мак",
|
||||
"win": "Уиндоус"
|
||||
},
|
||||
"Plugins": "Добавки",
|
||||
"Projects": "Проекти",
|
||||
"Recent": "Играно наскоро",
|
||||
"search": "Търсене на игри",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Изберете тема",
|
||||
"showfps": "Показване на кадрите/сек (DX9, 10 и 11)",
|
||||
"showUnrealMarket": "Показване на Пазара на Unreal (изисква рестартиране)",
|
||||
"start-in-tray": "Стартиране в минимизирано състояние",
|
||||
"steamruntime": "Използване на средата за изпълнение на Steam",
|
||||
"winecrossoverbottle": "Бутилка CrossOver",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Мазагин на Epic",
|
||||
"stores": "Магазини",
|
||||
"title": {
|
||||
"allGames": "Всички игри",
|
||||
"allUnreal": "Unreal – Всичко",
|
||||
"unrealAssets": "Unreal – Ресурси",
|
||||
"unrealPlugins": "Unreal – Добавки",
|
||||
"unrealProjects": "Unreal – Проекти"
|
||||
"allGames": "Всички игри"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "Aquest joc inclou programari antitrampes."
|
||||
},
|
||||
"Assets": "Recursos",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "S'ha buidat la memòria cau de l'Heroic.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "L'execució del programa ha fallat. El joc necessita una connexió a Internet per funcionar.",
|
||||
"title": "El mode sense connexió no està suportat."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Error",
|
||||
"uncaught-exception": {
|
||||
"message": "S'ha produït una excepció no detectada:{{newLine}}{{error}}{{newLine}}{{newLine}}El Heroic es tancarà! Informeu de l'error al nostre dipòsit de Github.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Extensions",
|
||||
"Projects": "Projectes",
|
||||
"Recent": "Jugats fa poc",
|
||||
"search": "Cerca jocs",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Selecciona un tema",
|
||||
"showfps": "Mostra els FPS (DX9, 10 i 11)",
|
||||
"showUnrealMarket": "Mostra la llibreria d'Unreal (cal reiniciar)",
|
||||
"start-in-tray": "Inicia minimitzat",
|
||||
"steamruntime": "Usa el Runtime de l'Steam",
|
||||
"winecrossoverbottle": "Ampolla CrossOver",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Botiga de l'Epic",
|
||||
"stores": "Botigues",
|
||||
"title": {
|
||||
"allGames": "Tots els jocs",
|
||||
"allUnreal": "Unreal - Tot",
|
||||
"unrealAssets": "Unreal - Recursos",
|
||||
"unrealPlugins": "Unreal - Extensions",
|
||||
"unrealProjects": "Unreal - Projectes"
|
||||
"allGames": "Tots els jocs"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "Tato hra obsahuje anticheatový software"
|
||||
},
|
||||
"Assets": "Assety",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Mezipaměť Heroic vymazána.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Spouštění přerušeno! Ke spuštění hry je potřeba připojení k internetu.",
|
||||
"title": "Režim offline není podporován."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Error",
|
||||
"uncaught-exception": {
|
||||
"message": "Došlo k nezachycené výjimce:{{newLine}}{{error}}{{newLine}}{{newLine}} Heroic bude ukončen! Nahlaste výjimku v našem repozitáři na Githubu.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projekty",
|
||||
"Recent": "Recently Played",
|
||||
"search": "Vyhledat hry",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Vybrat motiv",
|
||||
"showfps": "Zobrazit FPS (DX9, 10 a 11)",
|
||||
"showUnrealMarket": "Zobrazit Unreal Marketplace (vyžaduje restart)",
|
||||
"start-in-tray": "Spustit minimalizované",
|
||||
"steamruntime": "Použít služby Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Obchody",
|
||||
"title": {
|
||||
"allGames": "Všechny hry",
|
||||
"allUnreal": "Unreal - Vše",
|
||||
"unrealAssets": "Unreal - Součásti",
|
||||
"unrealPlugins": "Unreal - Pluginy",
|
||||
"unrealProjects": "Unreal - Projekty"
|
||||
"allGames": "Všechny hry"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "Dieses Spiel enthält Anticheat-Software"
|
||||
},
|
||||
"Assets": "Assets",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic Cache wurde geleert.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Start abgebrochen! Das Spiel benötigt eine Internetverbindung, damit es läuft.",
|
||||
"title": "Offline-Modus wird nicht unterstützt."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Error",
|
||||
"uncaught-exception": {
|
||||
"message": "Ein unbekannter Fehler ist aufgetreten:{{newLine}}{{error}}{{newLine}}{{newLine}} Heroic wird geschlossen! Melde diesen Fehler in unserem Github-Repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projekte",
|
||||
"Recent": "Zuletzt gespielte Spiele",
|
||||
"search": "Suche nach Spielen",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Design auswählen",
|
||||
"showfps": "FPS anzeigen (DX9, 10 und 11)",
|
||||
"showUnrealMarket": "Unreal Marketplace anzeigen (benötigt Neustart)",
|
||||
"start-in-tray": "Minimiert starten",
|
||||
"steamruntime": "Steam Runtime verwenden",
|
||||
"winecrossoverbottle": "CrossOver Flasche",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Shops",
|
||||
"title": {
|
||||
"allGames": "Alle Spiele",
|
||||
"allUnreal": "Unreal - Alles",
|
||||
"unrealAssets": "Unreal - Assets",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projekte"
|
||||
"allGames": "Alle Spiele"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Κατάσταση",
|
||||
"title": "Αυτό το παιχνίδι περιλαμβάνει λογισμικό anticheat"
|
||||
},
|
||||
"Assets": "Αντικείμενα",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Η προσωρινή μνήμη Heroic καθαρίστηκε.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Η έναρξη διακόπηκε! Το παιχνίδι απαιτεί σύνδεση στο διαδύκτιο για να τρέξει.",
|
||||
"title": "Η λειτουργία εκτός σύνδεσης δεν υποστηρίζεται."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Σφάλμα",
|
||||
"uncaught-exception": {
|
||||
"message": "Συνέβη μια μη διαχειρίσιμη εξαίρεση:{{newLine}}{{error}}{{newLine}}{{newLine}} Το Heroic θα κλείσει! Κάντε αναφορά στο Github repository μας.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Πρότζεκτ",
|
||||
"Recent": "Παίχτηκε Πρόσφατα",
|
||||
"search": "Αναζήτηση για Παιχνίδια",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Επιλογή θέματος",
|
||||
"showfps": "Ένδειξη ταχύτητας καρέ (DX9, 10 και 11)",
|
||||
"showUnrealMarket": "Εμφάνιση Καταστήματος Unreal (απαιτεί επανεκίννηση)",
|
||||
"start-in-tray": "Ελαχιστοποιημένη Έναρξη",
|
||||
"steamruntime": "Χρήση Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Περιβάλλον",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Κατάστημα Epic",
|
||||
"stores": "Καταστήματα",
|
||||
"title": {
|
||||
"allGames": "Όλα τα παιχνίδια",
|
||||
"allUnreal": "Unreal - Όλα",
|
||||
"unrealAssets": "Unreal - Στοιχεία",
|
||||
"unrealPlugins": "Unreal - Πρόσθετα",
|
||||
"unrealProjects": "Unreal - Πρότζεκτ"
|
||||
"allGames": "Όλα τα παιχνίδια"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Assets",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic cache cleared.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Error",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projects",
|
||||
"Recent": "Played Recently",
|
||||
"search": "Search for Games",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "Show FPS (DX9, 10 and 11)",
|
||||
"showUnrealMarket": "Show Unreal Marketplace (needs restart)",
|
||||
"start-in-tray": "Start Minimized",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "All Games",
|
||||
"allUnreal": "Unreal - Everything",
|
||||
"unrealAssets": "Unreal - Assets",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projects"
|
||||
"allGames": "All Games"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Estado",
|
||||
"title": "Este juego contiene software anticheat"
|
||||
},
|
||||
"Assets": "Recursos",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "El caché de Heroic ha sido limpiado.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "¡Lanzamiento abortado! El juego requiere una conexión a Internet ser ejecutado.",
|
||||
"title": "El modo sin conexión no está soportado."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Error",
|
||||
"uncaught-exception": {
|
||||
"message": "Ocurrió un error:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic va a ser cerrado! Reporta el error en nuestro repositorio de Github.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "macOS",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Proyectos",
|
||||
"Recent": "Jugado recientemente",
|
||||
"search": "Buscar juegos",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Seleccionar Tema",
|
||||
"showfps": "Mostrar FPS (Cuadros Por Segundo) (DX9, 10 y 11)",
|
||||
"showUnrealMarket": "Mostrar la tienda de Unreal (requiere reiniciar)",
|
||||
"start-in-tray": "Iniciar minimizado",
|
||||
"steamruntime": "Usar Steam Runtime",
|
||||
"winecrossoverbottle": "Botella de CrossOver",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Tienda de Epic",
|
||||
"stores": "Tiendas",
|
||||
"title": {
|
||||
"allGames": "Todos los juegos",
|
||||
"allUnreal": "Unreal - Todo",
|
||||
"unrealAssets": "Unreal - Recursos",
|
||||
"unrealPlugins": "Unreal - Plug-ins",
|
||||
"unrealProjects": "Unreal - Proyectos"
|
||||
"allGames": "Todos los juegos"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "See mäng sisaldab sohitegemisvastast tarkvara"
|
||||
},
|
||||
"Assets": "Varamu",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic-u vahemälu puhastati.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Käivitamine katkestatud! Mängu käivitamiseks on vaja internetiühendust.",
|
||||
"title": "Võrguühenduseta režiim ei ole toetatud."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Viga",
|
||||
"uncaught-exception": {
|
||||
"message": "Tekkis tabamata erand:{{newLine}}{{error}}{{newLine}}{{newLine}} Heroic suletakse! Teatage erandist meie Githubi repositooriumis.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Pluginad",
|
||||
"Projects": "Projektid",
|
||||
"Recent": "Hiljuti mängitud",
|
||||
"search": "Otsi mänge",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Valige teema",
|
||||
"showfps": "Näita FPS-i (DX9, 10 ja 11)",
|
||||
"showUnrealMarket": "Näita Unreal Marketplace'i (vajab taaskäivitamist)",
|
||||
"start-in-tray": "Käivita minimeeritult",
|
||||
"steamruntime": "Kasuta Steam Runtime'i",
|
||||
"winecrossoverbottle": "CrossOveri pudel",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Pood",
|
||||
"stores": "Poed",
|
||||
"title": {
|
||||
"allGames": "Kõik mängud",
|
||||
"allUnreal": "Unreal - Kõik",
|
||||
"unrealAssets": "Unreal - varamu",
|
||||
"unrealPlugins": "Unreal - pluginad",
|
||||
"unrealProjects": "Unreal - Projektid"
|
||||
"allGames": "Kõik mängud"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "این بازی شامل نرم افزار آنتی چیت می باشد"
|
||||
},
|
||||
"Assets": "دارایی ها",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "کش Heroic پاک شد.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "اجرا متوقف شد! بازی برای اجرا به اتصال اینترنت نیاز دارد.",
|
||||
"title": "حالت آفلاین پشتیبانی نمیشود."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "خطا",
|
||||
"uncaught-exception": {
|
||||
"message": "یک خطای نامشخص رخ داد:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic بسته خواهد شد! خطا را در ریپوزیتوری گیتهاب ما گزارش دهید.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "مک",
|
||||
"win": "ویندوز"
|
||||
},
|
||||
"Plugins": "افزونهها",
|
||||
"Projects": "پروژهها",
|
||||
"Recent": "اخیرا بازی شده",
|
||||
"search": "جست و جو برای بازی ها",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "انتخاب تم",
|
||||
"showfps": "نمایش تعداد فریم (11 و DX9, 10)",
|
||||
"showUnrealMarket": "نمایش فروشگاه Unreal (نیازمند راهاندازی مجدد)",
|
||||
"start-in-tray": "شروع در حالت پسزمینه",
|
||||
"steamruntime": "استفاده از Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver باتل",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "فروشگاه اپیک",
|
||||
"stores": "فروشگاهها",
|
||||
"title": {
|
||||
"allGames": "همه بازیها",
|
||||
"allUnreal": "Unreal - همهچیز",
|
||||
"unrealAssets": "Unreal - مهرهها",
|
||||
"unrealPlugins": "Unreal - افزونهها",
|
||||
"unrealProjects": "Unreal - پروژهها"
|
||||
"allGames": "همه بازیها"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Omaisuuserät",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic:in välimuisti tyhjennettiin.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Käynnistys keskeytetty! Peli vaatii Internet-yhteyden toimiakseen.",
|
||||
"title": "Offline-tilaa ei tueta."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Virhe",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Laajennukset",
|
||||
"Projects": "Projektit",
|
||||
"Recent": "Viimeksi pelatut",
|
||||
"search": "Kirjoita tähän pelin nimi...",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "Näytä kuvataajuus (DX9, 10 ja 11)",
|
||||
"showUnrealMarket": "Näytä Unreal Marketplace (tarvitsee uudelleenkäynnistyksen)",
|
||||
"start-in-tray": "Käynnistä minimoituna ilmoitusalueelle",
|
||||
"steamruntime": "Käytä Steam Runtimea",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic-kauppa",
|
||||
"stores": "Kaupat",
|
||||
"title": {
|
||||
"allGames": "Kaikki pelit",
|
||||
"allUnreal": "Unreal - Kaikki",
|
||||
"unrealAssets": "Unreal - Assets",
|
||||
"unrealPlugins": "Unreal - Liitännäiset",
|
||||
"unrealProjects": "Unreal - Projektit"
|
||||
"allGames": "Kaikki pelit"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Objets",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Le cache Heroic a été effacé !",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Erreur",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projets",
|
||||
"Recent": "Joué récemment",
|
||||
"search": "Chercher un jeu",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Sélectionner le thème",
|
||||
"showfps": "Afficher les FPS (DX9, 10 et 11)",
|
||||
"showUnrealMarket": "Afficher Unreal Marketplace (nécessite le redémarrage d'Heroic)",
|
||||
"start-in-tray": "Réduire au démarrage",
|
||||
"steamruntime": "Utiliser le runtime Steam",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Boutique Epic",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "Tous les jeux",
|
||||
"allUnreal": "Unreal - Tout",
|
||||
"unrealAssets": "Unreal - Ressources",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projets"
|
||||
"allGames": "Tous les jeux"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Recursos",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Limpouse a caché de Heroic.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Lanzamento abortado! O xogo reqire dunha conexión a Internet para executalo.",
|
||||
"title": "O modo sin conexión non está soportado."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Erro",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "macOS",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Engadidos",
|
||||
"Projects": "Proxectos",
|
||||
"Recent": "Xogado recentemente",
|
||||
"search": "Escribe o nome do xogo aquí...",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "Mostrar FPS (DX9, 10 e 11)",
|
||||
"showUnrealMarket": "Mostrar a tenda de Unreal (require reinicio)",
|
||||
"start-in-tray": "Iniciar minimizado",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "Todos os xogos",
|
||||
"allUnreal": "Unreal - Todo",
|
||||
"unrealAssets": "Unreal - Recursos",
|
||||
"unrealPlugins": "Recursos - Engadidos",
|
||||
"unrealProjects": "Unreal - Proxectos"
|
||||
"allGames": "Todos os xogos"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Assets",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic cache je očišćen.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Greška",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projects",
|
||||
"Recent": "Played Recently",
|
||||
"search": "Traži igre",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "Pokaži FPS (DX9, 10 i 11)",
|
||||
"showUnrealMarket": "Show Unreal Marketplace (needs restart)",
|
||||
"start-in-tray": "Start Minimized",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "All Games",
|
||||
"allUnreal": "Unreal - Everything",
|
||||
"unrealAssets": "Unreal - Assets",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projects"
|
||||
"allGames": "All Games"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Állapot",
|
||||
"title": "Ez a játék csalásellenes szoftvert tartalmaz"
|
||||
},
|
||||
"Assets": "Eszközök",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic gyorsítótár törölve.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Hiba",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Beépülők",
|
||||
"Projects": "Projektek",
|
||||
"Recent": "Nemrég játszott",
|
||||
"search": "Játékok keresése",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "FPS megjelenítése (DX9, 10 és 11)",
|
||||
"showUnrealMarket": "Unreal piactér megjelenítése (újraindítás szükséges)",
|
||||
"start-in-tray": "Indítás tálcára téve",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver palack",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "Összes játék",
|
||||
"allUnreal": "Unreal - Minden",
|
||||
"unrealAssets": "Unreal - Eszközök",
|
||||
"unrealPlugins": "Unreal - Bővítmények",
|
||||
"unrealProjects": "Unreal - Projektek"
|
||||
"allGames": "Összes játék"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "Gim ini mengandung perangkat lunak anticheat"
|
||||
},
|
||||
"Assets": "Aset",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Tembolok Heroic telah dibersihkan.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Peluncuran dibatalkan! Gim membutuhkan koneksi internet untuk menjalankannya.",
|
||||
"title": "Mode luring tidak didukung."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Galat",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Pengaya",
|
||||
"Projects": "Proyek",
|
||||
"Recent": "Dimainkan Baru-Baru Ini",
|
||||
"search": "Cari Gim",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Pilih Tema",
|
||||
"showfps": "Tampilkan FPS (DX9, 10, dan 11)",
|
||||
"showUnrealMarket": "Tampilkan Unreal Marketplace (perlu memulai ulang)",
|
||||
"start-in-tray": "Mulai Diminimalkan",
|
||||
"steamruntime": "Gunakan Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Toko",
|
||||
"title": {
|
||||
"allGames": "Semua Gim",
|
||||
"allUnreal": "Unreal - Semuanya",
|
||||
"unrealAssets": "Unreal - Aset",
|
||||
"unrealPlugins": "Unreal - Pengaya",
|
||||
"unrealProjects": "Unreal - Proyek"
|
||||
"allGames": "Semua Gim"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "Questo gioco include software anticheat"
|
||||
},
|
||||
"Assets": "Asset",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Cache di Heroic cancellata.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Avvio interrotto! Il gioco richiede una connessione a Internet per essere eseguito.",
|
||||
"title": "Modalità offline non supportata."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Errore",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Progetti",
|
||||
"Recent": "Giocato di recente",
|
||||
"search": "Cerca giochi",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "Mostra FPS (DX9, 10 e 11)",
|
||||
"showUnrealMarket": "Mostra Unreal Marketplace (richiede riavvio)",
|
||||
"start-in-tray": "Avvio Minimizzato",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "Bottiglia CrossOver",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "Tutti i giochi",
|
||||
"allUnreal": "Unreal - Tutto",
|
||||
"unrealAssets": "Unreal - Risorse",
|
||||
"unrealPlugins": "Unreal - Plugin",
|
||||
"unrealProjects": "Unreal - Progetti"
|
||||
"allGames": "Tutti i giochi"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "資産",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic Cache Was Cleared!",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "エラー",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "マック",
|
||||
"win": "ウィンドウズ"
|
||||
},
|
||||
"Plugins": "プラグイン",
|
||||
"Projects": "プロジェクト",
|
||||
"Recent": "最近のゲーム",
|
||||
"search": "ここにゲーム名を入力してください...",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "FPSを表示(DX9、10、11)",
|
||||
"showUnrealMarket": "Show Unreal Marketplace (needs restart)",
|
||||
"start-in-tray": "最小化を開始",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver ボトル",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "ストア",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "All Games",
|
||||
"allUnreal": "Unreal - Everything",
|
||||
"unrealAssets": "Unreal - Assets",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projects"
|
||||
"allGames": "All Games"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "안티 치트 소프트웨어가 포함된 게임입니다"
|
||||
},
|
||||
"Assets": "에셋",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic 캐시 삭제됨.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "시작 실패! 게임을 실행하려면 인터넷 연결이 필요합니다.",
|
||||
"title": "오프라인 모드는 지원되지 않습니다."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "오류",
|
||||
"uncaught-exception": {
|
||||
"message": "잡히지 않는 예외가 발생했습니다:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic이 종료 됩니다! Github 레포지트리에 이 예외를 보고하세요.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "플러그인",
|
||||
"Projects": "프로젝트",
|
||||
"Recent": "최근 플레이한 게임",
|
||||
"search": "게임 검색",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "FPS 표시 (DX9, 10 및 11)",
|
||||
"showUnrealMarket": "Unreal Marketplace 표시 (다시 시작해야 함)",
|
||||
"start-in-tray": "최소화로 시작",
|
||||
"steamruntime": "Steam 런타임 사용",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Store",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "모든 게임",
|
||||
"allUnreal": "Unreal - Everything",
|
||||
"unrealAssets": "Unreal - 에셋",
|
||||
"unrealPlugins": "Unreal - 플러그인",
|
||||
"unrealProjects": "Unreal - 프로젝트"
|
||||
"allGames": "모든 게임"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "വഹകള്",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "ഹീറോയിക് പിടിച്ചുവച്ചവ വിട്ടു.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "കുഴപ്പം",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "മാക്",
|
||||
"win": "വിൻഡോസ്"
|
||||
},
|
||||
"Plugins": "കൂടുതലുകള്",
|
||||
"Projects": "ഏര്പാടുകള്",
|
||||
"Recent": "പതിവ് കളികള്",
|
||||
"search": "കളിയുടെ പേര് ഇവിടെ എഴുതൂ...",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "ചിത്രനിരക്ക് കാണിക്കൂ (DX9ലും 10ലും 11ലും)",
|
||||
"showUnrealMarket": "Show Unreal Marketplace (needs restart)",
|
||||
"start-in-tray": "ചെറിയ കളമാക്കി തുറക്കൂ",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "ക്രോസ്സോവര് കുപ്പി",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "കട",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "മൊത്തം കളികള്",
|
||||
"allUnreal": "അണ്റിയല് - എല്ലാം",
|
||||
"unrealAssets": "അണ്റിയല് - വകകള്",
|
||||
"unrealPlugins": "അണ്റിയല് - Plugins",
|
||||
"unrealProjects": "അണ്റിയല് - പണികള്"
|
||||
"allGames": "മൊത്തം കളികള്"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Assets",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic Cache Was Cleared!",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Error",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "ramen"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projecten",
|
||||
"Recent": "Recente Spellen",
|
||||
"search": "Voor je spel naam hier in...",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "Toon FPS (DX9, DX10 en DX11)",
|
||||
"showUnrealMarket": "Show Unreal Marketplace (needs restart)",
|
||||
"start-in-tray": "Start Geminimaliseerd",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Winkel",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "All Games",
|
||||
"allUnreal": "Unreal - Everything",
|
||||
"unrealAssets": "Unreal - Assets",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projects"
|
||||
"allGames": "All Games"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "Ta gra wykorzystuje system anticheat"
|
||||
},
|
||||
"Assets": "Assety",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Wyczyszczono pamięć podręczną Heroic.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Uruchomienie przerwane! Gra wymaga połączenia z internetem aby ją uruchomić.",
|
||||
"title": "Tryb Offline nie jest wspierany."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Error",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Rozszerzenia",
|
||||
"Projects": "Projekty",
|
||||
"Recent": "Ostatnio uruchamiane",
|
||||
"search": "Wyszukaj gry",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Wybierz Motyw",
|
||||
"showfps": "Pokaż FPS (DX9, DX10, DX11)",
|
||||
"showUnrealMarket": "Pokaż Unreal Marketplace (wymaga ponownego uruchomienia)",
|
||||
"start-in-tray": "Uruchamiaj zminimalizowany",
|
||||
"steamruntime": "Używaj Steam Runtime",
|
||||
"winecrossoverbottle": "Butelka CrossOver",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Sklep Epic",
|
||||
"stores": "Sklepy",
|
||||
"title": {
|
||||
"allGames": "Wszystkie gry",
|
||||
"allUnreal": "Unreal - Wszystko",
|
||||
"unrealAssets": "Unreal - Assety",
|
||||
"unrealPlugins": "Unreal - Pluginy",
|
||||
"unrealProjects": "Unreal - Projekty"
|
||||
"allGames": "Wszystkie gry"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Assets",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Cache do Heroic foi limpo!",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Error",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "janelas"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projetos",
|
||||
"Recent": "Played Recently",
|
||||
"search": "Digite o nome do jogo...",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "Mostrar FPS (DX9, 10 e 11)",
|
||||
"showUnrealMarket": "Show Unreal Marketplace (needs restart)",
|
||||
"start-in-tray": "Iniciar Minimizado",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Loja Epic",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "All Games",
|
||||
"allUnreal": "Unreal - Everything",
|
||||
"unrealAssets": "Unreal - Assets",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projects"
|
||||
"allGames": "All Games"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "Este jogo inclui software anticheat"
|
||||
},
|
||||
"Assets": "Recursos",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "O cache do Heroic foi limpo.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Lançamento cancelado! O jogo requer uma conexão com a internet para executá-lo.",
|
||||
"title": "Modo offline não suportado."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Erro",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projetos",
|
||||
"Recent": "Jogados Recentemente",
|
||||
"search": "Buscar jogos",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Selecione um tema",
|
||||
"showfps": "Exibir contador de FPS (DX9, 10 e 11)",
|
||||
"showUnrealMarket": "Mostrar Unreal Marketplace (requer reinício)",
|
||||
"start-in-tray": "Iniciar minimizado",
|
||||
"steamruntime": "Utilizar Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Loja Epic",
|
||||
"stores": "Lojas",
|
||||
"title": {
|
||||
"allGames": "Todos os Jogos",
|
||||
"allUnreal": "Unreal - Tudo",
|
||||
"unrealAssets": "Unreal - Recursos",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projetos"
|
||||
"allGames": "Todos os Jogos"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Состояние",
|
||||
"title": "Эта игра включает в себя античит"
|
||||
},
|
||||
"Assets": "Ресурсы",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Кэш Heroic очищен.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Запуск прерван! Для запуска игры требуется подключение к интернету.",
|
||||
"title": "Автономный режим не поддерживается."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Ошибка",
|
||||
"uncaught-exception": {
|
||||
"message": "Произошло необрабатываемое исключение:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic будет закрыт! Сообщите об исключении в нашем репозитории Github.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Плагины",
|
||||
"Projects": "Проекты",
|
||||
"Recent": "Запускались недавно",
|
||||
"search": "Поиск игр",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Выбор темы",
|
||||
"showfps": "Показывать FPS (DX9, 10 и 11)",
|
||||
"showUnrealMarket": "Показать Unreal Marketplace (требуется перезапуск)",
|
||||
"start-in-tray": "Запускать свернутым в трей",
|
||||
"steamruntime": "Использовать среду выполнения Steam",
|
||||
"winecrossoverbottle": "Бутылка CrossOver",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Магазин Epic",
|
||||
"stores": "Магазины",
|
||||
"title": {
|
||||
"allGames": "Все игры",
|
||||
"allUnreal": "Unreal - Все",
|
||||
"unrealAssets": "Unreal - Ресурсы",
|
||||
"unrealPlugins": "Unreal - Плагины",
|
||||
"unrealProjects": "Unreal - Проекты"
|
||||
"allGames": "Все игры"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Tillgångar",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic cache rensades.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Speluppstart avbröts! Internetanslutning krävs.",
|
||||
"title": "Offline-läge stöds ej."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Problem",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugins",
|
||||
"Projects": "Projekt",
|
||||
"Recent": "Nyligen spelat",
|
||||
"search": "Sök efter spel",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Välj tema",
|
||||
"showfps": "Visa FPS (DX9, 10 och 11)",
|
||||
"showUnrealMarket": "Visa Unreal Marketplace (kräver omstart)",
|
||||
"start-in-tray": "Starta minimerad",
|
||||
"steamruntime": "Använd Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Butik",
|
||||
"stores": "Butiker",
|
||||
"title": {
|
||||
"allGames": "Alla spel",
|
||||
"allUnreal": "Unreal - allt",
|
||||
"unrealAssets": "Unreal - Tillgångar",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projekt"
|
||||
"allGames": "Alla spel"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "சொத்து",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "ஹீராய்க் இன் cache அழிக்கப்பட்டது",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Launch aborted! The game requires a internet connection to run it.",
|
||||
"title": "Offline mode not supported."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "பிழை",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "மேக்",
|
||||
"win": "விண்டோஸ்"
|
||||
},
|
||||
"Plugins": "செருகுநிரல்கள்",
|
||||
"Projects": "செயல்திட்டம்",
|
||||
"Recent": "சமீபத்தில் விளையாடியவை",
|
||||
"search": "விளையாட்டுகளைத் தேடு",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Select Theme",
|
||||
"showfps": "FPS ஐக் காட்டு (DX9, 10 and 11)",
|
||||
"showUnrealMarket": "Show Unreal Marketplace (needs restart)",
|
||||
"start-in-tray": "Start Minimized",
|
||||
"steamruntime": "Use Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic விற்பனைக்கூடம்",
|
||||
"stores": "Stores",
|
||||
"title": {
|
||||
"allGames": "All Games",
|
||||
"allUnreal": "Unreal - Everything",
|
||||
"unrealAssets": "Unreal - Assets",
|
||||
"unrealPlugins": "Unreal - Plugins",
|
||||
"unrealProjects": "Unreal - Projects"
|
||||
"allGames": "All Games"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Varlıklar",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic önbelleği temizlendi.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Başlatma iptal edildi! Oyunu çalıştırmak için internet bağlantısı gereklidir.",
|
||||
"title": "Çevrim dışı mod desteklenmiyor."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Hata",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Eklentiler",
|
||||
"Projects": "Projeler",
|
||||
"Recent": "Son Oynanan",
|
||||
"search": "Oyun Ara",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Tema Seç",
|
||||
"showfps": "FPS'i Göster (DX9, 10 and 11)",
|
||||
"showUnrealMarket": "Unreal Pazar Yerini Göster (yeniden başlatma gerekir)",
|
||||
"start-in-tray": "Simge Durumunda Başlat",
|
||||
"steamruntime": "Steam Çalışma Zamanını Kullan",
|
||||
"winecrossoverbottle": "CrossOver Şişesi",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic Mağazası",
|
||||
"stores": "Mağazalar",
|
||||
"title": {
|
||||
"allGames": "Tüm Oyunlar",
|
||||
"allUnreal": "Unreal - Her şey",
|
||||
"unrealAssets": "Unreal - Varlıklar",
|
||||
"unrealPlugins": "Unreal - Eklentiler",
|
||||
"unrealProjects": "Unreal - Projeler"
|
||||
"allGames": "Tüm Oyunlar"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Стан",
|
||||
"title": "Ця гра містить в собі систему античитів"
|
||||
},
|
||||
"Assets": "Надатки",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Кеш Heroic очищено.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Запуск перервано! Гра потребує з'єднання з інтернетом.",
|
||||
"title": "Автономний режим не підтримується."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Помилка",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Плагіни",
|
||||
"Projects": "Проєкти",
|
||||
"Recent": "Нещодавно зіграні",
|
||||
"search": "Пошук ігор",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "Оберіть тему",
|
||||
"showfps": "Показувати кількість кадрів (DX9, 10 та 11)",
|
||||
"showUnrealMarket": "Показувати торговий майданчик Unreal (необхідний перезапуск)",
|
||||
"start-in-tray": "Запускати згорнутим",
|
||||
"steamruntime": "Використовувати Steam Runtime",
|
||||
"winecrossoverbottle": "Пляшка CrossOver",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Крамниця Epic",
|
||||
"stores": "Крамниці",
|
||||
"title": {
|
||||
"allGames": "Усі ігри",
|
||||
"allUnreal": "Unreal - Всі",
|
||||
"unrealAssets": "Unreal - Надатки",
|
||||
"unrealPlugins": "Unreal - Плагіни",
|
||||
"unrealProjects": "Unreal - Проєкти"
|
||||
"allGames": "Усі ігри"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "This game includes anticheat software"
|
||||
},
|
||||
"Assets": "Asset",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Đã xoá bộ nhớ đệm của Heroic.",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "Quá trình khởi chạy đã hủy! Cần có kết nối mạng để chạy game.",
|
||||
"title": "Không hỗ trợ chế độ offline."
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "Lỗi",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "Plugin",
|
||||
"Projects": "Project",
|
||||
"Recent": "Chơi gần đây",
|
||||
"search": "Tìm kiếm game",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "chọn Theme",
|
||||
"showfps": "Hiển thị FPS (DX9, 10 và 11)",
|
||||
"showUnrealMarket": "Hiển thị Unreal Marketplace (cần khởi động lại)",
|
||||
"start-in-tray": "Thu nhỏ khi khởi động",
|
||||
"steamruntime": "Sử dụng Steam Runtime",
|
||||
"winecrossoverbottle": "CrossOver Bottle",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Cửa hàng Epic",
|
||||
"stores": "Cửa hàng",
|
||||
"title": {
|
||||
"allGames": "Tất cả game",
|
||||
"allUnreal": "Unreal - Tất cả",
|
||||
"unrealAssets": "Unreal - Asset",
|
||||
"unrealPlugins": "Unreal - Plugin",
|
||||
"unrealProjects": "Unreal - Project"
|
||||
"allGames": "Tất cả game"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "状态",
|
||||
"title": "此游戏包含反作弊软件"
|
||||
},
|
||||
"Assets": "素材",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic 缓存已清理。",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "启动失败!游戏需要网络连接才能运行。",
|
||||
"title": "不支持离线模式。"
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "错误",
|
||||
"uncaught-exception": {
|
||||
"message": "发生了未捕获的异常:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic 将被关闭!在我们的 Github 仓库报告这个异常。",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "插件",
|
||||
"Projects": "项目",
|
||||
"Recent": "最近玩过",
|
||||
"search": "搜索游戏",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "选择主题",
|
||||
"showfps": "显示帧率(DX 9、10 和 11)",
|
||||
"showUnrealMarket": "显示虚幻市场(需重启)",
|
||||
"start-in-tray": "启动时最小化",
|
||||
"steamruntime": "使用 Steam 运行时",
|
||||
"winecrossoverbottle": "CrossOver 容器",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic 商店",
|
||||
"stores": "商店",
|
||||
"title": {
|
||||
"allGames": "所有游戏",
|
||||
"allUnreal": "虚幻 - 所有",
|
||||
"unrealAssets": "虚幻 - 素材",
|
||||
"unrealPlugins": "虚幻 - 插件",
|
||||
"unrealProjects": "虚幻 - 项目"
|
||||
"allGames": "所有游戏"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"status": "Status",
|
||||
"title": "此遊戲包含反作弊軟體"
|
||||
},
|
||||
"Assets": "素材",
|
||||
"box": {
|
||||
"cache-cleared": {
|
||||
"message": "Heroic快取已清理。",
|
||||
@@ -59,10 +58,7 @@
|
||||
"message": "",
|
||||
"title": ""
|
||||
},
|
||||
"no-offline-mode": {
|
||||
"message": "啟動失敗!遊戲需要網路才能執行。",
|
||||
"title": "不支持離線模式。"
|
||||
},
|
||||
"launchAborted": "Launch aborted",
|
||||
"title": "錯誤",
|
||||
"uncaught-exception": {
|
||||
"message": "A uncaught exception occured:{{newLine}}{{error}}{{newLine}}{{newLine}}Heroic will be closed! Report the exception on our Github repository.",
|
||||
@@ -364,8 +360,6 @@
|
||||
"mac": "Mac",
|
||||
"win": "Windows"
|
||||
},
|
||||
"Plugins": "外掛插件",
|
||||
"Projects": "專案",
|
||||
"Recent": "最近遊玩的遊戲",
|
||||
"search": "搜尋遊戲",
|
||||
"setting": {
|
||||
@@ -459,7 +453,6 @@
|
||||
},
|
||||
"select_theme": "選擇主題",
|
||||
"showfps": "顯示幀數(Directx 9,10和11)",
|
||||
"showUnrealMarket": "顯示 Unreal Marketplace(需要重啟)",
|
||||
"start-in-tray": "啟動時最小化",
|
||||
"steamruntime": "使用 Steam 執行環境",
|
||||
"winecrossoverbottle": "CrossOver 容器",
|
||||
@@ -519,11 +512,7 @@
|
||||
"store": "Epic 商店",
|
||||
"stores": "商店",
|
||||
"title": {
|
||||
"allGames": "所有遊戲",
|
||||
"allUnreal": "Unreal - 所有項目",
|
||||
"unrealAssets": "Unreal - 素材",
|
||||
"unrealPlugins": "Unreal - 外掛插件",
|
||||
"unrealProjects": "Unreal - 專案"
|
||||
"allGames": "所有遊戲"
|
||||
},
|
||||
"toolbox": {
|
||||
"settings": {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
const net = require('net')
|
||||
const { exec } = require('child_process')
|
||||
|
||||
const port = process.env.PORT ? process.env.PORT - 100 : 3000
|
||||
|
||||
process.env.ELECTRON_START_URL = `http://localhost:${port}`
|
||||
|
||||
const client = new net.Socket()
|
||||
|
||||
let startedElectron = false
|
||||
const tryConnection = () => {
|
||||
client.connect({ port }, () => {
|
||||
client.end()
|
||||
if (!startedElectron) {
|
||||
console.log('starting electron')
|
||||
startedElectron = true
|
||||
exec('npm run electron')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
tryConnection()
|
||||
|
||||
client.on('error', () => {
|
||||
setTimeout(tryConnection, 1000)
|
||||
})
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-2
@@ -1,5 +1,5 @@
|
||||
import { readFileSync, copyFileSync, mkdirSync } from 'graceful-fs'
|
||||
import { GameInfo } from '../../types'
|
||||
import { GameInfo } from 'common/types'
|
||||
import { join } from 'path'
|
||||
import { DirResult, dirSync } from 'tmp'
|
||||
import {
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
removeNonSteamGame
|
||||
} from '../../shortcuts/nonesteamgame/nonesteamgame'
|
||||
import { dialog } from 'electron'
|
||||
import { createNewLogFileAndClearOldOnces } from '../../logger/logger'
|
||||
|
||||
jest.mock('../../logger/logger', () => {
|
||||
const original = jest.requireActual('../../logger/logger')
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,13 @@
|
||||
import { quoteIfNecessary, removeQuoteIfNecessary, semverGt } from '../utils'
|
||||
|
||||
jest.mock('../logger/logger', () => {
|
||||
const original = jest.requireActual('../logger/logger')
|
||||
return {
|
||||
...original,
|
||||
createNewLogFileAndClearOldOnces: jest.fn().mockReturnValue('')
|
||||
}
|
||||
})
|
||||
|
||||
describe('electron/utils.ts', () => {
|
||||
test('quoteIfNeccessary', () => {
|
||||
const testCases = new Map<string, string>([
|
||||
File renamed without changes.
@@ -2,7 +2,7 @@ import { heroicAnticheatDataPath, isLinux } from '../constants'
|
||||
import * as axios from 'axios'
|
||||
import { logInfo, LogPrefix, logWarning } from '../logger/logger'
|
||||
import { readFileSync, writeFileSync } from 'graceful-fs'
|
||||
import { AntiCheatInfo } from '../types'
|
||||
import { AntiCheatInfo } from 'common/types'
|
||||
|
||||
async function downloadAntiCheatData() {
|
||||
if (!isLinux) return
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
import { homedir, userInfo as user } from 'os'
|
||||
import { parse as plistParse, PlistObject } from 'plist'
|
||||
|
||||
import { AppSettings, GlobalConfigVersion, WineInstallation } from './types'
|
||||
import {
|
||||
AppSettings,
|
||||
GlobalConfigVersion,
|
||||
WineInstallation
|
||||
} from 'common/types'
|
||||
import { LegendaryUser } from './legendary/user'
|
||||
import {
|
||||
currentGlobalConfigVersion,
|
||||
@@ -40,6 +44,7 @@ abstract class GlobalConfig {
|
||||
|
||||
public abstract version: GlobalConfigVersion
|
||||
|
||||
// @ts-expect-error TODO: Somehow figure out how to synchronously load the config
|
||||
public config: AppSettings
|
||||
|
||||
/**
|
||||
@@ -470,6 +475,7 @@ class GlobalConfigV0 extends GlobalConfig {
|
||||
const userName = user().username
|
||||
const defaultWine = isWindows ? {} : await this.getDefaultWine()
|
||||
|
||||
// @ts-expect-error TODO: We need to settle on *one* place to define settings defaults
|
||||
return {
|
||||
checkUpdatesInterval: 10,
|
||||
enableUpdates: false,
|
||||
@@ -489,7 +495,6 @@ class GlobalConfigV0 extends GlobalConfig {
|
||||
nvidiaPrime: false,
|
||||
enviromentOptions: [],
|
||||
wrapperOptions: [],
|
||||
showUnrealMarket: false,
|
||||
showFps: false,
|
||||
useGameMode: false,
|
||||
userInfo: {
|
||||
@@ -1,10 +1,10 @@
|
||||
import { spawnSync } from 'child_process'
|
||||
import { homedir, platform } from 'os'
|
||||
import { join } from 'path'
|
||||
import { join, resolve } from 'path'
|
||||
import Store from 'electron-store'
|
||||
import { parse } from '@node-steam/vdf'
|
||||
|
||||
import { GameConfigVersion, GlobalConfigVersion } from './types'
|
||||
import { GameConfigVersion, GlobalConfigVersion } from 'common/types'
|
||||
import {
|
||||
createNewLogFileAndClearOldOnces,
|
||||
logDebug,
|
||||
@@ -60,11 +60,12 @@ const imagesCachePath = join(heroicFolder, 'images-cache')
|
||||
const { currentLogFile: currentLogFile, lastLogFile: lastLogFile } =
|
||||
createNewLogFileAndClearOldOnces()
|
||||
|
||||
const icon = fixAsarPath(join(__dirname, 'icon.png'))
|
||||
const iconDark = fixAsarPath(join(__dirname, 'icon-dark.png'))
|
||||
const iconLight = fixAsarPath(join(__dirname, 'icon-light.png'))
|
||||
const publicDir = resolve(__dirname, '..', app.isPackaged ? '' : '../public')
|
||||
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 libraryPath = join(legendaryConfigPath, 'metadata')
|
||||
const legendaryMetadata = join(legendaryConfigPath, 'metadata')
|
||||
const fallBackImage = 'fallback'
|
||||
const epicLoginUrl =
|
||||
'https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect'
|
||||
@@ -119,7 +120,7 @@ function fixAsarPath(origin: string): string {
|
||||
export function getSteamCompatFolder() {
|
||||
// Paths are from https://savelocation.net/steam-game-folder
|
||||
if (isWindows) {
|
||||
const defaultWinPath = join(process.env['PROGRAMFILES(X86)'], 'Steam')
|
||||
const defaultWinPath = join(process.env['PROGRAMFILES(X86)'] ?? '', 'Steam')
|
||||
return defaultWinPath
|
||||
} else if (isMac) {
|
||||
return join(userHome, 'Library/Application Support/Steam')
|
||||
@@ -208,7 +209,7 @@ export {
|
||||
isWindows,
|
||||
isLinux,
|
||||
legendaryConfigPath,
|
||||
libraryPath,
|
||||
legendaryMetadata,
|
||||
epicLoginUrl,
|
||||
gogLoginUrl,
|
||||
patreonPage,
|
||||
@@ -224,5 +225,6 @@ export {
|
||||
runtimePath,
|
||||
isCLIFullscreen,
|
||||
isCLINoGui,
|
||||
publicDir,
|
||||
GITHUB_API
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from 'graceful-fs'
|
||||
|
||||
import { GameConfigVersion, GameSettings } from './types'
|
||||
import { GameConfigVersion, GameSettings } from 'common/types'
|
||||
import { GlobalConfig } from './config'
|
||||
import {
|
||||
currentGameConfigVersion,
|
||||
@@ -26,6 +26,7 @@ abstract class GameConfig {
|
||||
|
||||
public abstract version: GameConfigVersion
|
||||
|
||||
// @ts-expect-error TODO: Somehow figure out how to synchronously load the config
|
||||
public config: GameSettings
|
||||
|
||||
readonly appName: string
|
||||
@@ -72,7 +73,7 @@ abstract class GameConfig {
|
||||
GameConfig.reload(appName, version)
|
||||
}
|
||||
|
||||
return GameConfig.instances.get(appName)
|
||||
return GameConfig.instances.get(appName)!
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +100,7 @@ abstract class GameConfig {
|
||||
break
|
||||
}
|
||||
// Try to upgrade outdated config.
|
||||
if (GameConfig.instances.get(appName).upgrade()) {
|
||||
if (GameConfig.instances.get(appName)!.upgrade()) {
|
||||
// Upgrade done, we need to fully reload config.
|
||||
logInfo(
|
||||
`[${appName}]: Upgraded outdated ${version} config to ${currentGameConfigVersion}.`,
|
||||
@@ -1,25 +1,18 @@
|
||||
import { GOGCloudSavesLocation, GogInstallInfo } from 'common/types/gog'
|
||||
import { LegendaryInstallInfo } from 'common/types/legendary'
|
||||
import {
|
||||
ExecResult,
|
||||
ExtraInfo,
|
||||
GameInfo,
|
||||
GameSettings,
|
||||
GOGCloudSavesLocation,
|
||||
InstallArgs,
|
||||
InstallInfo,
|
||||
Runner
|
||||
} from './types'
|
||||
InstallArgs
|
||||
} from 'common/types'
|
||||
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { heroicGamesConfigPath } from './constants'
|
||||
|
||||
abstract class Game {
|
||||
public static get(
|
||||
appName: string,
|
||||
runner: Runner = 'legendary'
|
||||
): LegendaryGame | GOGGame {
|
||||
if (runner === 'legendary') {
|
||||
return LegendaryGame.get(appName)
|
||||
} else if (runner === 'gog') {
|
||||
return GOGGame.get(appName)
|
||||
}
|
||||
}
|
||||
public get logFileLocation() {
|
||||
return join(heroicGamesConfigPath, `${this.appName}-lastPlay.log`)
|
||||
}
|
||||
@@ -28,7 +21,9 @@ abstract class Game {
|
||||
abstract window: BrowserWindow
|
||||
abstract getExtraInfo(): Promise<ExtraInfo>
|
||||
abstract getGameInfo(installPlatform?: string): GameInfo
|
||||
abstract getInstallInfo(installPlatform?: string): Promise<InstallInfo>
|
||||
abstract getInstallInfo(
|
||||
installPlatform?: string
|
||||
): Promise<LegendaryInstallInfo | GogInstallInfo>
|
||||
abstract getSettings(): Promise<GameSettings>
|
||||
abstract hasUpdate(): Promise<boolean>
|
||||
abstract import(path: string): Promise<ExecResult>
|
||||
@@ -51,10 +46,4 @@ abstract class Game {
|
||||
abstract runWineCommand(command: string, wait?: boolean): Promise<ExecResult>
|
||||
}
|
||||
|
||||
import { LegendaryGame } from './legendary/games'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { GOGGame } from './gog/games'
|
||||
import { join } from 'path'
|
||||
import { heroicGamesConfigPath } from './constants'
|
||||
|
||||
export { Game, Runner }
|
||||
export { Game }
|
||||
File renamed without changes.
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { GOGLibrary } from './library'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { BrowserWindow, dialog } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { Game } from '../games'
|
||||
import { GameConfig } from '../game_config'
|
||||
@@ -9,15 +8,11 @@ import { killPattern } from '../utils'
|
||||
import {
|
||||
ExtraInfo,
|
||||
GameInfo,
|
||||
InstallInfo,
|
||||
GameSettings,
|
||||
ExecResult,
|
||||
InstallArgs,
|
||||
GOGLoginData,
|
||||
InstalledInfo,
|
||||
SteamRuntime,
|
||||
GOGCloudSavesLocation
|
||||
} from 'types'
|
||||
InstalledInfo
|
||||
} from 'common/types'
|
||||
import { appendFileSync, existsSync, rmSync } from 'graceful-fs'
|
||||
import {
|
||||
heroicGamesConfigPath,
|
||||
@@ -26,19 +21,9 @@ import {
|
||||
isMac,
|
||||
isLinux
|
||||
} from '../constants'
|
||||
import {
|
||||
configStore,
|
||||
installedGamesStore,
|
||||
syncStore
|
||||
} from '../gog/electronStores'
|
||||
import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger'
|
||||
import {
|
||||
errorHandler,
|
||||
execAsync,
|
||||
getFileSize,
|
||||
getGOGdlBin,
|
||||
getSteamRuntime
|
||||
} from '../utils'
|
||||
import { installedGamesStore, syncStore } from '../gog/electronStores'
|
||||
import { logError, logInfo, LogPrefix } from '../logger/logger'
|
||||
import { errorHandler, execAsync, getFileSize, getGOGdlBin } from '../utils'
|
||||
import { GOGUser } from './user'
|
||||
import {
|
||||
getRunnerCallWithoutCredentials,
|
||||
@@ -54,6 +39,12 @@ import setup from './setup'
|
||||
import { runGogdlCommand } from './library'
|
||||
import { removeNonSteamGame } from '../shortcuts/nonesteamgame/nonesteamgame'
|
||||
import shlex from 'shlex'
|
||||
import {
|
||||
GOGCloudSavesLocation,
|
||||
GogInstallInfo,
|
||||
GogInstallPlatform
|
||||
} from 'common/types/gog'
|
||||
import { t } from 'i18next'
|
||||
|
||||
class GOGGame extends Game {
|
||||
public appName: string
|
||||
@@ -67,11 +58,11 @@ class GOGGame extends Game {
|
||||
if (!this.instances.get(appName)) {
|
||||
this.instances.set(appName, new GOGGame(appName))
|
||||
}
|
||||
return this.instances.get(appName)
|
||||
return this.instances.get(appName) as GOGGame
|
||||
}
|
||||
public async getExtraInfo(): Promise<ExtraInfo> {
|
||||
const gameInfo = GOGLibrary.get().getGameInfo(this.appName)
|
||||
let targetPlatform: 'windows' | 'osx' | 'linux' = 'windows'
|
||||
const gameInfo = this.getGameInfo()
|
||||
let targetPlatform: GogInstallPlatform = 'windows'
|
||||
|
||||
if (isMac && gameInfo.is_mac_native) {
|
||||
targetPlatform = 'osx'
|
||||
@@ -88,10 +79,41 @@ class GOGGame extends Game {
|
||||
return extra
|
||||
}
|
||||
public getGameInfo(): GameInfo {
|
||||
return GOGLibrary.get().getGameInfo(this.appName)
|
||||
const info = GOGLibrary.get().getGameInfo(this.appName)
|
||||
if (!info) {
|
||||
logError(
|
||||
[
|
||||
'Could not get game info for',
|
||||
`${this.appName},`,
|
||||
'returning empty object. Something is probably gonna go wrong soon'
|
||||
],
|
||||
LogPrefix.Gog
|
||||
)
|
||||
// @ts-expect-error TODO: Handle this better
|
||||
return {}
|
||||
}
|
||||
return info
|
||||
}
|
||||
async getInstallInfo(installPlatform?: string): Promise<InstallInfo> {
|
||||
return GOGLibrary.get().getInstallInfo(this.appName, installPlatform)
|
||||
async getInstallInfo(
|
||||
installPlatform: GogInstallPlatform = 'windows'
|
||||
): Promise<GogInstallInfo> {
|
||||
const info = await GOGLibrary.get().getInstallInfo(
|
||||
this.appName,
|
||||
installPlatform
|
||||
)
|
||||
if (!info) {
|
||||
logError(
|
||||
[
|
||||
'Could not get install info for',
|
||||
`${this.appName},`,
|
||||
'returning empty object. Something is probably gonna go wrong soon'
|
||||
],
|
||||
LogPrefix.Gog
|
||||
)
|
||||
// @ts-expect-error TODO: Handle this better
|
||||
return {}
|
||||
}
|
||||
return info
|
||||
}
|
||||
async getSettings(): Promise<GameSettings> {
|
||||
return (
|
||||
@@ -113,16 +135,21 @@ class GOGGame extends Game {
|
||||
['Failed to import', `${this.appName}:`, res.error],
|
||||
LogPrefix.Gog
|
||||
)
|
||||
return res
|
||||
}
|
||||
try {
|
||||
await GOGLibrary.get().importGame(JSON.parse(res.stdout), path)
|
||||
return res
|
||||
} catch (error) {
|
||||
logError(
|
||||
['Failed to import', `${this.appName}:`, res.error],
|
||||
[
|
||||
'Failed to import',
|
||||
`${this.appName}:`,
|
||||
error instanceof Error ? error.stack ?? error.message : `${error}`
|
||||
],
|
||||
LogPrefix.Gog
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
public onInstallOrUpdateOutput(
|
||||
@@ -171,11 +198,17 @@ class GOGGame extends Game {
|
||||
|
||||
const credentials = await GOGUser.getCredentials()
|
||||
|
||||
let installPlatform = platformToInstall.toLowerCase()
|
||||
if (installPlatform === 'mac') {
|
||||
installPlatform = 'osx'
|
||||
if (!credentials) {
|
||||
logError(
|
||||
['Failed to install', `${this.appName}:`, 'No credentials'],
|
||||
LogPrefix.Gog
|
||||
)
|
||||
return { status: 'error' }
|
||||
}
|
||||
|
||||
const installPlatform =
|
||||
platformToInstall.toLowerCase() as GogInstallPlatform
|
||||
|
||||
const logPath = join(heroicGamesConfigPath, this.appName + '.log')
|
||||
|
||||
const commandParts: string[] = [
|
||||
@@ -212,7 +245,7 @@ class GOGGame extends Game {
|
||||
// Installation succeded
|
||||
// Save new game info to installed games store
|
||||
const installInfo = await this.getInstallInfo(installPlatform)
|
||||
const gameInfo = GOGLibrary.get().getGameInfo(this.appName)
|
||||
const gameInfo = this.getGameInfo()
|
||||
const isLinuxNative = installPlatform === 'linux'
|
||||
const additionalInfo = isLinuxNative
|
||||
? await GOGLibrary.getLinuxInstallerInfo(this.appName)
|
||||
@@ -274,10 +307,16 @@ class GOGGame extends Game {
|
||||
}
|
||||
|
||||
async launch(launchArguments?: string): Promise<boolean> {
|
||||
const gameSettings =
|
||||
GameConfig.get(this.appName).config ||
|
||||
(await GameConfig.get(this.appName).getSettings())
|
||||
const gameInfo = GOGLibrary.get().getGameInfo(this.appName)
|
||||
const gameSettings = await this.getSettings()
|
||||
const gameInfo = this.getGameInfo()
|
||||
|
||||
if (
|
||||
!gameInfo.install ||
|
||||
!gameInfo.install.install_path ||
|
||||
!gameInfo.install.platform
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!existsSync(gameInfo.install.install_path)) {
|
||||
errorHandler({
|
||||
@@ -285,6 +324,7 @@ class GOGGame extends Game {
|
||||
runner: 'gog',
|
||||
appName: gameInfo.app_name
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -300,6 +340,10 @@ class GOGGame extends Game {
|
||||
this.logFileLocation,
|
||||
`Launch aborted: ${launchPrepFailReason}`
|
||||
)
|
||||
dialog.showErrorBox(
|
||||
t('box.error.launchAborted', 'Launch aborted'),
|
||||
launchPrepFailReason!
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -307,38 +351,12 @@ class GOGGame extends Game {
|
||||
? ['--override-exe', gameSettings.targetExe]
|
||||
: []
|
||||
|
||||
const isNative = await this.isNative()
|
||||
let commandEnv = isWindows
|
||||
? process.env
|
||||
: { ...process.env, ...setupEnvVars(gameSettings) }
|
||||
const wineFlag: string[] = []
|
||||
|
||||
let commandParts = new Array<string>()
|
||||
let commandEnv = {}
|
||||
let wrappers = new Array<string>()
|
||||
|
||||
if (isNative) {
|
||||
if (!isWindows) {
|
||||
// These options can only be used on Mac/Linux
|
||||
commandEnv = {
|
||||
...commandEnv,
|
||||
...setupEnvVars(gameSettings)
|
||||
}
|
||||
wrappers = setupWrappers(
|
||||
gameSettings,
|
||||
mangoHudCommand,
|
||||
gameModeBin,
|
||||
steamRuntime
|
||||
)
|
||||
}
|
||||
|
||||
commandParts = [
|
||||
'launch',
|
||||
gameInfo.install.install_path,
|
||||
...exeOverrideFlag,
|
||||
gameInfo.app_name,
|
||||
'--platform',
|
||||
`${gameInfo.install.platform}`,
|
||||
...shlex.split(launchArguments ?? ''),
|
||||
...shlex.split(gameSettings.launcherArgs ?? '')
|
||||
]
|
||||
} else {
|
||||
if (!this.isNative()) {
|
||||
const {
|
||||
success: wineLaunchPrepSuccess,
|
||||
failureReason: wineLaunchPrepFailReason,
|
||||
@@ -349,58 +367,53 @@ class GOGGame extends Game {
|
||||
this.logFileLocation,
|
||||
`Launch aborted: ${wineLaunchPrepFailReason}`
|
||||
)
|
||||
dialog.showErrorBox(
|
||||
t('box.error.launchAborted', 'Launch aborted'),
|
||||
wineLaunchPrepFailReason!
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
commandEnv = {
|
||||
...commandEnv,
|
||||
...setupEnvVars(gameSettings),
|
||||
...wineEnvVars
|
||||
}
|
||||
wrappers = setupWrappers(
|
||||
gameSettings,
|
||||
mangoHudCommand,
|
||||
gameModeBin,
|
||||
steamRuntime
|
||||
|
||||
const { bin: wineExec, type: wineType } = gameSettings.wineVersion
|
||||
|
||||
// Fix for people with old config
|
||||
const wineBin =
|
||||
wineExec.startsWith("'") && wineExec.endsWith("'")
|
||||
? wineExec.replaceAll("'", '')
|
||||
: wineExec
|
||||
|
||||
wineFlag.push(
|
||||
...(wineType === 'proton'
|
||||
? ['--no-wine', '--wrapper', `'${wineBin}' run`]
|
||||
: ['--wine', wineBin])
|
||||
)
|
||||
|
||||
const { wineVersion, winePrefix, launcherArgs, useSteamRuntime } =
|
||||
gameSettings
|
||||
let wineFlag = ['--wine', wineVersion.bin]
|
||||
|
||||
// avoid breaking on old configs when path is not absolute
|
||||
let winePrefixFlag = isMac ? [] : ['--wine-prefix', winePrefix]
|
||||
if (wineVersion.type === 'proton') {
|
||||
let runtime = null as SteamRuntime
|
||||
if (useSteamRuntime) {
|
||||
await getSteamRuntime('soldier').then((path) => (runtime = path))
|
||||
}
|
||||
|
||||
if (runtime?.path) {
|
||||
const runWithRuntime = `${runtime.path} -- '${wineVersion.bin}' waitforexitandrun`
|
||||
wineFlag = ['--no-wine', '--wrapper', runWithRuntime]
|
||||
winePrefixFlag = []
|
||||
} else {
|
||||
logWarning('No Steam runtime found')
|
||||
wineFlag = ['--no-wine', '--wrapper', `'${wineVersion.bin}' run`]
|
||||
winePrefixFlag = []
|
||||
}
|
||||
}
|
||||
|
||||
commandParts = [
|
||||
'launch',
|
||||
gameInfo.install.install_path,
|
||||
...exeOverrideFlag,
|
||||
gameInfo.app_name,
|
||||
...wineFlag,
|
||||
...winePrefixFlag,
|
||||
'--os',
|
||||
gameInfo.install.platform.toLowerCase(),
|
||||
...shlex.split(launchArguments ?? ''),
|
||||
...shlex.split(launcherArgs ?? '')
|
||||
]
|
||||
}
|
||||
|
||||
const commandParts = [
|
||||
'launch',
|
||||
gameInfo.install.install_path,
|
||||
...exeOverrideFlag,
|
||||
gameInfo.app_name,
|
||||
...wineFlag,
|
||||
'--platform',
|
||||
gameInfo.install.platform.toLowerCase(),
|
||||
...shlex.split(launchArguments ?? ''),
|
||||
...shlex.split(gameSettings.launcherArgs ?? '')
|
||||
]
|
||||
const wrappers = setupWrappers(
|
||||
gameSettings,
|
||||
mangoHudCommand,
|
||||
gameModeBin,
|
||||
steamRuntime?.length
|
||||
? [...steamRuntime, `--filesystem=${gameInfo.install.install_path}`]
|
||||
: undefined
|
||||
)
|
||||
|
||||
const fullCommand = getRunnerCallWithoutCredentials(
|
||||
commandParts,
|
||||
commandEnv,
|
||||
@@ -436,10 +449,14 @@ class GOGGame extends Game {
|
||||
title
|
||||
} = this.getGameInfo()
|
||||
|
||||
if (!install_path) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
newInstallPath += '\\' + install_path.split('\\').slice(-1)[0]
|
||||
newInstallPath += '\\' + install_path.split('\\').at(-1)
|
||||
} else {
|
||||
newInstallPath += '/' + install_path.split('/').slice(-1)[0]
|
||||
newInstallPath += '/' + install_path.split('/').at(-1)
|
||||
}
|
||||
|
||||
logInfo(`Moving ${title} to ${newInstallPath}`, LogPrefix.Gog)
|
||||
@@ -465,11 +482,15 @@ class GOGGame extends Game {
|
||||
workers
|
||||
} = await this.getCommandParameters()
|
||||
|
||||
if (!credentials) {
|
||||
return { stderr: 'Unable to repair game, no credentials', stdout: '' }
|
||||
}
|
||||
|
||||
const commandParts = [
|
||||
'repair',
|
||||
this.appName,
|
||||
'--platform',
|
||||
installPlatform,
|
||||
installPlatform!,
|
||||
`--path=${gameData.install.install_path}`,
|
||||
'--token',
|
||||
`"${credentials.access_token}"`,
|
||||
@@ -504,8 +525,23 @@ class GOGGame extends Game {
|
||||
path: string,
|
||||
gogSaves?: GOGCloudSavesLocation[]
|
||||
): Promise<ExecResult> {
|
||||
if (!gogSaves) {
|
||||
return {
|
||||
stderr: 'Unable to sync saves, gogSaves is undefined',
|
||||
stdout: ''
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await GOGUser.getCredentials()
|
||||
if (!credentials) {
|
||||
return { stderr: 'Unable to sync saves, no credentials', stdout: '' }
|
||||
}
|
||||
|
||||
const gameInfo = GOGLibrary.get().getGameInfo(this.appName)
|
||||
if (!gameInfo || !gameInfo.install.platform) {
|
||||
return { stderr: 'Unable to sync saves, game info not found', stdout: '' }
|
||||
}
|
||||
|
||||
const stderr: string[] = []
|
||||
|
||||
for (const location of gogSaves) {
|
||||
@@ -614,6 +650,10 @@ class GOGGame extends Game {
|
||||
logPath,
|
||||
workers
|
||||
} = await this.getCommandParameters()
|
||||
if (!installPlatform || !credentials) {
|
||||
return { status: 'error' }
|
||||
}
|
||||
|
||||
const commandParts = [
|
||||
'update',
|
||||
this.appName,
|
||||
@@ -661,13 +701,16 @@ class GOGGame extends Game {
|
||||
const gameObject = installedArray[gameIndex]
|
||||
|
||||
if (gameData.install.platform !== 'linux') {
|
||||
const installInfo = await GOGLibrary.get().getInstallInfo(this.appName)
|
||||
const installInfo = await this.getInstallInfo()
|
||||
gameObject.buildId = installInfo.game.buildId
|
||||
gameObject.version = installInfo.game.version
|
||||
gameObject.versionEtag = installInfo.manifest.versionEtag
|
||||
gameObject.install_size = getFileSize(installInfo.manifest.disk_size)
|
||||
} else {
|
||||
const installerInfo = await GOGLibrary.getLinuxInstallerInfo(this.appName)
|
||||
if (!installerInfo) {
|
||||
return { status: 'error' }
|
||||
}
|
||||
gameObject.version = installerInfo.version
|
||||
}
|
||||
installedGamesStore.set('installed', installedArray)
|
||||
@@ -682,7 +725,9 @@ class GOGGame extends Game {
|
||||
public async getCommandParameters() {
|
||||
const { maxWorkers } = await GlobalConfig.get().getSettings()
|
||||
const workers = maxWorkers ? ['--max-workers', `${maxWorkers}`] : []
|
||||
const gameData = GOGLibrary.get().getGameInfo(this.appName)
|
||||
const gameData = this.getGameInfo()
|
||||
const logPath = join(heroicGamesConfigPath, this.appName + '.log')
|
||||
const credentials = await GOGUser.getCredentials()
|
||||
|
||||
const withDlcs = gameData.install.installedWithDLCs
|
||||
? '--with-dlcs'
|
||||
@@ -690,10 +735,8 @@ class GOGGame extends Game {
|
||||
if (GOGUser.isTokenExpired()) {
|
||||
await GOGUser.refreshToken()
|
||||
}
|
||||
const credentials = configStore.get('credentials', {}) as GOGLoginData
|
||||
|
||||
const installPlatform = gameData.install.platform
|
||||
const logPath = join(heroicGamesConfigPath, this.appName + '.log')
|
||||
|
||||
return {
|
||||
withDlcs,
|
||||
@@ -725,7 +768,8 @@ class GOGGame extends Game {
|
||||
) as Array<InstalledInfo>
|
||||
const newInstalled = installed.filter((g) => g.appName !== this.appName)
|
||||
installedGamesStore.set('installed', newInstalled)
|
||||
const mainWindow = BrowserWindow.getFocusedWindow()
|
||||
const mainWindow =
|
||||
BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]
|
||||
mainWindow.webContents.send('refreshLibrary', 'gog')
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,12 @@ import { GOGUser } from './user'
|
||||
import {
|
||||
GOGGameInfo,
|
||||
GameInfo,
|
||||
InstallInfo,
|
||||
InstalledInfo,
|
||||
GOGImportData,
|
||||
ExecResult,
|
||||
CallRunnerOptions,
|
||||
GOGCloudSavesLocation
|
||||
} from '../types'
|
||||
CallRunnerOptions
|
||||
} from 'common/types'
|
||||
import { GOGCloudSavesLocation, GogInstallInfo } from 'common/types/gog'
|
||||
import { join } from 'node:path'
|
||||
import { existsSync, readFileSync } from 'graceful-fs'
|
||||
|
||||
@@ -24,9 +23,10 @@ import {
|
||||
import { callRunner } from '../launcher'
|
||||
|
||||
export class GOGLibrary {
|
||||
private static globalInstance: GOGLibrary = null
|
||||
private library: Map<string, null | GameInfo> = new Map()
|
||||
private installedGames: Map<string, null | InstalledInfo> = new Map()
|
||||
private static globalInstance: GOGLibrary
|
||||
|
||||
private library: Map<string, GameInfo> = new Map()
|
||||
private installedGames: Map<string, InstalledInfo> = new Map()
|
||||
|
||||
private constructor() {
|
||||
this.refreshInstalled()
|
||||
@@ -34,7 +34,7 @@ export class GOGLibrary {
|
||||
public async getSaveSyncLocation(
|
||||
appName: string,
|
||||
install: InstalledInfo
|
||||
): Promise<GOGCloudSavesLocation[] | null> {
|
||||
): Promise<GOGCloudSavesLocation[] | undefined> {
|
||||
let syncPlatform = 'Windows'
|
||||
const platform = install.platform
|
||||
switch (platform) {
|
||||
@@ -51,7 +51,7 @@ export class GOGLibrary {
|
||||
logWarning(
|
||||
`No clientId in goggame-${appName}.info file. Cannot resolve save path`
|
||||
)
|
||||
return null
|
||||
return
|
||||
}
|
||||
const response = await axios
|
||||
.get(
|
||||
@@ -65,12 +65,12 @@ export class GOGLibrary {
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
return null
|
||||
return
|
||||
}
|
||||
const platformInfo = response.data.content[syncPlatform]
|
||||
const savesInfo = platformInfo.cloudStorage
|
||||
if (!savesInfo.enabled) {
|
||||
return null
|
||||
return
|
||||
}
|
||||
|
||||
const locations = savesInfo.locations
|
||||
@@ -82,8 +82,14 @@ export class GOGLibrary {
|
||||
* @param features
|
||||
* @returns
|
||||
*/
|
||||
private async getGamesWithFeatures(features: string[] = ['512']) {
|
||||
private async getGamesWithFeatures(
|
||||
features: string[] = ['512']
|
||||
): Promise<string[]> {
|
||||
const credentials = await GOGUser.getCredentials()
|
||||
if (!credentials) {
|
||||
return []
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Authorization: 'Bearer ' + credentials.access_token,
|
||||
'User-Agent': 'GOGGalaxyClient/2.0.45.61 (GOG Galaxy)'
|
||||
@@ -103,9 +109,12 @@ export class GOGLibrary {
|
||||
],
|
||||
LogPrefix.Gog
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!games) {
|
||||
return []
|
||||
}
|
||||
|
||||
gameArray.push(...games.data.products)
|
||||
const numberOfPages = games?.data.totalPages
|
||||
for (let page = 2; page <= numberOfPages; page++) {
|
||||
@@ -132,7 +141,7 @@ export class GOGLibrary {
|
||||
for (const game of libraryStore.get('games', []) as GameInfo[]) {
|
||||
const copyObject = { ...game }
|
||||
if (this.installedGames.has(game.app_name)) {
|
||||
copyObject.install = this.installedGames.get(game.app_name)
|
||||
copyObject.install = this.installedGames.get(game.app_name)!
|
||||
copyObject.is_installed = true
|
||||
}
|
||||
this.library.set(game.app_name, copyObject)
|
||||
@@ -252,17 +261,17 @@ export class GOGLibrary {
|
||||
}
|
||||
|
||||
public static get() {
|
||||
if (this.globalInstance === null) {
|
||||
if (!this.globalInstance) {
|
||||
GOGLibrary.globalInstance = new GOGLibrary()
|
||||
}
|
||||
return this.globalInstance
|
||||
}
|
||||
|
||||
public getGameInfo(slug: string): GameInfo {
|
||||
return this.library.get(slug) || this.getInstallAndGameInfo(slug) || null
|
||||
public getGameInfo(slug: string): GameInfo | undefined {
|
||||
return this.library.get(slug) || this.getInstallAndGameInfo(slug)
|
||||
}
|
||||
|
||||
public getInstallAndGameInfo(slug: string): GameInfo {
|
||||
public getInstallAndGameInfo(slug: string): GameInfo | undefined {
|
||||
const lib = libraryStore.get('games')
|
||||
|
||||
if (!Array.isArray(lib)) {
|
||||
@@ -271,7 +280,9 @@ export class GOGLibrary {
|
||||
|
||||
const game: GameInfo = lib.find((value) => value.app_name === slug)
|
||||
|
||||
if (!game) return null
|
||||
if (!game) {
|
||||
return
|
||||
}
|
||||
const installedInfo = this.installedGames.get(game.app_name)
|
||||
if (installedInfo) {
|
||||
game.is_installed = true
|
||||
@@ -288,7 +299,10 @@ export class GOGLibrary {
|
||||
* @param appName
|
||||
* @returns InstallInfo object
|
||||
*/
|
||||
public async getInstallInfo(appName: string, installPlatform = 'windows') {
|
||||
public async getInstallInfo(
|
||||
appName: string,
|
||||
installPlatform = 'windows'
|
||||
): Promise<GogInstallInfo | undefined> {
|
||||
const credentials = await GOGUser.getCredentials()
|
||||
if (!credentials) {
|
||||
logError('No credentials, cannot get install info')
|
||||
@@ -296,6 +310,10 @@ export class GOGLibrary {
|
||||
}
|
||||
const gameData = this.library.get(appName)
|
||||
|
||||
if (!gameData) {
|
||||
return
|
||||
}
|
||||
|
||||
installPlatform = installPlatform.toLowerCase()
|
||||
|
||||
switch (installPlatform) {
|
||||
@@ -317,9 +335,9 @@ export class GOGLibrary {
|
||||
installPlatform
|
||||
]
|
||||
|
||||
logInfo('Getting game metadata.', LogPrefix.Gog)
|
||||
|
||||
const res = await runGogdlCommand(commandParts)
|
||||
const res = await runGogdlCommand(commandParts, {
|
||||
logMessagePrefix: 'Getting game metadata'
|
||||
})
|
||||
if (res.error) {
|
||||
logError(
|
||||
['Failed to get game metadata for', `${appName}:`, res.error],
|
||||
@@ -339,7 +357,7 @@ export class GOGLibrary {
|
||||
) {
|
||||
gameData.gog_save_location = await this.getSaveSyncLocation(
|
||||
appName,
|
||||
this.installedGames.get(appName)
|
||||
this.installedGames.get(appName)!
|
||||
)
|
||||
}
|
||||
libraryArray[gameObjectIndex].folder_name = gogInfo.folder_name
|
||||
@@ -347,23 +365,19 @@ export class GOGLibrary {
|
||||
gameData.folder_name = gogInfo.folder_name
|
||||
libraryStore.set('games', libraryArray)
|
||||
this.library.set(appName, gameData)
|
||||
const info: InstallInfo = {
|
||||
const info: GogInstallInfo = {
|
||||
game: {
|
||||
app_name: appName,
|
||||
title: gameData.title,
|
||||
owned_dlc: gogInfo.dlcs,
|
||||
version: gogInfo.versionName,
|
||||
launch_options: [],
|
||||
platform_versions: null,
|
||||
buildId: gogInfo.buildId
|
||||
},
|
||||
manifest: {
|
||||
disk_size: Number(gogInfo.disk_size),
|
||||
download_size: Number(gogInfo.download_size),
|
||||
app_name: appName,
|
||||
install_tags: [],
|
||||
launch_exe: '',
|
||||
prerequisites: null,
|
||||
languages: gogInfo.languages,
|
||||
versionEtag: gogInfo.versionEtag
|
||||
}
|
||||
@@ -379,6 +393,9 @@ export class GOGLibrary {
|
||||
(installedGamesStore.get('installed', []) as Array<InstalledInfo>) || []
|
||||
this.installedGames.clear()
|
||||
installedArray.forEach((value) => {
|
||||
if (!value.appName) {
|
||||
return
|
||||
}
|
||||
this.installedGames.set(value.appName, value)
|
||||
})
|
||||
}
|
||||
@@ -386,6 +403,14 @@ export class GOGLibrary {
|
||||
public changeGameInstallPath(appName: string, newInstallPath: string) {
|
||||
const cachedGameData = this.library.get(appName)
|
||||
|
||||
if (!cachedGameData) {
|
||||
logError(
|
||||
"Changing game install path failed: Game data couldn't be found",
|
||||
LogPrefix.Gog
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const installedArray =
|
||||
(installedGamesStore.get('installed', []) as Array<InstalledInfo>) || []
|
||||
|
||||
@@ -398,13 +423,17 @@ export class GOGLibrary {
|
||||
installedGamesStore.set('installed', installedArray)
|
||||
}
|
||||
public async importGame(data: GOGImportData, path: string) {
|
||||
const gameInfo = await this.getInstallInfo(data.appName)
|
||||
|
||||
if (!gameInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
const installInfo: InstalledInfo = {
|
||||
appName: data.appName,
|
||||
install_path: path,
|
||||
executable: '',
|
||||
install_size: getFileSize(
|
||||
(await this.getInstallInfo(data.appName)).manifest.disk_size
|
||||
),
|
||||
install_size: getFileSize(gameInfo.manifest.disk_size),
|
||||
is_dlc: false,
|
||||
version: data.versionName,
|
||||
platform: data.platform,
|
||||
@@ -412,7 +441,7 @@ export class GOGLibrary {
|
||||
installedWithDLCs: data.installedWithDlcs
|
||||
}
|
||||
this.installedGames.set(data.appName, installInfo)
|
||||
const gameData = this.library.get(data.appName)
|
||||
const gameData = this.library.get(data.appName)!
|
||||
gameData.install = installInfo
|
||||
gameData.is_installed = true
|
||||
this.library.set(data.appName, gameData)
|
||||
@@ -431,6 +460,9 @@ export class GOGLibrary {
|
||||
const installed = Array.from(this.installedGames.values())
|
||||
const updateable: Array<string> = []
|
||||
for (const game of installed) {
|
||||
if (!game.appName) {
|
||||
continue
|
||||
}
|
||||
// use different check for linux games
|
||||
if (game.platform === 'linux') {
|
||||
if (
|
||||
@@ -441,8 +473,8 @@ export class GOGLibrary {
|
||||
}
|
||||
const hasUpdate = await this.checkForGameUpdate(
|
||||
game.appName,
|
||||
game?.versionEtag,
|
||||
game.platform
|
||||
game.platform,
|
||||
game?.versionEtag
|
||||
)
|
||||
if (hasUpdate) {
|
||||
updateable.push(game.appName)
|
||||
@@ -465,12 +497,13 @@ export class GOGLibrary {
|
||||
return installer.version === version
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public async checkForGameUpdate(
|
||||
appName: string,
|
||||
etag: string,
|
||||
platform: string
|
||||
platform: string,
|
||||
etag?: string
|
||||
) {
|
||||
const buildData = await axios.get(
|
||||
`https://content-system.gog.com/products/${appName}/os/${platform}/builds?generation=2`
|
||||
@@ -480,7 +513,7 @@ export class GOGLibrary {
|
||||
? {
|
||||
'If-None-Match': etag
|
||||
}
|
||||
: null
|
||||
: undefined
|
||||
const metaResponse = await axios.get(metaUrl, {
|
||||
headers,
|
||||
validateStatus: (status) => status === 200 || status === 304
|
||||
@@ -499,10 +532,10 @@ export class GOGLibrary {
|
||||
gamesdbData: any,
|
||||
cloudSavesEnabledGames: string[]
|
||||
): Promise<GameInfo> {
|
||||
let developer: string
|
||||
let developer = ''
|
||||
let verticalCover = fallBackImage
|
||||
let horizontalCover: string
|
||||
let description: string
|
||||
let description = ''
|
||||
if (gamesdbData?.game) {
|
||||
const developers: Array<string> = []
|
||||
for (const developer of gamesdbData.game.developers) {
|
||||
@@ -537,31 +570,20 @@ export class GOGLibrary {
|
||||
const object: GameInfo = {
|
||||
runner: 'gog',
|
||||
store_url: `https://gog.com${info.url}`,
|
||||
developer: developer || '',
|
||||
developer: developer,
|
||||
app_name: String(info.id),
|
||||
art_logo: null,
|
||||
art_cover: horizontalCover,
|
||||
art_square: verticalCover,
|
||||
cloud_save_enabled: cloudSavesEnabledGames.includes(String(info.id)),
|
||||
compatible_apps: [],
|
||||
extra: {
|
||||
about: { description: description, shortDescription: '' },
|
||||
about: { description: description, longDescription: '' },
|
||||
reqs: []
|
||||
},
|
||||
folder_name: '',
|
||||
install: {
|
||||
version: null,
|
||||
executable: '',
|
||||
install_path: '',
|
||||
install_size: '',
|
||||
is_dlc: false,
|
||||
platform: ''
|
||||
is_dlc: false
|
||||
},
|
||||
is_game: true,
|
||||
is_installed: false,
|
||||
is_ue_asset: false,
|
||||
is_ue_plugin: false,
|
||||
is_ue_project: false,
|
||||
namespace: info.slug,
|
||||
save_folder: '',
|
||||
title: info.title,
|
||||
@@ -649,20 +671,18 @@ export class GOGLibrary {
|
||||
if (!gameInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
installPath = installPath ?? gameInfo?.install.install_path
|
||||
if (!installPath) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const infoFileName = `goggame-${appName}.info`
|
||||
let infoFilePath = join(
|
||||
installPath ? installPath : gameInfo.install.install_path,
|
||||
infoFileName
|
||||
)
|
||||
let infoFilePath = join(installPath, infoFileName)
|
||||
|
||||
if (gameInfo.install.platform === 'osx') {
|
||||
// Since mac games can only be installed on mac we don't need to check for current platfrom
|
||||
infoFilePath = join(
|
||||
installPath ? installPath : gameInfo.install.install_path,
|
||||
'Contents',
|
||||
'Resources',
|
||||
infoFileName
|
||||
)
|
||||
infoFilePath = join(installPath, 'Contents', 'Resources', infoFileName)
|
||||
}
|
||||
|
||||
if (existsSync(infoFilePath)) {
|
||||
@@ -710,15 +730,15 @@ export class GOGLibrary {
|
||||
etag?: string
|
||||
) {
|
||||
const url = `https://gamesdb.gog.com/platforms/${store}/external_releases/${game_id}`
|
||||
const headers = {
|
||||
'If-None-Match': etag
|
||||
}
|
||||
const headers = etag
|
||||
? {
|
||||
'If-None-Match': etag
|
||||
}
|
||||
: undefined
|
||||
|
||||
const response = await axios
|
||||
.get(url, { headers: etag ? headers : {} })
|
||||
.catch(() => {
|
||||
return null
|
||||
})
|
||||
const response = await axios.get(url, { headers: headers }).catch(() => {
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
return { isUpdated: false, data: {} }
|
||||
}
|
||||
@@ -737,18 +757,13 @@ export class GOGLibrary {
|
||||
* Handler of https://api.gog.com/products/ endpoint
|
||||
* @param appName id of game
|
||||
* @param expand expanded results to be returned
|
||||
* @returns raw axios response null when there was a error
|
||||
* @returns raw axios response, or null if there was a error
|
||||
*/
|
||||
public static async getProductApi(appName: string, expand?: string[]) {
|
||||
const isExpanded = expand?.length > 0
|
||||
let expandString = '?expand='
|
||||
if (isExpanded) {
|
||||
expandString += expand.join(',')
|
||||
}
|
||||
const url = `https://api.gog.com/products/${appName}${
|
||||
isExpanded ? expandString : ''
|
||||
}`
|
||||
const response: AxiosResponse = await axios.get(url).catch(() => null)
|
||||
expand = expand ?? []
|
||||
const expandString = expand.length ? '?expand=' + expand.join(',') : ''
|
||||
const url = `https://api.gog.com/products/${appName}${expandString}`
|
||||
const response = await axios.get(url).catch(() => null)
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -782,23 +797,25 @@ export class GOGLibrary {
|
||||
* @param appName
|
||||
* @returns
|
||||
*/
|
||||
public static async getLinuxInstallerInfo(appName: string): Promise<{
|
||||
version: string
|
||||
} | null> {
|
||||
const response = await GOGLibrary.getProductApi(appName, ['downloads'])
|
||||
if (response) {
|
||||
const installers = response.data?.downloads?.installers
|
||||
|
||||
for (const installer of installers) {
|
||||
if (installer.os === 'linux')
|
||||
return {
|
||||
version: installer.version
|
||||
}
|
||||
public static async getLinuxInstallerInfo(appName: string): Promise<
|
||||
| {
|
||||
version: string
|
||||
}
|
||||
} else {
|
||||
logError("Couldn't get installer info")
|
||||
return null
|
||||
| undefined
|
||||
> {
|
||||
const response = await GOGLibrary.getProductApi(appName, ['downloads'])
|
||||
if (!response) {
|
||||
return
|
||||
}
|
||||
const installers = response.data?.downloads?.installers
|
||||
|
||||
for (const installer of installers) {
|
||||
if (installer.os === 'linux')
|
||||
return {
|
||||
version: installer.version
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { copySync } from 'fs-extra'
|
||||
import path from 'node:path'
|
||||
import { GOGLibrary } from './library'
|
||||
import { GameInfo, InstalledInfo } from '../types'
|
||||
import { GameInfo, InstalledInfo } from 'common/types'
|
||||
import { execAsync, isOnline, quoteIfNecessary } from '../utils'
|
||||
import { GameConfig } from '../game_config'
|
||||
import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger'
|
||||
@@ -72,12 +72,12 @@ async function setup(
|
||||
).catch()
|
||||
}
|
||||
// Funny part begins here
|
||||
// Deterimine if it's basicly from .script file or from manifest
|
||||
// Deterimine if it's basically from .script file or from manifest
|
||||
if (instructions[0]?.install) {
|
||||
// It's from .script file
|
||||
// Parse actions
|
||||
const supportDir = path.join(
|
||||
gameInfo.install.install_path,
|
||||
gameInfo.install.install_path!,
|
||||
'support',
|
||||
appName
|
||||
)
|
||||
@@ -138,7 +138,7 @@ async function setup(
|
||||
case 'Execute': {
|
||||
const executableName = actionArguments.executable
|
||||
const infoPath = path.join(
|
||||
gameInfo.install.install_path,
|
||||
gameInfo.install.install_path!,
|
||||
`goggame-${appName}.info`
|
||||
)
|
||||
let Language = 'english'
|
||||
@@ -313,7 +313,7 @@ async function setup(
|
||||
async function obtainSetupInstructions(gameInfo: GameInfo) {
|
||||
const { buildId, appName, install_path } = gameInfo.install
|
||||
|
||||
const scriptPath = path.join(install_path, `goggame-${appName}.script`)
|
||||
const scriptPath = path.join(install_path!, `goggame-${appName}.script`)
|
||||
if (existsSync(scriptPath)) {
|
||||
const data = readFileSync(scriptPath, { encoding: 'utf-8' })
|
||||
return JSON.parse(data).actions
|
||||
@@ -350,7 +350,7 @@ const registryDataTypes = new Map([
|
||||
['binary', 'REG_BINARY']
|
||||
// If needed please add those values REG_NONE REG_EXPAND_SZ REG_MULTI_SZ
|
||||
])
|
||||
const getRegDataType = (dataType: string): string =>
|
||||
const getRegDataType = (dataType: string): string | undefined =>
|
||||
registryDataTypes.get(dataType.toLowerCase())
|
||||
|
||||
/**
|
||||
@@ -372,7 +372,10 @@ const handlePathVars = (
|
||||
for (const value of variables) {
|
||||
const trimmedValue = value.slice(1, -1)
|
||||
|
||||
return path.replace(value, possibleValues.get(trimmedValue.toLowerCase()))
|
||||
return path.replace(
|
||||
value,
|
||||
possibleValues.get(trimmedValue.toLowerCase()) ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
return ''
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger'
|
||||
import { GOGLoginData } from '../types'
|
||||
import { GOGLoginData } from 'common/types'
|
||||
import { configStore, libraryStore } from '../gog/electronStores'
|
||||
import { errorHandler, isOnline } from '../utils'
|
||||
|
||||
@@ -45,6 +45,10 @@ export class GOGUser {
|
||||
return
|
||||
}
|
||||
const user = await this.getCredentials()
|
||||
if (!user) {
|
||||
logError("No credentials, can't get user data", LogPrefix.Gog)
|
||||
return
|
||||
}
|
||||
const response = await axios
|
||||
.get(`https://embed.gog.com/userData.json`, {
|
||||
headers: {
|
||||
@@ -54,9 +58,12 @@ export class GOGUser {
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(['Error getting user Data', `${error}`], LogPrefix.Gog)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = response.data
|
||||
|
||||
//Exclude email, it won't be needed
|
||||
@@ -83,7 +90,7 @@ export class GOGUser {
|
||||
/**
|
||||
* Refreshes token and returns new credentials
|
||||
*/
|
||||
public static async refreshToken(): Promise<GOGLoginData | null> {
|
||||
public static async refreshToken(): Promise<GOGLoginData | undefined> {
|
||||
const user: GOGLoginData = configStore.get(
|
||||
'credentials',
|
||||
{}
|
||||
@@ -97,7 +104,6 @@ export class GOGUser {
|
||||
'Error with refreshing token, reauth required',
|
||||
LogPrefix.Gog
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
@@ -115,7 +121,7 @@ export class GOGUser {
|
||||
error: 'No credentials',
|
||||
runner: 'GOG'
|
||||
})
|
||||
return null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const getImageFromCache = (url: string) => {
|
||||
const digest = createHash('sha256').update(realUrl).digest('hex')
|
||||
const cachePath = join(imagesCachePath, digest)
|
||||
|
||||
if (!existsSync(cachePath)) {
|
||||
if (!existsSync(cachePath) && realUrl.startsWith('http')) {
|
||||
// if not found, download in the background
|
||||
axios
|
||||
.get(realUrl, { responseType: 'stream' })
|
||||
@@ -1,23 +1,17 @@
|
||||
module.exports = {
|
||||
displayName: 'Backend',
|
||||
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/electron/tsconfig.json'
|
||||
}
|
||||
},
|
||||
|
||||
moduleDirectories: ['node_modules', '<rootDir>'],
|
||||
// Module file extensions for importing
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
testPathIgnorePatterns: ['./node_modules/'],
|
||||
resetMocks: true,
|
||||
|
||||
rootDir: '..',
|
||||
rootDir: '../..',
|
||||
|
||||
// The root of your source code, typically /src
|
||||
// `<rootDir>` is a token Jest substitutes
|
||||
roots: ['<rootDir>/electron'],
|
||||
roots: ['<rootDir>/src/backend'],
|
||||
|
||||
// Test spec file resolution pattern
|
||||
// should contain `test` or `spec`.
|
||||
@@ -33,8 +33,8 @@ import { GlobalConfig } from './config'
|
||||
import { GameConfig } from './game_config'
|
||||
import { DXVK } from './tools'
|
||||
import setup from './gog/setup'
|
||||
import { GOGGame } from 'gog/games'
|
||||
import { LegendaryGame } from 'legendary/games'
|
||||
import { GOGGame } from './gog/games'
|
||||
import { LegendaryGame } from './legendary/games'
|
||||
import {
|
||||
CallRunnerOptions,
|
||||
GameInfo,
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
GameSettings,
|
||||
LaunchPreperationResult,
|
||||
RpcClient
|
||||
} from './types'
|
||||
} from 'common/types'
|
||||
import { spawn } from 'child_process'
|
||||
import shlex from 'shlex'
|
||||
import { Game } from './games'
|
||||
@@ -64,16 +64,6 @@ async function prepareLaunch(
|
||||
|
||||
// Check if the game needs an internet connection
|
||||
if (!gameInfo.canRunOffline && offlineMode) {
|
||||
showErrorBoxModalAuto(
|
||||
i18next.t(
|
||||
'box.error.no-offline-mode.title',
|
||||
'Offline mode not supported.'
|
||||
),
|
||||
i18next.t(
|
||||
'box.error.no-offline-mode.message',
|
||||
'Launch aborted! The game requires a internet connection to run it.'
|
||||
)
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
failureReason: 'Offline mode not supported'
|
||||
@@ -81,55 +71,62 @@ async function prepareLaunch(
|
||||
}
|
||||
|
||||
// Update Discord RPC if enabled
|
||||
let rpcClient = null
|
||||
let rpcClient = undefined
|
||||
if (globalSettings.discordRPC) {
|
||||
rpcClient = constructAndUpdateRPC(gameInfo.title)
|
||||
}
|
||||
|
||||
// If we're not on Linux, we can return here
|
||||
if (!isLinux) {
|
||||
return { success: true, rpcClient: rpcClient }
|
||||
return { success: true, rpcClient }
|
||||
}
|
||||
|
||||
// Figure out where MangoHud/GameMode are located, if they're enabled
|
||||
let mangoHudCommand = ''
|
||||
let mangoHudCommand: string[] = []
|
||||
let gameModeBin = ''
|
||||
if (gameSettings.showMangohud) {
|
||||
const mangoHudBin = await searchForExecutableOnPath('mangohud')
|
||||
if (!mangoHudBin) {
|
||||
logWarning('MangoHud enabled but not installed', LogPrefix.Backend)
|
||||
// Should we display an error box and return { success: false } here?
|
||||
return {
|
||||
success: false,
|
||||
failureReason:
|
||||
'Mangohud is enabled, but `mangohud` executable could not be found on $PATH'
|
||||
}
|
||||
} else {
|
||||
mangoHudCommand = `${mangoHudBin} --dlsym`
|
||||
mangoHudCommand = [mangoHudBin, '--dlsym']
|
||||
}
|
||||
}
|
||||
if (gameSettings.useGameMode) {
|
||||
gameModeBin = await searchForExecutableOnPath('gamemoderun')
|
||||
if (!gameModeBin) {
|
||||
logWarning('GameMode enabled but not installed', LogPrefix.Backend)
|
||||
return {
|
||||
success: false,
|
||||
failureReason:
|
||||
'GameMode is enabled, but `gamemoderun` executable could not be found on $PATH'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the Steam Runtime is enabled, find a valid one
|
||||
let steamRuntime = ''
|
||||
const isLinuxNative =
|
||||
gameInfo?.install?.platform &&
|
||||
gameInfo?.install?.platform.toLowerCase() === 'linux'
|
||||
|
||||
if (gameSettings.useSteamRuntime && isLinuxNative) {
|
||||
let steamRuntime: string[] = []
|
||||
const shouldUseRuntime =
|
||||
gameSettings.useSteamRuntime &&
|
||||
(game.isNative() || gameSettings.wineVersion.type === 'proton')
|
||||
if (shouldUseRuntime) {
|
||||
// for native games lets use scout for now
|
||||
await getSteamRuntime('scout').then((runtime) => {
|
||||
if (!runtime.path) {
|
||||
logWarning(
|
||||
`Couldn't find a valid Steam runtime path`,
|
||||
LogPrefix.Backend
|
||||
)
|
||||
} else {
|
||||
logInfo(`Using ${runtime.type} Steam runtime`, LogPrefix.Backend)
|
||||
steamRuntime =
|
||||
runtime.version === 'soldier' ? `${runtime.path} -- ` : runtime.path
|
||||
const runtimeType = game.isNative() ? 'scout' : 'soldier'
|
||||
const { path, args } = await getSteamRuntime(runtimeType)
|
||||
if (!path) {
|
||||
return {
|
||||
success: false,
|
||||
failureReason:
|
||||
'Steam Runtime is enabled, but no runtimes could be found\n' +
|
||||
`Make sure Steam ${
|
||||
game.isNative() ? 'is' : 'and the "SteamLinuxRuntime - Soldier" are'
|
||||
} installed`
|
||||
}
|
||||
})
|
||||
}
|
||||
steamRuntime = [path, ...args]
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -340,7 +337,9 @@ function setupWineEnvVars(gameSettings: GameSettings, gameId = '0') {
|
||||
wineVersion.lib32,
|
||||
wineVersion.lib,
|
||||
process.env.LD_LIBRARY_PATH
|
||||
].join(':')
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':')
|
||||
} else {
|
||||
logError(
|
||||
[
|
||||
@@ -356,26 +355,25 @@ function setupWineEnvVars(gameSettings: GameSettings, gameId = '0') {
|
||||
|
||||
function setupWrappers(
|
||||
gameSettings: GameSettings,
|
||||
mangoHudBin: string,
|
||||
gameModeBin: string,
|
||||
steamRuntime: string
|
||||
mangoHudCommand?: string[],
|
||||
gameModeBin?: string,
|
||||
steamRuntime?: string[]
|
||||
): Array<string> {
|
||||
const wrappers = Array<string>()
|
||||
const wrappers: string[] = []
|
||||
if (gameSettings.wrapperOptions) {
|
||||
gameSettings.wrapperOptions.forEach((wrapperEntry: WrapperVariable) => {
|
||||
wrappers.push(wrapperEntry.exe)
|
||||
wrappers.push(...shlex.split(wrapperEntry.args ?? ''))
|
||||
})
|
||||
}
|
||||
if (gameSettings.showMangohud) {
|
||||
// Mangohud needs some arguments in addition to the command, so we have to split here
|
||||
wrappers.push(...mangoHudBin.split(' '))
|
||||
if (mangoHudCommand) {
|
||||
wrappers.push(...mangoHudCommand)
|
||||
}
|
||||
if (gameSettings.useGameMode) {
|
||||
if (gameModeBin) {
|
||||
wrappers.push(gameModeBin)
|
||||
}
|
||||
if (gameSettings.useSteamRuntime) {
|
||||
wrappers.push(steamRuntime)
|
||||
if (steamRuntime) {
|
||||
wrappers.push(...steamRuntime)
|
||||
}
|
||||
return wrappers.filter((n) => n)
|
||||
}
|
||||
@@ -425,7 +423,7 @@ export async function verifyWinePrefix(
|
||||
})
|
||||
}
|
||||
|
||||
function launchCleanup(rpcClient: RpcClient) {
|
||||
function launchCleanup(rpcClient?: RpcClient) {
|
||||
if (rpcClient) {
|
||||
rpcClient.disconnect()
|
||||
logInfo('Stopped Discord Rich Presence', LogPrefix.Backend)
|
||||
@@ -526,7 +524,7 @@ async function callRunner(
|
||||
logDebug(`Logging to file "${options?.logFile}"`, runner.logPrefix)
|
||||
}
|
||||
|
||||
if (existsSync(options?.logFile)) {
|
||||
if (options?.logFile && existsSync(options.logFile)) {
|
||||
writeFileSync(options.logFile, '')
|
||||
}
|
||||
|
||||
@@ -535,13 +533,13 @@ async function callRunner(
|
||||
const wrappers = options?.wrappers || []
|
||||
let bin = ''
|
||||
if (wrappers.length) {
|
||||
bin = wrappers.shift()
|
||||
bin = wrappers.shift()!
|
||||
commandParts.unshift(...wrappers, runner.bin)
|
||||
} else {
|
||||
bin = runner.bin
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
return new Promise<ExecResult>((res, rej) => {
|
||||
const child = spawn(bin, commandParts, {
|
||||
cwd: runner.dir,
|
||||
env: { ...process.env, ...options?.env }
|
||||
@@ -632,18 +630,18 @@ async function callRunner(
|
||||
*/
|
||||
function getRunnerCallWithoutCredentials(
|
||||
commandParts: string[],
|
||||
env: Record<string, string> = {},
|
||||
env: Record<string, string> | NodeJS.ProcessEnv = {},
|
||||
wrappers: string[] = [],
|
||||
runnerPath: string
|
||||
): string {
|
||||
const commandArguments = Object.assign([], commandParts)
|
||||
const modifiedCommandParts = [...commandParts]
|
||||
// Redact sensitive arguments (SID for Legendary, token for GOGDL)
|
||||
for (const sensitiveArg of ['--sid', '--token']) {
|
||||
const sensitiveArgIndex = commandArguments.indexOf(sensitiveArg)
|
||||
const sensitiveArgIndex = modifiedCommandParts.indexOf(sensitiveArg)
|
||||
if (sensitiveArgIndex === -1) {
|
||||
continue
|
||||
}
|
||||
commandArguments[sensitiveArgIndex + 1] = '<redacted>'
|
||||
modifiedCommandParts[sensitiveArgIndex + 1] = '<redacted>'
|
||||
}
|
||||
|
||||
const formattedEnvVars: string[] = []
|
||||
@@ -654,7 +652,7 @@ function getRunnerCallWithoutCredentials(
|
||||
continue
|
||||
}
|
||||
}
|
||||
formattedEnvVars.push(`${key}=${quoteIfNecessary(value)}`)
|
||||
formattedEnvVars.push(`${key}=${quoteIfNecessary(value ?? '')}`)
|
||||
}
|
||||
|
||||
commandParts = commandParts.filter(Boolean)
|
||||
@@ -663,7 +661,7 @@ function getRunnerCallWithoutCredentials(
|
||||
...formattedEnvVars,
|
||||
...wrappers.map(quoteIfNecessary),
|
||||
quoteIfNecessary(runnerPath),
|
||||
...commandArguments.map(quoteIfNecessary)
|
||||
...modifiedCommandParts.map(quoteIfNecessary)
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
File renamed without changes.
+8
-12
@@ -8,9 +8,7 @@ import { heroicToolsPath, isLinux, legendaryConfigPath } from '../../constants'
|
||||
import { logError, LogPrefix, logWarning } from '../../logger/logger'
|
||||
import { runLegendaryCommand } from '../library'
|
||||
import { LegendaryGame } from '../games'
|
||||
import { killPattern } from '../../utils'
|
||||
import { Runner } from '../../types'
|
||||
import { Game } from '../../games'
|
||||
import { getGame, killPattern } from '../../utils'
|
||||
import { verifyWinePrefix } from '../../launcher'
|
||||
|
||||
const currentVersionPath = join(legendaryConfigPath, 'overlay_version.json')
|
||||
@@ -139,12 +137,11 @@ function cancelInstallOrUpdate() {
|
||||
}
|
||||
|
||||
async function enable(
|
||||
appName: string,
|
||||
runner: Runner
|
||||
appName: string
|
||||
): Promise<{ wasEnabled: boolean; installNow?: boolean }> {
|
||||
let prefix = ''
|
||||
if (isLinux) {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, 'legendary')
|
||||
await verifyWinePrefix(game)
|
||||
const { winePrefix, wineVersion } = await game.getSettings()
|
||||
prefix =
|
||||
@@ -169,10 +166,10 @@ async function enable(
|
||||
return { wasEnabled: true }
|
||||
}
|
||||
|
||||
async function disable(appName: string, runner: Runner) {
|
||||
async function disable(appName: string) {
|
||||
let prefix = ''
|
||||
if (isLinux) {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, 'legendary')
|
||||
const { winePrefix, wineVersion } = await game.getSettings()
|
||||
prefix =
|
||||
wineVersion.type === 'proton' ? join(winePrefix, 'pfx') : winePrefix
|
||||
@@ -191,15 +188,14 @@ function isInstalled() {
|
||||
/**
|
||||
* Checks if the EOS Overlay is enabled (either for a specific game on Linux or globally on Windows)
|
||||
* @param appName required on Linux, does nothing on Windows
|
||||
* @param runner required on Linux, does nothing on Windows
|
||||
* @returns Enabled = True; Disabled = False
|
||||
*/
|
||||
async function isEnabled(appName?: string, runner?: Runner) {
|
||||
async function isEnabled(appName?: string) {
|
||||
let enabled = false
|
||||
|
||||
let prefix = ''
|
||||
if (isLinux && appName && runner) {
|
||||
const game = Game.get(appName, runner)
|
||||
if (isLinux && appName) {
|
||||
const game = getGame(appName, 'legendary')
|
||||
const { winePrefix, wineVersion } = await game.getSettings()
|
||||
prefix =
|
||||
wineVersion.type === 'proton' ? join(winePrefix, 'pfx') : winePrefix
|
||||
+6
-6
@@ -17,12 +17,12 @@ ipcMain.handle('updateEosOverlayInfo', updateInfo)
|
||||
ipcMain.handle('installEosOverlay', install)
|
||||
ipcMain.handle('removeEosOverlay', remove)
|
||||
ipcMain.handle('cancelEosOverlayInstallOrUpdate', cancelInstallOrUpdate)
|
||||
ipcMain.handle('enableEosOverlay', async (e, appName, runner) => {
|
||||
return enable(appName, runner)
|
||||
ipcMain.handle('enableEosOverlay', async (e, appName) => {
|
||||
return enable(appName)
|
||||
})
|
||||
ipcMain.handle('disableEosOverlay', async (e, appName, runner) => {
|
||||
return disable(appName, runner)
|
||||
ipcMain.handle('disableEosOverlay', async (e, appName) => {
|
||||
return disable(appName)
|
||||
})
|
||||
ipcMain.handle('isEosOverlayEnabled', async (e, appName?, runner?) => {
|
||||
return isEnabled(appName, runner)
|
||||
ipcMain.handle('isEosOverlayEnabled', async (e, appName?) => {
|
||||
return isEnabled(appName)
|
||||
})
|
||||
@@ -1,30 +1,29 @@
|
||||
import { appendFileSync, existsSync, mkdirSync } from 'graceful-fs'
|
||||
import axios from 'axios'
|
||||
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { ExecResult, ExtraInfo, InstallArgs, SteamRuntime } from '../types'
|
||||
import { BrowserWindow, dialog } from 'electron'
|
||||
import {
|
||||
ExecResult,
|
||||
ExtraInfo,
|
||||
GameInfo,
|
||||
InstallArgs,
|
||||
InstallPlatform
|
||||
} from 'common/types'
|
||||
import { Game } from '../games'
|
||||
import { GameConfig } from '../game_config'
|
||||
import { GlobalConfig } from '../config'
|
||||
import { LegendaryLibrary } from './library'
|
||||
import { LegendaryUser } from './user'
|
||||
import {
|
||||
execAsync,
|
||||
getLegendaryBin,
|
||||
getSteamRuntime,
|
||||
isOnline,
|
||||
killPattern
|
||||
} from '../utils'
|
||||
import { execAsync, getLegendaryBin, isOnline, killPattern } from '../utils'
|
||||
import {
|
||||
heroicGamesConfigPath,
|
||||
userHome,
|
||||
isLinux,
|
||||
isMac,
|
||||
isWindows,
|
||||
installed,
|
||||
configStore
|
||||
} from '../constants'
|
||||
import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger'
|
||||
import { logError, logInfo, LogPrefix } from '../logger/logger'
|
||||
import {
|
||||
prepareLaunch,
|
||||
prepareWineLaunch,
|
||||
@@ -40,6 +39,7 @@ import { runLegendaryCommand } from './library'
|
||||
import { gameInfoStore } from './electronStores'
|
||||
import { removeNonSteamGame } from '../shortcuts/nonesteamgame/nonesteamgame'
|
||||
import shlex from 'shlex'
|
||||
import { t } from 'i18next'
|
||||
|
||||
class LegendaryGame extends Game {
|
||||
public appName: string
|
||||
@@ -55,14 +55,14 @@ class LegendaryGame extends Game {
|
||||
if (LegendaryGame.instances.get(appName) === undefined) {
|
||||
LegendaryGame.instances.set(appName, new LegendaryGame(appName))
|
||||
}
|
||||
return LegendaryGame.instances.get(appName)
|
||||
return LegendaryGame.instances.get(appName) as LegendaryGame
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for `LegendaryLibrary.listUpdateableGames`
|
||||
*/
|
||||
public static async checkGameUpdates() {
|
||||
const isLoggedIn = await LegendaryUser.isLoggedIn()
|
||||
const isLoggedIn = LegendaryUser.isLoggedIn()
|
||||
if (!isLoggedIn) {
|
||||
return []
|
||||
}
|
||||
@@ -74,8 +74,21 @@ class LegendaryGame extends Game {
|
||||
*
|
||||
* @returns GameInfo
|
||||
*/
|
||||
public getGameInfo() {
|
||||
return LegendaryLibrary.get().getGameInfo(this.appName)
|
||||
public getGameInfo(): GameInfo {
|
||||
const info = LegendaryLibrary.get().getGameInfo(this.appName)
|
||||
if (!info) {
|
||||
logError(
|
||||
[
|
||||
'Could not get game info for',
|
||||
`${this.appName},`,
|
||||
'returning empty object. Something is probably gonna go wrong soon'
|
||||
],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
// @ts-expect-error TODO: Handle this better
|
||||
return {}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +96,7 @@ class LegendaryGame extends Game {
|
||||
*
|
||||
* @returns InstallInfo
|
||||
*/
|
||||
public async getInstallInfo(installPlatform?: string) {
|
||||
public async getInstallInfo(installPlatform: InstallPlatform) {
|
||||
return LegendaryLibrary.get().getInstallInfo(this.appName, installPlatform)
|
||||
}
|
||||
|
||||
@@ -121,9 +134,12 @@ class LegendaryGame extends Game {
|
||||
}
|
||||
if (!isOnline()) {
|
||||
return {
|
||||
about: {},
|
||||
about: {
|
||||
description: '',
|
||||
longDescription: ''
|
||||
},
|
||||
reqs: []
|
||||
} as ExtraInfo
|
||||
}
|
||||
}
|
||||
let lang = GlobalConfig.get().config.language
|
||||
if (lang === 'pt') {
|
||||
@@ -164,15 +180,18 @@ class LegendaryGame extends Game {
|
||||
return {
|
||||
about: about.data.about,
|
||||
reqs: about.data.requirements.systems[0].details
|
||||
} as ExtraInfo
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Error Getting Info from Epic API', LogPrefix.Legendary)
|
||||
|
||||
gameInfoStore.set(namespace, { about: {}, reqs: [] })
|
||||
return {
|
||||
about: {},
|
||||
about: {
|
||||
description: '',
|
||||
longDescription: ''
|
||||
},
|
||||
reqs: []
|
||||
} as ExtraInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,14 +209,14 @@ class LegendaryGame extends Game {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for `checkGameUpdates().contains(this.appName)`
|
||||
* Helper for `listUpdateableGames().includes(this.appName)`
|
||||
*
|
||||
* @returns If game has an update.
|
||||
*/
|
||||
public async hasUpdate() {
|
||||
return (await LegendaryLibrary.get().listUpdateableGames()).includes(
|
||||
this.appName
|
||||
)
|
||||
const allUpdateableGames =
|
||||
await LegendaryLibrary.get().listUpdateableGames()
|
||||
return allUpdateableGames.includes(this.appName)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +227,7 @@ class LegendaryGame extends Game {
|
||||
* @returns The amended install path.
|
||||
*/
|
||||
public async moveInstall(newInstallPath: string) {
|
||||
const oldInstallPath = this.getGameInfo().install.install_path
|
||||
const oldInstallPath = this.getGameInfo().install.install_path!
|
||||
|
||||
newInstallPath = join(newInstallPath, basename(oldInstallPath))
|
||||
|
||||
@@ -220,8 +239,8 @@ class LegendaryGame extends Game {
|
||||
)
|
||||
|
||||
await execAsync(command)
|
||||
.then(() => {
|
||||
LegendaryLibrary.get().changeGameInstallPath(
|
||||
.then(async () => {
|
||||
await LegendaryLibrary.get().changeGameInstallPath(
|
||||
this.appName,
|
||||
newInstallPath
|
||||
)
|
||||
@@ -305,7 +324,8 @@ class LegendaryGame extends Game {
|
||||
})
|
||||
const { maxWorkers, downloadNoHttps } =
|
||||
await GlobalConfig.get().getSettings()
|
||||
const info = await Game.get(this.appName, 'legendary').getInstallInfo()
|
||||
const installPlatform = this.getGameInfo().install.platform!
|
||||
const info = await this.getInstallInfo(installPlatform)
|
||||
const workers = maxWorkers ? ['--max-workers', `${maxWorkers}`] : []
|
||||
const noHttps = downloadNoHttps ? ['--no-https'] : []
|
||||
const logPath = join(heroicGamesConfigPath, this.appName + '.log')
|
||||
@@ -382,9 +402,7 @@ class LegendaryGame extends Game {
|
||||
}: InstallArgs): Promise<{ status: 'done' | 'error' }> {
|
||||
const { maxWorkers, downloadNoHttps } =
|
||||
await GlobalConfig.get().getSettings()
|
||||
const info = await Game.get(this.appName, 'legendary').getInstallInfo(
|
||||
platformToInstall
|
||||
)
|
||||
const info = await this.getInstallInfo(platformToInstall)
|
||||
const workers = maxWorkers ? ['--max-workers', `${maxWorkers}`] : []
|
||||
const noHttps = downloadNoHttps ? ['--no-https'] : []
|
||||
const withDlcs = installDlcs ? '--with-dlcs' : '--skip-dlcs'
|
||||
@@ -448,7 +466,6 @@ class LegendaryGame extends Game {
|
||||
public async uninstall(): Promise<ExecResult> {
|
||||
const commandParts = ['uninstall', this.appName, '-y']
|
||||
|
||||
LegendaryLibrary.get().installState(this.appName, false)
|
||||
const res = await runLegendaryCommand(commandParts, {
|
||||
logMessagePrefix: `Uninstalling ${this.appName}`
|
||||
})
|
||||
@@ -459,8 +476,9 @@ class LegendaryGame extends Game {
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
} else {
|
||||
LegendaryLibrary.get().installState(this.appName, false)
|
||||
await removeShortcuts(this.appName, 'legendary')
|
||||
const gameInfo = await this.getGameInfo()
|
||||
const gameInfo = this.getGameInfo()
|
||||
const { defaultSteamPath } = await GlobalConfig.get().getSettings()
|
||||
const steamUserdataDir = join(
|
||||
defaultSteamPath.replaceAll("'", ''),
|
||||
@@ -569,6 +587,10 @@ class LegendaryGame extends Game {
|
||||
this.logFileLocation,
|
||||
`Launch aborted: ${launchPrepFailReason}`
|
||||
)
|
||||
dialog.showErrorBox(
|
||||
t('box.error.launchAborted', 'Launch aborted'),
|
||||
launchPrepFailReason!
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -577,44 +599,16 @@ class LegendaryGame extends Game {
|
||||
? ['--override-exe', gameSettings.targetExe]
|
||||
: []
|
||||
|
||||
const isNative = this.isNative()
|
||||
|
||||
const languageCode =
|
||||
gameSettings.language || (configStore.get('language', '') as string)
|
||||
|
||||
const languageFlag = languageCode ? ['--language', languageCode] : []
|
||||
|
||||
let commandParts = new Array<string>()
|
||||
let commandEnv = process.env
|
||||
let wrappers = new Array<string>()
|
||||
if (isNative) {
|
||||
if (!isWindows) {
|
||||
// These options can only be used on Mac/Linux
|
||||
commandEnv = {
|
||||
...commandEnv,
|
||||
...setupEnvVars(gameSettings)
|
||||
}
|
||||
|
||||
wrappers = setupWrappers(
|
||||
gameSettings,
|
||||
mangoHudCommand,
|
||||
gameModeBin,
|
||||
steamRuntime
|
||||
)
|
||||
}
|
||||
|
||||
// These options are required on both Windows and Mac
|
||||
commandParts = [
|
||||
'launch',
|
||||
this.appName,
|
||||
...languageFlag,
|
||||
...exeOverrideFlag,
|
||||
offlineFlag,
|
||||
...shlex.split(launchArguments ?? ''),
|
||||
...shlex.split(gameSettings.launcherArgs ?? '')
|
||||
]
|
||||
} else {
|
||||
// -> We're using Wine/Proton/CX on either Linux or Mac
|
||||
let commandEnv = isWindows
|
||||
? process.env
|
||||
: { ...process.env, ...setupEnvVars(gameSettings) }
|
||||
const wineFlag: string[] = []
|
||||
if (!this.isNative()) {
|
||||
// -> We're using Wine/Proton on Linux or CX on Mac
|
||||
const {
|
||||
success: wineLaunchPrepSuccess,
|
||||
failureReason: wineLaunchPrepFailReason,
|
||||
@@ -625,66 +619,50 @@ class LegendaryGame extends Game {
|
||||
this.logFileLocation,
|
||||
`Launch aborted: ${wineLaunchPrepFailReason}`
|
||||
)
|
||||
dialog.showErrorBox(
|
||||
t('box.error.launchAborted', 'Launch aborted'),
|
||||
wineLaunchPrepFailReason!
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
commandEnv = {
|
||||
...commandEnv,
|
||||
...setupEnvVars(gameSettings),
|
||||
...wineEnvVars
|
||||
}
|
||||
|
||||
wrappers = setupWrappers(
|
||||
gameSettings,
|
||||
mangoHudCommand,
|
||||
gameModeBin,
|
||||
steamRuntime
|
||||
)
|
||||
|
||||
const { wineVersion, winePrefix, launcherArgs, useSteamRuntime } =
|
||||
gameSettings
|
||||
const { bin: wineExec, type: wineType } = gameSettings.wineVersion
|
||||
|
||||
// Fix for people with old config
|
||||
const wineBin =
|
||||
wineVersion.bin.startsWith("'") && wineVersion.bin.endsWith("'")
|
||||
? wineVersion.bin.replaceAll("'", '')
|
||||
: wineVersion.bin
|
||||
wineExec.startsWith("'") && wineExec.endsWith("'")
|
||||
? wineExec.replaceAll("'", '')
|
||||
: wineExec
|
||||
|
||||
let wineFlag = ['--wine', wineBin]
|
||||
|
||||
// avoid breaking on old configs when path is not absolute
|
||||
let winePrefixFlag = isMac ? [] : ['--wine-prefix', winePrefix]
|
||||
if (wineVersion.type === 'proton') {
|
||||
let runtime = null as SteamRuntime
|
||||
if (useSteamRuntime) {
|
||||
await getSteamRuntime('soldier').then((path) => (runtime = path))
|
||||
}
|
||||
|
||||
if (runtime?.path) {
|
||||
// The Steam runtime masks /run, so if our game is on another hard drive, we'll get problems. Just including the game's install path
|
||||
// should be fine for now, if we ever get more issues we can change this to just /run/ entirely or something
|
||||
const runWithRuntime = `${runtime.path} --filesystem=${gameInfo.install.install_path} -- '${wineVersion.bin}' waitforexitandrun`
|
||||
wineFlag = ['--no-wine', '--wrapper', runWithRuntime]
|
||||
winePrefixFlag = []
|
||||
} else {
|
||||
logWarning('No Steam runtime found', LogPrefix.Legendary)
|
||||
wineFlag = ['--no-wine', '--wrapper', `'${wineVersion.bin}' run`]
|
||||
winePrefixFlag = []
|
||||
}
|
||||
}
|
||||
|
||||
commandParts = [
|
||||
'launch',
|
||||
this.appName,
|
||||
...languageFlag,
|
||||
...exeOverrideFlag,
|
||||
offlineFlag,
|
||||
...wineFlag,
|
||||
...winePrefixFlag,
|
||||
...shlex.split(launchArguments ?? ''),
|
||||
...shlex.split(launcherArgs ?? '')
|
||||
]
|
||||
wineFlag.push(
|
||||
...(wineType === 'proton'
|
||||
? ['--no-wine', '--wrapper', `'${wineBin}' run`]
|
||||
: ['--wine', wineBin])
|
||||
)
|
||||
}
|
||||
const commandParts = [
|
||||
'launch',
|
||||
this.appName,
|
||||
...languageFlag,
|
||||
...exeOverrideFlag,
|
||||
...offlineFlag,
|
||||
...wineFlag,
|
||||
...shlex.split(launchArguments ?? ''),
|
||||
...shlex.split(gameSettings.launcherArgs ?? '')
|
||||
]
|
||||
const wrappers = setupWrappers(
|
||||
gameSettings,
|
||||
mangoHudCommand,
|
||||
gameModeBin,
|
||||
steamRuntime?.length
|
||||
? [...steamRuntime, `--filesystem=${gameInfo.install.install_path}`]
|
||||
: undefined
|
||||
)
|
||||
|
||||
const fullCommand = getRunnerCallWithoutCredentials(
|
||||
commandParts,
|
||||
@@ -753,10 +731,6 @@ class LegendaryGame extends Game {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isLinux && gameInfo?.install?.platform === 'Linux') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -769,7 +743,8 @@ class LegendaryGame extends Game {
|
||||
'-y',
|
||||
'--keep-files'
|
||||
])
|
||||
const mainWindow = BrowserWindow.getFocusedWindow()
|
||||
const mainWindow =
|
||||
BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]
|
||||
mainWindow.webContents.send('refreshLibrary', 'legendary')
|
||||
} catch (error) {
|
||||
logError(
|
||||
@@ -0,0 +1,554 @@
|
||||
import { existsSync, readFileSync, readdirSync } from 'graceful-fs'
|
||||
|
||||
import {
|
||||
GameInfo,
|
||||
InstalledInfo,
|
||||
CallRunnerOptions,
|
||||
ExecResult,
|
||||
InstallPlatform
|
||||
} from 'common/types'
|
||||
import {
|
||||
InstalledJsonMetadata,
|
||||
GameMetadata,
|
||||
LegendaryInstallInfo
|
||||
} from 'common/types/legendary'
|
||||
import { LegendaryGame } from './games'
|
||||
import { LegendaryUser } from './user'
|
||||
import {
|
||||
formatEpicStoreUrl,
|
||||
getLegendaryBin,
|
||||
isEpicServiceOffline,
|
||||
isOnline,
|
||||
getFileSize
|
||||
} from '../utils'
|
||||
import {
|
||||
fallBackImage,
|
||||
legendaryConfigPath,
|
||||
legendaryMetadata
|
||||
} from '../constants'
|
||||
import {
|
||||
logDebug,
|
||||
logError,
|
||||
logInfo,
|
||||
LogPrefix,
|
||||
logWarning
|
||||
} from '../logger/logger'
|
||||
import { installStore, libraryStore } from './electronStores'
|
||||
import { callRunner } from '../launcher'
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* Legendary LegendaryLibrary.
|
||||
*
|
||||
* For multi-account support, the single global instance will need to become a instance map.
|
||||
* @see GameConfig
|
||||
*/
|
||||
export class LegendaryLibrary {
|
||||
private static globalInstance: LegendaryLibrary
|
||||
|
||||
private allGames: Set<string> = new Set()
|
||||
private installedGames: Map<string, InstalledJsonMetadata> = new Map()
|
||||
private library: Map<string, GameInfo> = new Map()
|
||||
|
||||
/**
|
||||
* Private constructor for LegendaryLibrary since we don't really want it to be constructible from outside.
|
||||
*/
|
||||
private constructor() {
|
||||
this.loadGamesInAccount()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global library instance, and if it doesn't exist, create one.
|
||||
*
|
||||
* @returns LegendaryLibrary instance.
|
||||
*/
|
||||
public static get() {
|
||||
if (!this.globalInstance) {
|
||||
LegendaryLibrary.globalInstance = new LegendaryLibrary()
|
||||
}
|
||||
return this.globalInstance
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all of the user's games into `this.allGames`
|
||||
*/
|
||||
public loadGamesInAccount() {
|
||||
readdirSync(legendaryMetadata).forEach((filename) => {
|
||||
// This shouldn't ever happen, but just in case
|
||||
if (!filename.endsWith('.json')) {
|
||||
return
|
||||
}
|
||||
const appName = filename.split('.').at(0)!
|
||||
this.allGames.add(appName)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh games in the user's library.
|
||||
*/
|
||||
public async refresh(): Promise<ExecResult> {
|
||||
logInfo('Refreshing Epic Games...', LogPrefix.Legendary)
|
||||
const epicOffline = await isEpicServiceOffline()
|
||||
if (epicOffline) {
|
||||
logWarning(
|
||||
'Epic is Offline right now, cannot update game list!',
|
||||
LogPrefix.Backend
|
||||
)
|
||||
return { stderr: 'Epic offline, unable to update game list', stdout: '' }
|
||||
}
|
||||
|
||||
const res = await runLegendaryCommand(['list'])
|
||||
|
||||
if (res.error) {
|
||||
logError(['Failed to refresh library:', res.error], LogPrefix.Legendary)
|
||||
}
|
||||
this.refreshInstalled()
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh `this.installedGames` from file.
|
||||
*/
|
||||
public refreshInstalled() {
|
||||
const installedJSON = join(legendaryConfigPath, 'installed.json')
|
||||
if (existsSync(installedJSON)) {
|
||||
try {
|
||||
this.installedGames = new Map(
|
||||
Object.entries(JSON.parse(readFileSync(installedJSON, 'utf-8')))
|
||||
)
|
||||
} 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,
|
||||
false
|
||||
)
|
||||
this.installedGames = new Map()
|
||||
}
|
||||
} else {
|
||||
this.installedGames = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the game info of all games in the library
|
||||
*
|
||||
* @param fullRefresh Reload from Legendary.
|
||||
* @returns Array of objects.
|
||||
*/
|
||||
public async getGames(fullRefresh = false): Promise<GameInfo[]> {
|
||||
logInfo('Refreshing library...', LogPrefix.Legendary)
|
||||
const isLoggedIn = LegendaryUser.isLoggedIn()
|
||||
if (!isLoggedIn) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (fullRefresh) {
|
||||
this.refresh()
|
||||
}
|
||||
this.loadGamesInAccount()
|
||||
this.refreshInstalled()
|
||||
|
||||
try {
|
||||
await this.loadAll()
|
||||
} catch (error) {
|
||||
logError(`${error}`, LogPrefix.Legendary)
|
||||
}
|
||||
const arr = Array.from(this.library.values())
|
||||
|
||||
if (libraryStore.has('library')) {
|
||||
libraryStore.delete('library')
|
||||
}
|
||||
libraryStore.set('library', arr)
|
||||
logInfo(
|
||||
['Game list updated, got', `${arr.length}`, 'games & DLCs'],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
return arr
|
||||
}
|
||||
|
||||
/**
|
||||
* Get game info for a particular game.
|
||||
*
|
||||
* @param appName
|
||||
* @returns GameInfo
|
||||
*/
|
||||
public getGameInfo(appName: string): GameInfo | undefined {
|
||||
if (!this.hasGame(appName)) {
|
||||
logWarning(
|
||||
['Requested game', appName, 'was not found in library'],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
return
|
||||
}
|
||||
// We have the game, but info wasn't loaded yet
|
||||
if (!this.library.has(appName)) {
|
||||
this.loadFile(appName + '.json')
|
||||
}
|
||||
return this.library.get(appName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get game info for a particular game.
|
||||
*/
|
||||
public async getInstallInfo(
|
||||
appName: string,
|
||||
installPlatform: InstallPlatform
|
||||
): Promise<LegendaryInstallInfo> {
|
||||
const cache = installStore.get(appName) as LegendaryInstallInfo
|
||||
if (cache) {
|
||||
logDebug('Using cached install info', LogPrefix.Legendary)
|
||||
return cache
|
||||
}
|
||||
|
||||
logInfo(`Getting more details with 'legendary info'`, LogPrefix.Legendary)
|
||||
const res = await runLegendaryCommand([
|
||||
'info',
|
||||
appName,
|
||||
...(installPlatform ? ['--platform', installPlatform] : []),
|
||||
'--json',
|
||||
(await isEpicServiceOffline()) ? '--offline' : ''
|
||||
])
|
||||
|
||||
if (res.error) {
|
||||
logError(
|
||||
['Failed to get more details:', res.error],
|
||||
LogPrefix.Legendary,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
const info: LegendaryInstallInfo = JSON.parse(res.stdout)
|
||||
installStore.set(appName, info)
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a list of updateable games.
|
||||
*
|
||||
* @returns App names of updateable games.
|
||||
*/
|
||||
public async listUpdateableGames(): Promise<string[]> {
|
||||
const isLoggedIn = LegendaryUser.isLoggedIn()
|
||||
|
||||
if (!isLoggedIn || !isOnline()) {
|
||||
return []
|
||||
}
|
||||
const epicOffline = await isEpicServiceOffline()
|
||||
if (epicOffline) {
|
||||
logWarning(
|
||||
'Epic servers are offline, cannot check for game updates',
|
||||
LogPrefix.Backend
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const res = await runLegendaryCommand(['list'], {
|
||||
logMessagePrefix: 'Checking for game updates'
|
||||
})
|
||||
|
||||
if (res.error) {
|
||||
logError(
|
||||
['Failed to check for game updates:', res.error],
|
||||
LogPrefix.Legendary,
|
||||
false
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
// Once we ran `legendary list`, `assets.json` will be updated with the newest
|
||||
// game versions, and `installed.json` has our currently installed ones
|
||||
const installedJson: Record<string, InstalledJsonMetadata> = JSON.parse(
|
||||
readFileSync(join(legendaryConfigPath, 'installed.json')).toString()
|
||||
)
|
||||
// First go through all our installed games and store their versions...
|
||||
const installedGames: Map<string, { version: string; platform: string }> =
|
||||
new Map()
|
||||
for (const [appName, data] of Object.entries(installedJson)) {
|
||||
installedGames.set(appName, {
|
||||
version: data.version,
|
||||
platform: data.platform
|
||||
})
|
||||
}
|
||||
// ...and now go through all games in `assets.json` to get the newest version
|
||||
// HACK: Same as above, ↓ this isn't always `string`, but it works for now
|
||||
const assetsJson: Record<string, Record<string, string>[]> = JSON.parse(
|
||||
readFileSync(join(legendaryConfigPath, 'assets.json')).toString()
|
||||
)
|
||||
const updateableGames: string[] = []
|
||||
for (const [platform, assets] of Object.entries(assetsJson)) {
|
||||
installedGames.forEach(
|
||||
({ version: currentVersion, platform: installedPlatform }, appName) => {
|
||||
if (installedPlatform === platform) {
|
||||
const currentAsset = assets.find((asset) => {
|
||||
return asset.app_name === appName
|
||||
})
|
||||
if (!currentAsset) {
|
||||
logWarning(
|
||||
[
|
||||
'Game with AppName',
|
||||
appName,
|
||||
'is installed but was not found on account'
|
||||
],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
return
|
||||
}
|
||||
const latestVersion = currentAsset.build_version
|
||||
if (currentVersion !== latestVersion) {
|
||||
logDebug(
|
||||
[
|
||||
'Update is available for',
|
||||
`${appName}:`,
|
||||
currentVersion,
|
||||
'!=',
|
||||
latestVersion
|
||||
],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
updateableGames.push(appName)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
logInfo(
|
||||
[
|
||||
'Found',
|
||||
`${updateableGames.length}`,
|
||||
'game' + (updateableGames.length !== 1 ? 's' : ''),
|
||||
'to update'
|
||||
],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
return updateableGames
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all updateable games.
|
||||
* Uses `listUpdateableGames` along with `LegendaryGame.update`
|
||||
*
|
||||
* @returns Array of results of `Game.update`.
|
||||
*/
|
||||
public async updateAllGames() {
|
||||
return (
|
||||
await Promise.allSettled(
|
||||
(await this.listUpdateableGames())
|
||||
.map(LegendaryGame.get)
|
||||
.map(async (game) => game.update())
|
||||
)
|
||||
).map((res) => {
|
||||
if (res.status === 'fulfilled') {
|
||||
return res.value
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the install path for a given game.
|
||||
*
|
||||
* DOES NOT MOVE FILES. Use `LegendaryGame.moveInstall` instead.
|
||||
*
|
||||
* @param appName
|
||||
* @param newPath
|
||||
*/
|
||||
public async changeGameInstallPath(appName: string, newPath: string) {
|
||||
// @ts-expect-error TODO: Verify appName is valid here
|
||||
this.library.get(appName).install.install_path = newPath
|
||||
// @ts-expect-error Same as above
|
||||
this.installedGames.get(appName).install_path = newPath
|
||||
|
||||
const { error } = await runLegendaryCommand([
|
||||
'move',
|
||||
appName,
|
||||
newPath,
|
||||
'--skip-move'
|
||||
])
|
||||
if (error) {
|
||||
logError(
|
||||
['Failed to set install path for', `${appName}:`, error],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the install state of a game without a complete library reload.
|
||||
*
|
||||
* @param appName
|
||||
* @param state true if its installed, false otherwise.
|
||||
*/
|
||||
public installState(appName: string, state: boolean) {
|
||||
if (state) {
|
||||
// This assumes that fileName and appName are same.
|
||||
// If that changes, this will break.
|
||||
this.loadFile(`${appName}.json`)
|
||||
} else {
|
||||
// @ts-expect-error TODO: Make sure game info is loaded & appName is valid here
|
||||
this.library.get(appName).is_installed = false
|
||||
// @ts-expect-error Same as above
|
||||
this.library.get(appName).install = {} as InstalledInfo
|
||||
this.installedGames.delete(appName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the file completely into our in-memory library.
|
||||
* Largely derived from legacy code.
|
||||
*
|
||||
* @returns True/False, whether or not the file was loaded
|
||||
*/
|
||||
private loadFile(fileName: string): boolean {
|
||||
const fullPath = join(legendaryMetadata, fileName)
|
||||
|
||||
let app_name: string
|
||||
let metadata
|
||||
try {
|
||||
const data: GameMetadata = JSON.parse(readFileSync(fullPath, 'utf-8'))
|
||||
app_name = data.app_name
|
||||
metadata = data.metadata
|
||||
} catch (error) {
|
||||
logError(['Failed to parse', fileName], LogPrefix.Legendary)
|
||||
return false
|
||||
}
|
||||
const { namespace } = metadata
|
||||
|
||||
if (namespace === 'ue') {
|
||||
return false
|
||||
}
|
||||
|
||||
const {
|
||||
description,
|
||||
longDescription = '',
|
||||
keyImages = [],
|
||||
title,
|
||||
developer,
|
||||
dlcItemList,
|
||||
releaseInfo,
|
||||
customAttributes
|
||||
} = metadata
|
||||
|
||||
const dlcs: string[] = []
|
||||
const CloudSaveFolder = customAttributes?.CloudSaveFolder
|
||||
const FolderName = customAttributes?.FolderName
|
||||
const canRunOffline = customAttributes?.CanRunOffline?.value === 'true'
|
||||
|
||||
if (dlcItemList) {
|
||||
dlcItemList.forEach((v: { releaseInfo: { appId: string }[] }) => {
|
||||
if (v.releaseInfo && v.releaseInfo[0]) {
|
||||
dlcs.push(v.releaseInfo[0].appId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const cloud_save_enabled = Boolean(CloudSaveFolder?.value)
|
||||
const saveFolder = cloud_save_enabled ? CloudSaveFolder.value : ''
|
||||
const installFolder = FolderName ? FolderName.value : app_name
|
||||
|
||||
const gameBox = keyImages.find(({ type }) => type === 'DieselGameBox')
|
||||
|
||||
const gameBoxTall = keyImages.find(
|
||||
({ type }) => type === 'DieselGameBoxTall'
|
||||
)
|
||||
|
||||
const gameBoxStore = keyImages.find(
|
||||
({ type }) => type === 'DieselStoreFrontTall'
|
||||
)
|
||||
|
||||
const logo = keyImages.find(({ type }) => type === 'DieselGameBoxLogo')
|
||||
|
||||
const art_cover = gameBox ? gameBox.url : undefined
|
||||
const art_logo = logo ? logo.url : undefined
|
||||
const art_square = gameBoxTall ? gameBoxTall.url : undefined
|
||||
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 is_dlc = Boolean(metadata.mainGameItem)
|
||||
|
||||
const convertedSize = install_size ? getFileSize(Number(install_size)) : '0'
|
||||
|
||||
this.library.set(app_name, {
|
||||
app_name,
|
||||
art_cover: art_cover || art_square || fallBackImage,
|
||||
art_logo,
|
||||
art_square: art_square || art_square_front || art_cover || fallBackImage,
|
||||
cloud_save_enabled,
|
||||
developer,
|
||||
extra: {
|
||||
about: {
|
||||
description,
|
||||
longDescription
|
||||
},
|
||||
reqs: []
|
||||
},
|
||||
folder_name: installFolder,
|
||||
install: {
|
||||
executable,
|
||||
install_path,
|
||||
install_size: convertedSize,
|
||||
is_dlc,
|
||||
version,
|
||||
platform
|
||||
},
|
||||
is_installed: info !== undefined,
|
||||
namespace,
|
||||
is_mac_native: info
|
||||
? platform === 'Mac'
|
||||
: releaseInfo[0].platform.includes('Mac'),
|
||||
save_folder: saveFolder,
|
||||
title,
|
||||
canRunOffline,
|
||||
is_linux_native: false,
|
||||
runner: 'legendary',
|
||||
store_url: formatEpicStoreUrl(title)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully loads all files in library into memory.
|
||||
*
|
||||
* @returns App names of loaded files.
|
||||
*/
|
||||
private async loadAll(): Promise<string[]> {
|
||||
if (existsSync(legendaryMetadata)) {
|
||||
const loadedFiles: string[] = []
|
||||
this.allGames.forEach((appName) => {
|
||||
const wasLoaded = this.loadFile(appName + '.json')
|
||||
if (wasLoaded) {
|
||||
loadedFiles.push(appName)
|
||||
}
|
||||
})
|
||||
return loadedFiles
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a game is in the users account
|
||||
* @param appName The game to search for
|
||||
* @returns True = Game is in account, False = Game is not in account
|
||||
*/
|
||||
public hasGame = (appName: string) => this.allGames.has(appName)
|
||||
}
|
||||
|
||||
export async function runLegendaryCommand(
|
||||
commandParts: string[],
|
||||
options?: CallRunnerOptions
|
||||
): Promise<ExecResult> {
|
||||
const { dir, bin } = getLegendaryBin()
|
||||
return callRunner(
|
||||
commandParts,
|
||||
{ name: 'legendary', logPrefix: LogPrefix.Legendary, bin, dir },
|
||||
options
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { existsSync, readFileSync } from 'graceful-fs'
|
||||
|
||||
import { UserInfo } from 'common/types'
|
||||
import { clearCache } from '../utils'
|
||||
import { userInfo, configStore } from '../constants'
|
||||
import { logError, LogPrefix } from '../logger/logger'
|
||||
import { userInfo as user } from 'os'
|
||||
import { session } from 'electron'
|
||||
import { runLegendaryCommand } from './library'
|
||||
|
||||
export class LegendaryUser {
|
||||
public static async login(sid: string) {
|
||||
const commandParts = ['auth', '--sid', sid]
|
||||
|
||||
try {
|
||||
await runLegendaryCommand(commandParts, {
|
||||
logMessagePrefix: 'Logging in'
|
||||
})
|
||||
const userInfo = await this.getUserInfo()
|
||||
return { status: 'done', data: userInfo }
|
||||
} catch (error) {
|
||||
logError(
|
||||
['Failed to login with Legendary:', `${error}`],
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
|
||||
return { status: 'failed' }
|
||||
}
|
||||
}
|
||||
|
||||
public static async logout() {
|
||||
const commandParts = ['auth', '--delete']
|
||||
|
||||
const res = await runLegendaryCommand(commandParts, {
|
||||
logMessagePrefix: 'Logging out'
|
||||
})
|
||||
|
||||
if (res.error) {
|
||||
logError(['Failed to logout:', res.error], LogPrefix.Legendary)
|
||||
}
|
||||
|
||||
const ses = session.fromPartition('persist:epicstore')
|
||||
await ses.clearStorageData()
|
||||
await ses.clearCache()
|
||||
await ses.clearAuthCache()
|
||||
await ses.clearHostResolverCache()
|
||||
configStore.clear()
|
||||
clearCache()
|
||||
}
|
||||
|
||||
public static isLoggedIn() {
|
||||
return existsSync(userInfo)
|
||||
}
|
||||
|
||||
public static async getUserInfo(): Promise<UserInfo> {
|
||||
if (!LegendaryUser.isLoggedIn()) {
|
||||
configStore.delete('userInfo')
|
||||
return {}
|
||||
}
|
||||
const userInfoContent = readFileSync(userInfo).toString()
|
||||
try {
|
||||
const userInfoObject = JSON.parse(userInfoContent)
|
||||
const info: UserInfo = {
|
||||
account_id: userInfoObject.account_id,
|
||||
displayName: userInfoObject.displayName,
|
||||
user: user().username
|
||||
}
|
||||
configStore.set('userInfo', info)
|
||||
return info
|
||||
} catch (error) {
|
||||
logError(
|
||||
`User info file corrupted, check ${userInfo}`,
|
||||
LogPrefix.Legendary
|
||||
)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -8,8 +8,9 @@ import {
|
||||
Runner,
|
||||
AppSettings,
|
||||
GameSettings,
|
||||
GOGCloudSavesLocation
|
||||
} from './types'
|
||||
InstallPlatform
|
||||
} from 'common/types'
|
||||
import { GOGCloudSavesLocation } from 'common/types/gog'
|
||||
import * as path from 'path'
|
||||
import {
|
||||
BrowserWindow,
|
||||
@@ -43,7 +44,6 @@ import i18next from 'i18next'
|
||||
import { join, normalize } from 'path'
|
||||
import checkDiskSpace from 'check-disk-space'
|
||||
import { DXVK, Winetricks } from './tools'
|
||||
import { Game } from './games'
|
||||
import { GameConfig } from './game_config'
|
||||
import { GlobalConfig } from './config'
|
||||
import { LegendaryLibrary } from './legendary/library'
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
showErrorBoxModal,
|
||||
getFileSize,
|
||||
detectVCRedist,
|
||||
getGame,
|
||||
getFirstExistingParentPath,
|
||||
getLatestReleases,
|
||||
notify
|
||||
@@ -98,7 +99,8 @@ import {
|
||||
isSteamDeckGameMode,
|
||||
isCLIFullscreen,
|
||||
isCLINoGui,
|
||||
isFlatpak
|
||||
isFlatpak,
|
||||
publicDir
|
||||
} from './constants'
|
||||
import { handleProtocol } from './protocol'
|
||||
import {
|
||||
@@ -116,7 +118,7 @@ import shlex from 'shlex'
|
||||
const { showMessageBox, showOpenDialog } = dialog
|
||||
const isWindows = platform() === 'win32'
|
||||
|
||||
let mainWindow: BrowserWindow = null
|
||||
let mainWindow: BrowserWindow
|
||||
|
||||
async function createWindow(): Promise<BrowserWindow> {
|
||||
configStore.set('userHome', userHome)
|
||||
@@ -228,23 +230,22 @@ async function createWindow(): Promise<BrowserWindow> {
|
||||
logWarning(['An error occurred: ', err], LogPrefix.Backend)
|
||||
})
|
||||
})
|
||||
mainWindow.loadURL('http://localhost:3000')
|
||||
mainWindow.loadURL('http://localhost:5173')
|
||||
// Open the DevTools.
|
||||
mainWindow.webContents.openDevTools()
|
||||
} else {
|
||||
Menu.setApplicationMenu(null)
|
||||
mainWindow.loadURL(`file://${path.join(__dirname, '../build/index.html')}`)
|
||||
mainWindow.loadURL(`file://${path.join(publicDir, '../build/index.html')}`)
|
||||
if (!isMac) {
|
||||
autoUpdater.checkForUpdates()
|
||||
}
|
||||
return mainWindow
|
||||
}
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
let appIcon: Tray = null
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
let openUrlArgument = ''
|
||||
|
||||
@@ -356,9 +357,9 @@ if (!gotTheLock) {
|
||||
|
||||
await i18next.use(Backend).init({
|
||||
backend: {
|
||||
addPath: path.join(__dirname, 'locales', '{{lng}}', '{{ns}}'),
|
||||
addPath: path.join(publicDir, 'locales', '{{lng}}', '{{ns}}'),
|
||||
allowMultiLoading: false,
|
||||
loadPath: path.join(__dirname, 'locales', '{{lng}}', '{{ns}}.json')
|
||||
loadPath: path.join(publicDir, 'locales', '{{lng}}', '{{ns}}.json')
|
||||
},
|
||||
debug: false,
|
||||
fallbackLng: 'en',
|
||||
@@ -429,7 +430,7 @@ if (!gotTheLock) {
|
||||
}, 200)
|
||||
|
||||
const trayIcon = darkTrayIcon ? iconDark : iconLight
|
||||
appIcon = new Tray(trayIcon)
|
||||
const appIcon = new Tray(trayIcon)
|
||||
|
||||
appIcon.on('double-click', () => {
|
||||
mainWindow.show()
|
||||
@@ -495,7 +496,6 @@ ipcMain.on('lock', () => {
|
||||
if (!powerId) {
|
||||
logInfo('Preventing machine to sleep', LogPrefix.Backend)
|
||||
powerId = powerSaveBlocker.start('prevent-app-suspension')
|
||||
return powerId
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -511,7 +511,7 @@ ipcMain.on('unlock', () => {
|
||||
})
|
||||
|
||||
ipcMain.handle('kill', async (event, appName, runner) => {
|
||||
return Game.get(appName, runner).stop()
|
||||
return getGame(appName, runner).stop()
|
||||
})
|
||||
|
||||
ipcMain.handle('checkDiskSpace', async (event, folder: string) => {
|
||||
@@ -621,7 +621,7 @@ interface Tools {
|
||||
ipcMain.handle(
|
||||
'callTool',
|
||||
async (event, { tool, exe, appName, runner }: Tools) => {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const { wineVersion, winePrefix } = await game.getSettings()
|
||||
await verifyWinePrefix(game)
|
||||
|
||||
@@ -641,18 +641,11 @@ ipcMain.handle(
|
||||
|
||||
/// IPC handlers begin here.
|
||||
|
||||
ipcMain.handle('checkGameUpdates', async (event, library?: Runner) => {
|
||||
switch (library) {
|
||||
case 'legendary':
|
||||
return LegendaryLibrary.get().listUpdateableGames()
|
||||
case 'gog':
|
||||
return GOGLibrary.get().listUpdateableGames()
|
||||
default:
|
||||
return [
|
||||
...(await LegendaryLibrary.get().listUpdateableGames()),
|
||||
...(await GOGLibrary.get().listUpdateableGames())
|
||||
]
|
||||
}
|
||||
ipcMain.handle('checkGameUpdates', async () => {
|
||||
return [
|
||||
...(await LegendaryLibrary.get().listUpdateableGames()),
|
||||
...(await GOGLibrary.get().listUpdateableGames())
|
||||
]
|
||||
})
|
||||
|
||||
ipcMain.handle('getEpicGamesStatus', async () => isEpicServiceOffline())
|
||||
@@ -676,6 +669,8 @@ ipcMain.handle('getLatestReleases', async () => {
|
||||
const { checkForUpdatesOnStartup } = GlobalConfig.get().config
|
||||
if (checkForUpdatesOnStartup) {
|
||||
return getLatestReleases()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -710,27 +705,35 @@ ipcMain.on('createNewWindow', async (e, url) =>
|
||||
new BrowserWindow({ height: 700, width: 1200 }).loadURL(url)
|
||||
)
|
||||
|
||||
ipcMain.handle('getGameInfo', async (event, appName, runner) => {
|
||||
try {
|
||||
const game = Game.get(appName, runner)
|
||||
const info = game.getGameInfo()
|
||||
if (!info) {
|
||||
ipcMain.handle(
|
||||
'getGameInfo',
|
||||
async (event, appName: string, runner: Runner) => {
|
||||
// Fastpath since we sometines have to request info for a GOG game as Legendary because we don't know it's a GOG game yet
|
||||
if (runner === 'legendary' && !LegendaryLibrary.get().hasGame(appName)) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const game = getGame(appName, runner)
|
||||
const info = game.getGameInfo()
|
||||
if (!info.app_name) {
|
||||
return null
|
||||
}
|
||||
info.extra = await game.getExtraInfo()
|
||||
// eslint-disable-next-line @typescript-eslint/return-await
|
||||
return info
|
||||
} catch (error) {
|
||||
logError(`${error}`, LogPrefix.Backend)
|
||||
return null
|
||||
}
|
||||
info.extra = await game.getExtraInfo()
|
||||
// eslint-disable-next-line @typescript-eslint/return-await
|
||||
return info
|
||||
} catch (error) {
|
||||
logError(`${error}`, LogPrefix.Backend)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle('getGameSettings', async (event, game, runner) => {
|
||||
try {
|
||||
const settings = await Game.get(game, runner).getSettings()
|
||||
return settings
|
||||
return await getGame(game, runner).getSettings()
|
||||
} catch (error) {
|
||||
logError(`${error}`, LogPrefix.Backend)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -744,13 +747,14 @@ ipcMain.handle('getGOGGameClientId', (event, appName) => {
|
||||
|
||||
ipcMain.handle(
|
||||
'getInstallInfo',
|
||||
async (event, game, runner: Runner, installPlatform: string) => {
|
||||
async (event, game, runner: Runner, installPlatform: InstallPlatform) => {
|
||||
if (!isOnline()) {
|
||||
return { game: {}, metadata: {} }
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await Game.get(game, runner).getInstallInfo(installPlatform)
|
||||
// @ts-ignore This is actually fine as long as the frontend always passes the right InstallPlatform for the right runner
|
||||
const info = await getGame(game, runner).getInstallInfo(installPlatform)
|
||||
return info
|
||||
} catch (error) {
|
||||
logError(`${error}`, LogPrefix.Backend)
|
||||
@@ -776,7 +780,7 @@ ipcMain.handle('getAlternativeWine', async () =>
|
||||
ipcMain.handle('readConfig', async (event, config_class) => {
|
||||
switch (config_class) {
|
||||
case 'library':
|
||||
return LegendaryLibrary.get().getGames('info')
|
||||
return LegendaryLibrary.get().getGames()
|
||||
case 'user':
|
||||
return (await LegendaryUser.getUserInfo()).displayName
|
||||
default:
|
||||
@@ -801,7 +805,7 @@ ipcMain.handle('requestSettings', async (event, appName) => {
|
||||
.filter((val) => val.indexOf('=') !== -1)
|
||||
.forEach((envKeyAndVar) => {
|
||||
const keyAndValueSplit = envKeyAndVar.split('=')
|
||||
const key = keyAndValueSplit.shift()
|
||||
const key = keyAndValueSplit.shift()!
|
||||
const value = keyAndValueSplit.join('=')
|
||||
config.enviromentOptions.push({ key, value })
|
||||
})
|
||||
@@ -821,7 +825,7 @@ ipcMain.handle('requestSettings', async (event, appName) => {
|
||||
})
|
||||
|
||||
if (config.wrapperOptions.at(0)) {
|
||||
config.wrapperOptions.at(0).args = shlex.join(args)
|
||||
config.wrapperOptions.at(0)!.args = shlex.join(args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,14 +877,14 @@ if (existsSync(installed)) {
|
||||
ipcMain.handle('refreshLibrary', async (e, fullRefresh, library?: Runner) => {
|
||||
switch (library) {
|
||||
case 'legendary':
|
||||
await LegendaryLibrary.get().getGames('info', fullRefresh)
|
||||
await LegendaryLibrary.get().getGames(fullRefresh)
|
||||
break
|
||||
case 'gog':
|
||||
await GOGLibrary.get().sync()
|
||||
break
|
||||
default:
|
||||
await Promise.allSettled([
|
||||
LegendaryLibrary.get().getGames('info', fullRefresh),
|
||||
LegendaryLibrary.get().getGames(fullRefresh),
|
||||
GOGLibrary.get().sync()
|
||||
])
|
||||
break
|
||||
@@ -909,7 +913,7 @@ ipcMain.handle(
|
||||
const window = BrowserWindow.getAllWindows()[0]
|
||||
const recentGames =
|
||||
(configStore.get('games.recent') as Array<RecentGame>) || []
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const { title } = game.getGameInfo()
|
||||
const { minimizeOnLaunch, maxRecentGames: MAX_RECENT_GAMES = 5 } =
|
||||
await GlobalConfig.get().getSettings()
|
||||
@@ -976,11 +980,11 @@ ipcMain.handle(
|
||||
)
|
||||
return game
|
||||
.launch(launchArguments)
|
||||
.catch((exception: Error) => {
|
||||
logError(exception.stack, LogPrefix.Backend)
|
||||
.catch((error) => {
|
||||
logError(error.stack ?? `${error}`, LogPrefix.Backend)
|
||||
appendFileSync(
|
||||
game.logFileLocation,
|
||||
`An exception occurred when launching the game:\n${exception.stack}`
|
||||
`An exception occurred when launching the game:\n${error.stack}`
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -1061,12 +1065,12 @@ ipcMain.handle('install', async (event, params) => {
|
||||
appName,
|
||||
path,
|
||||
installDlcs,
|
||||
sdlList,
|
||||
sdlList = [],
|
||||
runner,
|
||||
installLanguage,
|
||||
platformToInstall
|
||||
} = params as InstallParams
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const { title } = game.getGameInfo()
|
||||
|
||||
if (!isOnline()) {
|
||||
@@ -1138,7 +1142,7 @@ ipcMain.handle('install', async (event, params) => {
|
||||
|
||||
ipcMain.handle('uninstall', async (event, args) => {
|
||||
const [appName, shouldRemovePrefix, runner] = args
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
|
||||
const { title } = game.getGameInfo()
|
||||
const { winePrefix } = await game.getSettings()
|
||||
@@ -1167,7 +1171,7 @@ ipcMain.handle('repair', async (event, appName, runner) => {
|
||||
)
|
||||
return
|
||||
}
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const { title } = game.getGameInfo()
|
||||
|
||||
return game
|
||||
@@ -1186,7 +1190,7 @@ ipcMain.handle('repair', async (event, appName, runner) => {
|
||||
})
|
||||
|
||||
ipcMain.handle('moveInstall', async (event, [appName, path, runner]) => {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const { title } = game.getGameInfo()
|
||||
try {
|
||||
notify({ title, body: i18next.t('notify.moving', 'Moving Game') })
|
||||
@@ -1202,51 +1206,58 @@ ipcMain.handle('moveInstall', async (event, [appName, path, runner]) => {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('importGame', async (event, args) => {
|
||||
const { appName, path, runner } = args
|
||||
const epicOffline = await isEpicServiceOffline()
|
||||
if (epicOffline && runner === 'legendary') {
|
||||
showErrorBoxModal(
|
||||
mainWindow,
|
||||
i18next.t('box.warning.title', 'Warning'),
|
||||
i18next.t(
|
||||
'box.warning.epic.import',
|
||||
'Epic Servers are having major outage right now, the game cannot be imported!'
|
||||
ipcMain.handle(
|
||||
'importGame',
|
||||
async (event, args): Promise<{ status: 'done' | 'error' }> => {
|
||||
const { appName, path, runner } = args
|
||||
const epicOffline = await isEpicServiceOffline()
|
||||
if (epicOffline && runner === 'legendary') {
|
||||
showErrorBoxModal(
|
||||
mainWindow,
|
||||
i18next.t('box.warning.title', 'Warning'),
|
||||
i18next.t(
|
||||
'box.warning.epic.import',
|
||||
'Epic Servers are having major outage right now, the game cannot be imported!'
|
||||
)
|
||||
)
|
||||
)
|
||||
return { status: 'error' }
|
||||
}
|
||||
const game = Game.get(appName, runner)
|
||||
const { title } = game.getGameInfo()
|
||||
mainWindow.webContents.send('setGameStatus', {
|
||||
appName,
|
||||
runner,
|
||||
status: 'installing'
|
||||
})
|
||||
game
|
||||
.import(path)
|
||||
.then(() => {
|
||||
notify({
|
||||
title,
|
||||
body: i18next.t('notify.install.imported', 'Game Imported')
|
||||
})
|
||||
mainWindow.webContents.send('setGameStatus', {
|
||||
appName,
|
||||
runner,
|
||||
status: 'done'
|
||||
})
|
||||
logInfo(`imported ${title}`, LogPrefix.Backend)
|
||||
return { status: 'error' }
|
||||
}
|
||||
const game = getGame(appName, runner)
|
||||
const { title } = game.getGameInfo()
|
||||
mainWindow.webContents.send('setGameStatus', {
|
||||
appName,
|
||||
runner,
|
||||
status: 'installing'
|
||||
})
|
||||
.catch((err) => {
|
||||
try {
|
||||
await game.import(path)
|
||||
} catch (error) {
|
||||
notify({ title, body: i18next.t('notify.install.canceled') })
|
||||
mainWindow.webContents.send('setGameStatus', {
|
||||
appName,
|
||||
runner,
|
||||
status: 'done'
|
||||
})
|
||||
logInfo(err, LogPrefix.Backend)
|
||||
logInfo(
|
||||
error instanceof Error ? error.stack ?? error.message : `${error}`,
|
||||
LogPrefix.Backend
|
||||
)
|
||||
return { status: 'error' }
|
||||
}
|
||||
|
||||
notify({
|
||||
title,
|
||||
body: i18next.t('notify.install.imported', 'Game Imported')
|
||||
})
|
||||
})
|
||||
mainWindow.webContents.send('setGameStatus', {
|
||||
appName,
|
||||
runner,
|
||||
status: 'done'
|
||||
})
|
||||
logInfo(`imported ${title}`, LogPrefix.Backend)
|
||||
return { status: 'done' }
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle('updateGame', async (e, appName, runner) => {
|
||||
if (!isOnline()) {
|
||||
@@ -1270,7 +1281,7 @@ ipcMain.handle('updateGame', async (e, appName, runner) => {
|
||||
return { status: 'error' }
|
||||
}
|
||||
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const { title } = game.getGameInfo()
|
||||
notify({ title, body: i18next.t('notify.update.started', 'Update Started') })
|
||||
|
||||
@@ -1355,7 +1366,7 @@ ipcMain.handle(
|
||||
gogSaves: GOGCloudSavesLocation[],
|
||||
appName: string,
|
||||
arg: string
|
||||
) => Game.get(appName, 'gog').syncSaves(arg, '', gogSaves)
|
||||
) => getGame(appName, 'gog').syncSaves(arg, '', gogSaves)
|
||||
)
|
||||
|
||||
ipcMain.handle('syncSaves', async (event, args) => {
|
||||
@@ -1373,10 +1384,7 @@ ipcMain.handle('syncSaves', async (event, args) => {
|
||||
return
|
||||
}
|
||||
|
||||
const { stderr, stdout } = await Game.get(appName, runner).syncSaves(
|
||||
arg,
|
||||
path
|
||||
)
|
||||
const { stderr, stdout } = await getGame(appName, runner).syncSaves(arg, path)
|
||||
logInfo(`${stdout}`, LogPrefix.Backend)
|
||||
if (stderr.includes('ERROR')) {
|
||||
logError(`${stderr}`, LogPrefix.Backend)
|
||||
@@ -1499,7 +1507,7 @@ ipcMain.handle('getFonts', async (event, reload = false) => {
|
||||
ipcMain.handle(
|
||||
'runWineCommandForGame',
|
||||
async (event, { appName, command, runner }) => {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
if (isWindows) {
|
||||
return execAsync(command)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BrowserWindow, dialog } from 'electron'
|
||||
import { Game } from './games'
|
||||
import { logError, logInfo, LogPrefix } from './logger/logger'
|
||||
import i18next from 'i18next'
|
||||
import { getGame } from './utils'
|
||||
|
||||
export async function handleProtocol(window: BrowserWindow, args: string[]) {
|
||||
// Figure out which argv element is our protocol
|
||||
@@ -19,7 +19,7 @@ export async function handleProtocol(window: BrowserWindow, args: string[]) {
|
||||
let [command, arg] = path.split('/')
|
||||
if (!command || !arg) {
|
||||
command = path
|
||||
arg = null
|
||||
arg = ''
|
||||
}
|
||||
|
||||
logInfo(`received '${url}'`, LogPrefix.ProtocolHandler)
|
||||
@@ -30,8 +30,8 @@ export async function handleProtocol(window: BrowserWindow, args: string[]) {
|
||||
|
||||
if (command === 'launch') {
|
||||
const game =
|
||||
Game.get(arg, 'legendary').getGameInfo() ||
|
||||
Game.get(arg, 'gog').getGameInfo()
|
||||
getGame(arg, 'legendary').getGameInfo() ||
|
||||
getGame(arg, 'gog').getGameInfo()
|
||||
|
||||
if (!game) {
|
||||
return logError(
|
||||
@@ -3,13 +3,13 @@ import { GlobalConfig } from '../config'
|
||||
import { ipcMain, dialog } from 'electron'
|
||||
import i18next from 'i18next'
|
||||
import { join } from 'path'
|
||||
import { Game } from '../games'
|
||||
import { Runner } from '../types'
|
||||
import { Runner } from 'common/types'
|
||||
import {
|
||||
addNonSteamGame,
|
||||
isAddedToSteam,
|
||||
removeNonSteamGame
|
||||
} from './nonesteamgame/nonesteamgame'
|
||||
import { getGame } from '../utils'
|
||||
import { shortcutFiles } from './shortcuts/shortcuts'
|
||||
|
||||
const getSteamUserdataDir = async () => {
|
||||
@@ -20,7 +20,7 @@ const getSteamUserdataDir = async () => {
|
||||
ipcMain.on(
|
||||
'addShortcut',
|
||||
async (event, appName: string, runner: Runner, fromMenu: boolean) => {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
await game.addShortcuts(fromMenu)
|
||||
|
||||
dialog.showMessageBox({
|
||||
@@ -35,14 +35,14 @@ ipcMain.on(
|
||||
)
|
||||
|
||||
ipcMain.handle('shortcutsExists', (event, appName: string, runner: Runner) => {
|
||||
const title = Game.get(appName, runner).getGameInfo().title
|
||||
const title = getGame(appName, runner).getGameInfo().title
|
||||
const [desktopFile, menuFile] = shortcutFiles(title)
|
||||
|
||||
return existsSync(desktopFile) || existsSync(menuFile)
|
||||
return existsSync(desktopFile ?? '') || existsSync(menuFile ?? '')
|
||||
})
|
||||
|
||||
ipcMain.on('removeShortcut', async (event, appName: string, runner: Runner) => {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
await game.removeShortcuts()
|
||||
dialog.showMessageBox({
|
||||
buttons: [i18next.t('box.ok', 'Ok')],
|
||||
@@ -63,7 +63,7 @@ ipcMain.handle(
|
||||
bkgDataUrl: string,
|
||||
bigPicDataUrl: string
|
||||
) => {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const gameInfo = await game.getGameInfo()
|
||||
const steamUserdataDir = await getSteamUserdataDir()
|
||||
|
||||
@@ -79,7 +79,7 @@ ipcMain.handle(
|
||||
ipcMain.handle(
|
||||
'removeFromSteam',
|
||||
async (event, appName: string, runner: Runner) => {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const gameInfo = await game.getGameInfo()
|
||||
const steamUserdataDir = await getSteamUserdataDir()
|
||||
|
||||
@@ -90,7 +90,7 @@ ipcMain.handle(
|
||||
ipcMain.handle(
|
||||
'isAddedToSteam',
|
||||
async (event, appName: string, runner: Runner) => {
|
||||
const game = Game.get(appName, runner)
|
||||
const game = getGame(appName, runner)
|
||||
const gameInfo = await game.getGameInfo()
|
||||
const steamUserdataDir = await getSteamUserdataDir()
|
||||
|
||||
File renamed without changes.
+17
-11
@@ -7,7 +7,7 @@ import {
|
||||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'graceful-fs'
|
||||
import { readFileSync } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { GameInfo } from '../../types'
|
||||
import { GameInfo } from 'common/types'
|
||||
import { ShortcutsResult } from '../types'
|
||||
import { getIcon } from '../utils'
|
||||
import { notify } from '../../utils'
|
||||
@@ -65,11 +65,11 @@ function notifyFrontend(props: { message: string; adding: boolean }) {
|
||||
*/
|
||||
function checkSteamUserDataDir(steamUserdataDir: string): {
|
||||
folders: string[]
|
||||
error: string
|
||||
error?: string
|
||||
} {
|
||||
if (!existsSync(steamUserdataDir)) {
|
||||
return {
|
||||
folders: undefined,
|
||||
folders: [],
|
||||
error: `${steamUserdataDir} does not exist. Can't add/remove game to/from Steam!`
|
||||
}
|
||||
}
|
||||
@@ -83,12 +83,12 @@ function checkSteamUserDataDir(steamUserdataDir: string): {
|
||||
|
||||
if (folders.length <= 0) {
|
||||
return {
|
||||
folders: undefined,
|
||||
folders: [],
|
||||
error: `${steamUserdataDir} does not contain a valid user directory!`
|
||||
}
|
||||
}
|
||||
|
||||
return { folders, error: undefined }
|
||||
return { folders }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,11 +117,11 @@ function readShortcutFile(file: string): Partial<ShortcutObject> {
|
||||
function writeShortcutFile(
|
||||
file: string,
|
||||
object: Partial<ShortcutObject>
|
||||
): string {
|
||||
): string | undefined {
|
||||
const buffer = writeBuffer(object)
|
||||
try {
|
||||
writeFileSync(file, buffer)
|
||||
return undefined
|
||||
return
|
||||
} catch (error) {
|
||||
return `${error}`
|
||||
}
|
||||
@@ -164,8 +164,10 @@ function checkIfShortcutObjectIsValid(
|
||||
* @param title Title of the game
|
||||
* @returns Index of the found title, else if not found -1
|
||||
*/
|
||||
const checkIfAlreadyAdded = (object: Partial<ShortcutObject>, title: string) =>
|
||||
object.shortcuts.findIndex((entry) => entry.AppName === title)
|
||||
function checkIfAlreadyAdded(object: Partial<ShortcutObject>, title: string) {
|
||||
const shortcuts = object.shortcuts ?? []
|
||||
return shortcuts.findIndex((entry) => entry.AppName === title)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a non-steam game to steam via editing shortcuts.vdf
|
||||
@@ -206,6 +208,7 @@ async function addNonSteamGame(props: {
|
||||
|
||||
// read file
|
||||
const content = readShortcutFile(shortcutsFile)
|
||||
content.shortcuts = content.shortcuts ?? []
|
||||
|
||||
const checkResult = checkIfShortcutObjectIsValid(content)
|
||||
if (!checkResult.success) {
|
||||
@@ -375,15 +378,18 @@ async function removeNonSteamGame(props: {
|
||||
)
|
||||
continue
|
||||
}
|
||||
// This is just to make TS happy, in reality checkIfShortcutObjectIsValid already checks for this array
|
||||
content.shortcuts = content.shortcuts || []
|
||||
|
||||
const index = checkIfAlreadyAdded(content, props.gameInfo.title)
|
||||
|
||||
if (index < 0) {
|
||||
continue
|
||||
}
|
||||
const shortcutObj = content.shortcuts.at(index)!
|
||||
|
||||
const exe = content.shortcuts.at(index).Exe
|
||||
const appName = content.shortcuts.at(index).AppName
|
||||
const exe = shortcutObj.Exe
|
||||
const appName = shortcutObj.AppName
|
||||
|
||||
// remove
|
||||
content.shortcuts.splice(index, 1)
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { crc32 } from 'crc'
|
||||
import { existsSync, mkdirSync } from 'graceful-fs'
|
||||
import { join } from 'path'
|
||||
import { GameInfo } from '../../types'
|
||||
import { GameInfo } from 'common/types'
|
||||
import { logError, logInfo, LogPrefix } from '../../logger/logger'
|
||||
import {
|
||||
checkImageExistsAlready,
|
||||
+6
-4
@@ -2,9 +2,8 @@ import { app, shell } from 'electron'
|
||||
import { unlink, writeFile } from 'graceful-fs'
|
||||
import { logError, logInfo, LogPrefix } from '../../logger/logger'
|
||||
import { GlobalConfig } from '../../config'
|
||||
import { removeSpecialcharacters } from '../../utils'
|
||||
import { Game } from '../../games'
|
||||
import { Runner, GameInfo } from '../../types'
|
||||
import { getGame, removeSpecialcharacters } from '../../utils'
|
||||
import { Runner, GameInfo } from 'common/types'
|
||||
import { userHome } from '../../constants'
|
||||
import { GOGLibrary } from '../../gog/library'
|
||||
import { getIcon } from '../utils'
|
||||
@@ -23,6 +22,9 @@ async function addShortcuts(gameInfo: GameInfo, fromMenu?: boolean) {
|
||||
|
||||
const launchWithProtocol = `heroic://launch/${gameInfo.app_name}`
|
||||
const [desktopFile, menuFile] = shortcutFiles(gameInfo.title)
|
||||
if (!desktopFile || !menuFile) {
|
||||
return
|
||||
}
|
||||
const { addDesktopShortcuts, addStartMenuShortcuts } =
|
||||
await GlobalConfig.get().getSettings()
|
||||
|
||||
@@ -88,7 +90,7 @@ Categories=Game;
|
||||
* @public
|
||||
*/
|
||||
async function removeShortcuts(appName: string, runner: Runner) {
|
||||
const gameInfo = Game.get(appName, runner).getGameInfo()
|
||||
const gameInfo = getGame(appName, runner).getGameInfo()
|
||||
const [desktopFile, menuFile] = shortcutFiles(gameInfo.title)
|
||||
|
||||
if (desktopFile) {
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ShortcutsResult {
|
||||
success: boolean
|
||||
errors: string[]
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'graceful-fs'
|
||||
import { GOGLibrary } from '../gog/library'
|
||||
import { heroicIconFolder } from '../constants'
|
||||
import { GameInfo } from '../types'
|
||||
import { GameInfo } from 'common/types'
|
||||
import { spawnSync } from 'child_process'
|
||||
import { basename, dirname, extname, join } from 'path'
|
||||
|
||||
function createImage(buffer: Buffer, outputFilePath: string): string {
|
||||
function createImage(
|
||||
buffer: Buffer,
|
||||
outputFilePath: string
|
||||
): string | undefined {
|
||||
try {
|
||||
writeFileSync(outputFilePath, buffer, {
|
||||
encoding: 'ascii'
|
||||
@@ -13,25 +16,28 @@ function createImage(buffer: Buffer, outputFilePath: string): string {
|
||||
} catch (error) {
|
||||
return `${error}`
|
||||
}
|
||||
return undefined
|
||||
return
|
||||
}
|
||||
|
||||
function downloadImage(imageURL: string, outputFilePath: string): string {
|
||||
function downloadImage(
|
||||
imageURL: string,
|
||||
outputFilePath: string
|
||||
): string | undefined {
|
||||
try {
|
||||
spawnSync('curl', ['-L', imageURL, '-o', outputFilePath])
|
||||
} catch (error) {
|
||||
return `Donwloading of ${imageURL} failed with:\n${error}`
|
||||
}
|
||||
return undefined
|
||||
return
|
||||
}
|
||||
|
||||
function removeImage(imagePath: string): string {
|
||||
function removeImage(imagePath: string): string | undefined {
|
||||
try {
|
||||
unlinkSync(imagePath)
|
||||
} catch (error) {
|
||||
return `Removing of ${imagePath} failed with:\n${error}`
|
||||
}
|
||||
return undefined
|
||||
return
|
||||
}
|
||||
|
||||
function checkImageExistsAlready(image: string): boolean {
|
||||
@@ -63,7 +69,7 @@ async function getIcon(appName: string, gameInfo: GameInfo) {
|
||||
downloadImage(image, icon)
|
||||
}
|
||||
return icon
|
||||
} else if (gameInfo.runner === 'gog') {
|
||||
} else {
|
||||
const apiData = await GOGLibrary.get().getGamesData(appName)
|
||||
let iconUrl = apiData?._links?.icon.href
|
||||
iconUrl = iconUrl.replace('{ext}', 'png')
|
||||
@@ -1,6 +1,5 @@
|
||||
import { WineInstallation } from './types'
|
||||
import { WineInstallation } from 'common/types'
|
||||
import * as axios from 'axios'
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { existsSync, readFileSync, writeFileSync } from 'graceful-fs'
|
||||
import { exec, spawn } from 'child_process'
|
||||
|
||||
Loaded 100 of 376 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user