[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:
Mathis Dröge authored and GitHub committed 2022-08-25 19:23:42 +02:00
1 parent a9849928b0
commit 2ba6604f70
376 files changed
+3367 -8678

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
public/start-react.js
i18next-parser.config.js
flatpak/build/
flatpak/.flatpak-builder/
vite.config.ts
**/__tests__/**
+2 -1
View File
@@ -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",
+3
View File
@@ -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:
+3
View File
@@ -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:
+3
View File
@@ -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 }}
+1
View File
@@ -18,3 +18,4 @@ jobs:
- run: npm run release:mac
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
+2
View File
@@ -33,4 +33,6 @@ downloads*.json
flatpak/build
.flatpak-builder
vite-plugin-electron.log
.tool-versions
-1
View File
@@ -2,6 +2,5 @@ node_modules
dist
build
coverage
public/index.html
public/locales/
flatpak
-2
View File
@@ -1,2 +0,0 @@
react: npm run react-start
electron: npm run electron-start
-587
View File
@@ -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
)
}
-91
View File
@@ -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 }
}
}
-6
View File
@@ -1,6 +0,0 @@
interface ShortcutsResult {
success: boolean
errors: string[]
}
export { ShortcutsResult }
-16
View File
@@ -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__"]
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
-43
View File
@@ -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>
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
+2 -13
View File
@@ -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": {
-26
View File
@@ -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.
@@ -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')
@@ -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
+7 -2
View File
@@ -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}.`,
+12 -23
View File
@@ -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.
+162 -118
View File
@@ -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,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
@@ -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(
+554
View File
@@ -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
)
}
+78
View File
@@ -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.
+107 -99
View File
@@ -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()
@@ -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,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,
@@ -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) {
+4
View File
@@ -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 -2
View File
@@ -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