diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 95ab1157db..85a12ae575 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -12,7 +12,8 @@ const srcAliases = ['backend', 'frontend', 'common'].map((aliasName) => ({ const dependenciesToNotExternalize = [ '@xhmikosr/decompress', - '@xhmikosr/decompress-targz' + '@xhmikosr/decompress-targz', + '@xhmikosr/decompress-unzip' ] // FIXME: Potentially publish this as a dedicated plugin, if other projects diff --git a/meta/downloadHelperBinaries.ts b/meta/downloadHelperBinaries.ts index 915c5ebcc6..36d8406d59 100644 --- a/meta/downloadHelperBinaries.ts +++ b/meta/downloadHelperBinaries.ts @@ -5,12 +5,13 @@ import { Readable } from 'stream' import { finished } from 'stream/promises' type SupportedPlatform = 'win32' | 'darwin' | 'linux' -type DownloadedBinary = 'legendary' | 'gogdl' | 'nile' +type DownloadedBinary = 'legendary' | 'gogdl' | 'nile' | 'comet' const RELEASE_TAGS = { legendary: '0.20.35', gogdl: 'v1.1.1', - nile: 'v1.1.0' + nile: 'v1.1.0', + comet: 'v0.1.2' } as const satisfies Record const pathExists = async (path: string): Promise => @@ -137,6 +138,24 @@ async function downloadNile() { }) } +async function downloadComet() { + return downloadGithubAssets( + 'comet', + 'imLinguin/comet', + RELEASE_TAGS['comet'], + { + x64: { + linux: 'comet-x86_64-unknown-linux-gnu', + darwin: 'comet-x86_64-apple-darwin', + win32: 'comet-x86_64-pc-windows-msvc.exe' + }, + arm64: { + darwin: 'comet-aarch64-apple-darwin' + } + } + ) +} + /** * Finds out which binaries need to be downloaded by comparing * `public/bin/.release_tags` to RELEASE_TAGS @@ -150,7 +169,7 @@ async function compareDownloadedTags(): Promise { try { storedTagsParsed = JSON.parse(storedTagsText) } catch { - return ['legendary', 'gogdl', 'nile'] + return ['legendary', 'gogdl', 'nile', 'comet'] } const binariesToDownload: DownloadedBinary[] = [] for (const [runner, currentTag] of Object.entries(RELEASE_TAGS)) { @@ -184,6 +203,8 @@ async function main() { if (binariesToDownload.includes('gogdl')) promisesToAwait.push(downloadGogdl()) if (binariesToDownload.includes('nile')) promisesToAwait.push(downloadNile()) + if (binariesToDownload.includes('comet')) + promisesToAwait.push(downloadComet()) await Promise.all(promisesToAwait) diff --git a/package.json b/package.json index 03c6aa337e..14f1b69fe8 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@shockpkg/icon-encoder": "2.1.3", "@xhmikosr/decompress": "9.0.1", "@xhmikosr/decompress-targz": "7.0.0", + "@xhmikosr/decompress-unzip": "^7.0.0", "axios": "0.26.1", "classnames": "2.3.1", "compare-versions": "6.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 524a9079a7..c25843e2ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@xhmikosr/decompress-targz': specifier: 7.0.0 version: 7.0.0 + '@xhmikosr/decompress-unzip': + specifier: ^7.0.0 + version: 7.0.0 axios: specifier: 0.26.1 version: 0.26.1 @@ -1900,6 +1903,10 @@ packages: resolution: {integrity: sha512-R1HAkjXLS7RAL74YFLxYY9zYflCcYGssld9KKFDu87PnJ4h4btdhzXfSC8J5i5A2njH3oYIoCzx03RIGTH07Sg==} engines: {node: ^14.14.0 || >=16.0.0} + '@xhmikosr/decompress-unzip@7.0.0': + resolution: {integrity: sha512-GQMpzIpWTsNr6UZbISawsGI0hJ4KA/mz5nFq+cEoPs12UybAqZWKbyIaZZyLbJebKl5FkLpsGBkrplJdjvUoSQ==} + engines: {node: '>=18'} + '@xhmikosr/decompress@9.0.1': resolution: {integrity: sha512-9Lvlt6Qdpo9SaRQyRIXCo3lgU++eMZ68lzgjcTwtuKDrlwT635+5zsHZ1yrSx/Blc5IDuVLlPkBPj5CZkx+2+Q==} engines: {node: ^14.14.0 || >=16.0.0} @@ -3143,6 +3150,10 @@ packages: resolution: {integrity: sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==} engines: {node: '>=14.16'} + file-type@19.3.0: + resolution: {integrity: sha512-mROwiKLZf/Kwa/2Rol+OOZQn1eyTkPB3ZTwC0ExY6OLFCbgxHYZvBm7xI77NvfZFMKBsmuXfmLJnD4eEftEhrA==} + engines: {node: '>=18'} + file-type@5.2.0: resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==} engines: {node: '>=4'} @@ -4641,6 +4652,10 @@ packages: resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==} engines: {node: '>=14.16'} + peek-readable@5.1.3: + resolution: {integrity: sha512-kCsc9HwH5RgVA3H3VqkWFyGQwsxUxLdiSX1d5nqAm7hnMFjNFX1VhBLmJoUY0hZNc8gmDNgBkLjfhiWPsziXWA==} + engines: {node: '>=14.16'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -5390,6 +5405,10 @@ packages: resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==} engines: {node: '>=14.16'} + strtok3@8.0.1: + resolution: {integrity: sha512-HNkTAnNWQj2YBzfTtoC5OQyu1QwPsMwiB7VyQmNvQKCrmEDSvFB857Vh97UY9InGLNRAB91sdS1ztifRo/3hdA==} + engines: {node: '>=16'} + style-to-object@0.4.4: resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} @@ -5514,6 +5533,10 @@ packages: resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} engines: {node: '>=14.16'} + token-types@6.0.0: + resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} + engines: {node: '>=14.16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -5641,6 +5664,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + uint8array-extras@1.4.0: + resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} + engines: {node: '>=18'} + unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -5965,6 +5992,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@3.1.3: + resolution: {integrity: sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -7586,6 +7617,12 @@ snapshots: get-stream: 6.0.1 yauzl: 2.10.0 + '@xhmikosr/decompress-unzip@7.0.0': + dependencies: + file-type: 19.3.0 + get-stream: 6.0.1 + yauzl: 3.1.3 + '@xhmikosr/decompress@9.0.1': dependencies: '@xhmikosr/decompress-tar': 7.0.0 @@ -7678,7 +7715,7 @@ snapshots: app-builder-bin@4.0.0: {} - app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): + app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): dependencies: '@develar/schema-utils': 2.6.5 '@electron/notarize': 2.2.1 @@ -7692,7 +7729,7 @@ snapshots: builder-util-runtime: 9.2.4 chromium-pickle-js: 0.2.0 debug: 4.3.4 - dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) + dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) ejs: 3.1.9 electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3) electron-publish: 24.13.1 @@ -8571,9 +8608,9 @@ snapshots: - encoding - utf-8-validate - dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3): + dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): dependencies: - app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) + app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) builder-util: 24.13.1 builder-util-runtime: 9.2.4 fs-extra: 10.1.0 @@ -8670,7 +8707,7 @@ snapshots: electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3): dependencies: - app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) + app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) archiver: 5.3.2 builder-util: 24.13.1 fs-extra: 10.1.0 @@ -8680,11 +8717,11 @@ snapshots: electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): dependencies: - app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) + app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) builder-util: 24.13.1 builder-util-runtime: 9.2.4 chalk: 4.1.2 - dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) + dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) fs-extra: 10.1.0 is-ci: 3.0.1 lazy-val: 1.0.5 @@ -9216,6 +9253,12 @@ snapshots: strtok3: 7.0.0 token-types: 5.0.1 + file-type@19.3.0: + dependencies: + strtok3: 8.0.1 + token-types: 6.0.0 + uint8array-extras: 1.4.0 + file-type@5.2.0: {} file-uri-to-path@1.0.0: @@ -10987,6 +11030,8 @@ snapshots: peek-readable@5.0.0: {} + peek-readable@5.1.3: {} + pend@1.2.0: {} picocolors@1.0.0: {} @@ -11855,6 +11900,11 @@ snapshots: '@tokenizer/token': 0.3.0 peek-readable: 5.0.0 + strtok3@8.0.1: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.1.3 + style-to-object@0.4.4: dependencies: inline-style-parser: 0.1.1 @@ -11998,6 +12048,11 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + token-types@6.0.0: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + tr46@0.0.3: {} trim-lines@3.0.1: {} @@ -12120,6 +12175,8 @@ snapshots: typescript@5.4.3: {} + uint8array-extras@1.4.0: {} + unbox-primitive@1.0.2: dependencies: call-bind: 1.0.7 @@ -12533,6 +12590,11 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yauzl@3.1.3: + dependencies: + buffer-crc32: 0.2.13 + pend: 1.2.0 + yocto-queue@0.1.0: {} zip-stream@4.1.1: diff --git a/public/bin/.gitignore b/public/bin/.gitignore index 4e897d46c0..7e6ab417e0 100644 --- a/public/bin/.gitignore +++ b/public/bin/.gitignore @@ -4,4 +4,6 @@ **/gogdl.exe **/nile **/nile.exe -.release_tags \ No newline at end of file +**/comet +**/comet.exe +.release_tags diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index d7b46e40bf..c45f27f1fb 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -634,6 +634,7 @@ "exit-to-tray": "Exit to System Tray", "experimental_features": { "automaticWinetricksFixes": "Apply known fixes automatically", + "cometSupport": "Comet support", "enableHelp": "Help component", "enableNewDesign": "New design", "umuSupport": "Use UMU as Proton runtime" @@ -786,6 +787,7 @@ "warning": "Cloud Saves feature is in Beta, please backup your saves before syncing (in case something goes wrong)" }, "systemInformation": { + "cometVersion": "Comet: {{cometVersion}}", "copyToClipboard": "Copy to clipboard", "cpu": "CPU:", "cpuDescription": "{{numOfCores}}x {{modelName}}", diff --git a/src/backend/__mocks__/@xhmikosr/decompress-unzip.ts b/src/backend/__mocks__/@xhmikosr/decompress-unzip.ts new file mode 100644 index 0000000000..336ce12bb9 --- /dev/null +++ b/src/backend/__mocks__/@xhmikosr/decompress-unzip.ts @@ -0,0 +1 @@ +export {} diff --git a/src/backend/api/settings.ts b/src/backend/api/settings.ts index ac7e6a7aad..7adc7bca60 100644 --- a/src/backend/api/settings.ts +++ b/src/backend/api/settings.ts @@ -17,6 +17,7 @@ export const setSetting = (args: { export const getLegendaryVersion = async () => ipcRenderer.invoke('getLegendaryVersion') export const getGogdlVersion = async () => ipcRenderer.invoke('getGogdlVersion') +export const getCometVersion = async () => ipcRenderer.invoke('getCometVersion') export const getNileVersion = async () => ipcRenderer.invoke('getNileVersion') export const getEosOverlayStatus = async () => ipcRenderer.invoke('getEosOverlayStatus') diff --git a/src/backend/launcher.ts b/src/backend/launcher.ts index 580da0bb5d..332c31dfd6 100644 --- a/src/backend/launcher.ts +++ b/src/backend/launcher.ts @@ -344,6 +344,8 @@ async function prepareWineLaunch( } const { updated: winePrefixUpdated } = await verifyWinePrefix(gameSettings) + const experimentalFeatures = + GlobalConfig.get().getSettings().experimentalFeatures if (winePrefixUpdated) { logInfo( ['Created/Updated Wineprefix at', gameSettings.winePrefix], @@ -363,14 +365,33 @@ async function prepareWineLaunch( if (runner === 'legendary') { await legendarySetup(appName) } - if ( - GlobalConfig.get().getSettings().experimentalFeatures - ?.automaticWinetricksFixes !== false - ) { + if (experimentalFeatures?.automaticWinetricksFixes !== false) { await installFixes(appName, runner) } } + if (runner === 'gog' && experimentalFeatures?.cometSupport !== false) { + if (isOnline() && !(await isInstalled('comet_dummy_service'))) { + await download('comet_dummy_service') + } + const installerScript = join( + runtimePath, + 'comet_dummy_service', + 'install-dummy-service.bat' + ) + if (existsSync(installerScript)) { + await runWineCommand({ + commandParts: [installerScript], + gameSettings, + protonVerb: 'runinprefix' + }) + } else { + logWarning( + "Comet dummy service isn't downloaded, online functionality may not work" + ) + } + } + // If DXVK/VKD3D installation is enabled, install it if (gameSettings.wineVersion.type === 'wine') { if (gameSettings.autoInstallDxvk) { diff --git a/src/backend/main.ts b/src/backend/main.ts index 73efc4c0a3..cde2f4110a 100644 --- a/src/backend/main.ts +++ b/src/backend/main.ts @@ -44,6 +44,7 @@ import { GameConfig } from './game_config' import { GlobalConfig } from './config' import { LegendaryUser } from 'backend/storeManagers/legendary/user' import { GOGUser } from './storeManagers/gog/user' +import gogPresence from './storeManagers/gog/presence' import { NileUser } from './storeManagers/nile/user' import { clearCache, @@ -360,6 +361,7 @@ if (!gotTheLock) { prefix: LogPrefix.Backend }) } + runOnceWhenOnline(gogPresence.setPresence) await i18next.use(Backend).init({ backend: { addPath: path.join(publicDir, 'locales', '{{lng}}', '{{ns}}'), @@ -1050,6 +1052,11 @@ ipcMain.handle( skipVersionCheck ) + if (runner === 'gog') { + gogPresence.setCurrentGame(appName) + await gogPresence.setPresence() + } + const launchResult = await command .catch((exception) => { logError(exception, LogPrefix.Backend) @@ -1065,6 +1072,10 @@ ipcMain.handle( stopLogger(appName) }) + if (runner === 'gog') { + gogPresence.setCurrentGame('') + await gogPresence.setPresence() + } // Stop display sleep blocker if (powerDisplayId !== null) { logInfo('Stopping Display Power Saver Blocker', LogPrefix.Backend) diff --git a/src/backend/storeManagers/gog/games.ts b/src/backend/storeManagers/gog/games.ts index c0441da6f6..94630881a6 100644 --- a/src/backend/storeManagers/gog/games.ts +++ b/src/backend/storeManagers/gog/games.ts @@ -24,7 +24,8 @@ import { shutdownWine, sendProgressUpdate, sendGameStatusUpdate, - getPathDiskSize + getPathDiskSize, + getCometBin } from '../../utils' import { ExtraInfo, @@ -55,6 +56,7 @@ import { } from './electronStores' import { appendGamePlayLog, + appendRunnerLog, appendWinetricksGamePlayLog, logDebug, logError, @@ -104,6 +106,7 @@ import { readdir, readFile } from 'fs/promises' import { statSync } from 'fs' import ini from 'ini' import { getRequiredRedistList, updateRedist } from './redist' +import { spawn } from 'child_process' import { getUmuId } from 'backend/wiki_game_info/umu/utils' export async function getExtraInfo(appName: string): Promise { @@ -679,8 +682,34 @@ export async function launch( ) appendGamePlayLog(gameInfo, `Launch Command: ${fullCommand}\n\nGame Log:\n`) + const userData: UserData | undefined = configStore.get_nodefault('userData') + sendGameStatusUpdate({ appName, runner: 'gog', status: 'playing' }) + let child = undefined + + if ( + userData && + userData.username && + GlobalConfig.get().getSettings().experimentalFeatures?.cometSupport !== + false + ) { + const path = getCometBin() + child = spawn(join(path.dir, path.bin), [ + '--from-heroic', + '--username', + userData.username, + '--quit' + ]) + child.stdout.on('data', (data) => { + appendRunnerLog('gog', data.toString()) + }) + child.stderr.on('data', (data) => { + appendRunnerLog('gog', data.toString()) + }) + logInfo(`Launching Comet!`, LogPrefix.Gog) + } + const { error, abort } = await runGogdlCommand(commandParts, { abortId: appName, env: commandEnv, @@ -691,6 +720,10 @@ export async function launch( } }) + if (child) { + logInfo(`Killing Comet!`, LogPrefix.Gog) + child.kill() + } launchCleanup(rpcClient) if (abort) { diff --git a/src/backend/storeManagers/gog/presence.ts b/src/backend/storeManagers/gog/presence.ts new file mode 100644 index 0000000000..e05c385205 --- /dev/null +++ b/src/backend/storeManagers/gog/presence.ts @@ -0,0 +1,83 @@ +import { app } from 'electron' +import { logError, logInfo, LogPrefix } from 'backend/logger/logger' +import { axiosClient } from 'backend/utils' +import { GOGUser } from './user' +import { isOnline } from 'backend/online_monitor' +import { GlobalConfig } from 'backend/config' + +interface PresencePayload { + application_type: string + force_update: boolean + presence: 'online' | 'offline' + version: string + game_id?: string +} + +let CURRENT_GAME = '' +let interval: NodeJS.Timeout + +function setCurrentGame(game: string) { + CURRENT_GAME = game +} + +async function setPresence() { + try { + const { disablePlaytimeSync } = GlobalConfig.get().getSettings() + if (disablePlaytimeSync || !GOGUser.isLoggedIn() || !isOnline()) return + const credentials = await GOGUser.getCredentials() + if (!credentials) return + + if (!interval) { + interval = setInterval(setPresence, 5 * 60 * 1000) + } + + const payload: PresencePayload = { + application_type: 'Heroic Games Launcher', + force_update: false, + presence: 'online', + version: app.getVersion(), + game_id: undefined + } + + if (CURRENT_GAME !== '') { + payload.game_id = CURRENT_GAME + } + + const response = await axiosClient.post( + `https://presence.gog.com/users/${credentials.user_id}/status`, + payload, + { headers: { Authorization: `Bearer ${credentials.access_token}` } } + ) + if (response.status === 204) { + logInfo('GOG presence set', LogPrefix.Gog) + } + } catch (e) { + logError(['Failed to set gog presence', e], LogPrefix.Gog) + } +} + +async function deletePresence() { + try { + if (!GOGUser.isLoggedIn() || !isOnline()) return + const credentials = await GOGUser.getCredentials() + if (!credentials) { + return + } + clearInterval(interval) + const response = await axiosClient.delete( + `https://presence.gog.com/users/${credentials.user_id}/status`, + { headers: { Authorization: `Bearer ${credentials.access_token}` } } + ) + if (response.status === 204) { + logInfo('GOG presence deleted', LogPrefix.Gog) + } + } catch (e) { + logError(['Failed to delete gog presence', e], LogPrefix.Gog) + } +} + +export default { + setCurrentGame, + setPresence, + deletePresence +} diff --git a/src/backend/utils.ts b/src/backend/utils.ts index b91f07d7b8..712a1ab5cc 100644 --- a/src/backend/utils.ts +++ b/src/backend/utils.ts @@ -57,6 +57,7 @@ import { installInfoStore as GOGinstallInfoStore, libraryStore as GOGlibraryStore } from './storeManagers/gog/electronStores' +import gogPresence from './storeManagers/gog/presence' import { installStore as nileInstallStore, libraryStore as nileLibraryStore @@ -83,6 +84,7 @@ import EasyDl from 'easydl' import decompress from '@xhmikosr/decompress' import decompressTargz from '@xhmikosr/decompress-targz' import decompressTarxz from '@felipecrs/decompress-tarxz' +import decompressUnzip from '@xhmikosr/decompress-unzip' import { deviceNameCache, vendorNameCache @@ -243,6 +245,8 @@ async function handleExit() { const isLocked = existsSync(join(gamesConfigPath, 'lock')) const mainWindow = getMainWindow() + await gogPresence.deletePresence() + if (isLocked && mainWindow) { const { response } = await showMessageBox(mainWindow, { buttons: [i18next.t('box.no'), i18next.t('box.yes')], @@ -486,6 +490,18 @@ function getGOGdlBin(): { dir: string; bin: string } { return splitPathAndName(fixAsarPath(defaultGogdlPath)) } +let defaultCometPath: string | undefined = undefined +function getCometBin(): { dir: string; bin: string } { + const settings = GlobalConfig.get().getSettings() + if (settings?.altCometBin) { + return splitPathAndName(settings.altCometBin) + } + + if (!defaultCometPath) defaultCometPath = archSpecificBinary('comet') + + return splitPathAndName(fixAsarPath(defaultCometPath)) +} + let defaultNilePath: string | undefined = undefined function getNileBin(): { dir: string; bin: string } { const settings = GlobalConfig.get().getSettings() @@ -1519,7 +1535,7 @@ async function extractDecompress( ) try { await decompress(path, destination, { - plugins: [decompressTargz(), decompressTarxz()], + plugins: [decompressTargz(), decompressTarxz(), decompressUnzip()], strip }) } catch (error) { @@ -1546,6 +1562,7 @@ export { resetHeroic, getLegendaryBin, getGOGdlBin, + getCometBin, getNileBin, formatEpicStoreUrl, getSteamRuntime, diff --git a/src/backend/utils/helperBinaries/index.ts b/src/backend/utils/helperBinaries/index.ts index 592fc01f6c..45cfa54808 100644 --- a/src/backend/utils/helperBinaries/index.ts +++ b/src/backend/utils/helperBinaries/index.ts @@ -1,6 +1,9 @@ import { runRunnerCommand as runLegendaryCommand } from '../../storeManagers/legendary/library' import { runRunnerCommand as runGogdlCommand } from '../../storeManagers/gog/library' import { runRunnerCommand as runNileCommand } from '../../storeManagers/nile/library' +import { spawnSync } from 'node:child_process' +import { getCometBin } from 'backend/utils' +import { join } from 'path' async function getLegendaryVersion(): Promise { const { stdout, error, abort } = await runLegendaryCommand( @@ -35,6 +38,15 @@ async function getGogdlVersion(): Promise { return stdout } +async function getCometVersion(): Promise { + const path = getCometBin() + const { stdout, error } = spawnSync(join(path.dir, path.bin), ['--version']) + + if (error) return 'invalid' + + return stdout.toString() +} + async function getNileVersion(): Promise { const { stdout, error } = await runNileCommand(['--version'], { abortId: 'nile-version' @@ -45,4 +57,4 @@ async function getNileVersion(): Promise { return stdout } -export { getLegendaryVersion, getGogdlVersion, getNileVersion } +export { getLegendaryVersion, getGogdlVersion, getNileVersion, getCometVersion } diff --git a/src/backend/utils/ipc_handler.ts b/src/backend/utils/ipc_handler.ts index b6ea12da00..11233c8891 100644 --- a/src/backend/utils/ipc_handler.ts +++ b/src/backend/utils/ipc_handler.ts @@ -1,6 +1,7 @@ import { clipboard, ipcMain } from 'electron' import { callAbortController } from './aborthandler/aborthandler' import { + getCometVersion, getGogdlVersion, getLegendaryVersion, getNileVersion @@ -13,6 +14,7 @@ ipcMain.on('abort', async (event, id) => { }) ipcMain.handle('getLegendaryVersion', getLegendaryVersion) ipcMain.handle('getGogdlVersion', getGogdlVersion) +ipcMain.handle('getCometVersion', getCometVersion) ipcMain.handle('getNileVersion', getNileVersion) ipcMain.handle('getSystemInfo', async (e, cache) => getSystemInfo(cache)) ipcMain.on('copySystemInfoToClipboard', async () => diff --git a/src/backend/utils/systeminfo/index.ts b/src/backend/utils/systeminfo/index.ts index e3ef9e0dc2..37a0762047 100644 --- a/src/backend/utils/systeminfo/index.ts +++ b/src/backend/utils/systeminfo/index.ts @@ -12,6 +12,7 @@ import { getOsInfo } from './osInfo' import { getSteamDeckInfo, type SteamDeckInfo } from './steamDeck' import { getHeroicVersion } from './heroicVersion' import { + getCometVersion, getGogdlVersion, getLegendaryVersion, getNileVersion @@ -58,6 +59,7 @@ interface SystemInformation { heroicVersion: string legendaryVersion: string gogdlVersion: string + cometVersion: string nileVersion: string } } @@ -76,11 +78,13 @@ async function getSystemInfo(cache = true): Promise { const gpus = await getGpuInfo() const detailedOsInfo = await getOsInfo() const deckInfo = getSteamDeckInfo(cpus, gpus) - const [legendaryVersion, gogdlVersion, nileVersion] = await Promise.all([ - getLegendaryVersion(), - getGogdlVersion(), - getNileVersion() - ]) + const [legendaryVersion, gogdlVersion, cometVersion, nileVersion] = + await Promise.all([ + getLegendaryVersion(), + getGogdlVersion(), + getCometVersion(), + getNileVersion() + ]) const sysinfo: SystemInformation = { CPU: { @@ -107,6 +111,7 @@ async function getSystemInfo(cache = true): Promise { heroicVersion: getHeroicVersion(), legendaryVersion: legendaryVersion, gogdlVersion: gogdlVersion, + cometVersion: cometVersion, nileVersion: nileVersion } } @@ -137,6 +142,7 @@ Software Versions: Heroic: ${info.softwareInUse.heroicVersion} Legendary: ${info.softwareInUse.legendaryVersion} gogdl: ${info.softwareInUse.gogdlVersion} + comet: ${info.softwareInUse.cometVersion} Nile: ${info.softwareInUse.nileVersion}` } diff --git a/src/backend/wine/manager/downloader/__tests__/utilities/unzip.test.ts b/src/backend/wine/manager/downloader/__tests__/utilities/unzip.test.ts index 31e439767e..74dc586cb0 100644 --- a/src/backend/wine/manager/downloader/__tests__/utilities/unzip.test.ts +++ b/src/backend/wine/manager/downloader/__tests__/utilities/unzip.test.ts @@ -7,6 +7,10 @@ jest.mock('@xhmikosr/decompress', () => { return jest.fn().mockImplementation(async () => Promise.resolve()) }) +jest.mock('@xhmikosr/decompress-unzip', () => { + return jest.fn().mockImplementation(() => {}) +}) + jest.mock('@xhmikosr/decompress-targz', () => { return jest.fn().mockImplementation(() => {}) }) diff --git a/src/backend/wine/runtimes/runtimes.ts b/src/backend/wine/runtimes/runtimes.ts index 543aefcbf5..6557da8534 100644 --- a/src/backend/wine/runtimes/runtimes.ts +++ b/src/backend/wine/runtimes/runtimes.ts @@ -19,7 +19,15 @@ async function _get(): Promise { if (!allRuntimes.data) { logError('Failed to fetch runtime list', LogPrefix.Runtime) } - return allRuntimes.data || [] + const runtimes: Runtime[] = allRuntimes.data || [] + runtimes.push({ + id: 2000, + name: 'comet_dummy_service', + architecture: 'all', + created_at: '2024-07-27T19:35:07.389453Z', + url: 'https://github.com/imLinguin/comet/releases/download/v0.1.2/dummy-service.zip' + }) + return runtimes } async function download(name: RuntimeName): Promise { diff --git a/src/common/typedefs/decompress.d.ts b/src/common/typedefs/decompress.d.ts index 681eaf4b86..aa47a2c4e4 100644 --- a/src/common/typedefs/decompress.d.ts +++ b/src/common/typedefs/decompress.d.ts @@ -23,3 +23,4 @@ declare module '@xhmikosr/decompress' { declare module '@xhmikosr/decompress-targz' declare module '@felipecrs/decompress-tarxz' +declare module '@xhmikosr/decompress-unzip' diff --git a/src/common/typedefs/ipcBridge.d.ts b/src/common/typedefs/ipcBridge.d.ts index 440040d514..004c7ddff6 100644 --- a/src/common/typedefs/ipcBridge.d.ts +++ b/src/common/typedefs/ipcBridge.d.ts @@ -141,6 +141,7 @@ interface AsyncIPCFunctions { getHeroicVersion: () => string getLegendaryVersion: () => Promise getGogdlVersion: () => Promise + getCometVersion: () => Promise getNileVersion: () => Promise isFullscreen: () => boolean isFrameless: () => boolean diff --git a/src/common/types.ts b/src/common/types.ts index 0e7235a190..4525603400 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -65,6 +65,7 @@ export type ExperimentalFeatures = { enableNewDesign: boolean enableHelp: boolean automaticWinetricksFixes: boolean + cometSupport: boolean umuSupport: boolean } @@ -73,6 +74,7 @@ export interface AppSettings extends GameSettings { addStartMenuShortcuts: boolean addSteamShortcuts: boolean altGogdlBin: string + altCometBin: string altLegendaryBin: string altNileBin: string autoUpdateGames: boolean @@ -474,7 +476,11 @@ export interface Runtime { url: string } -export type RuntimeName = 'eac_runtime' | 'battleye_runtime' | 'umu' +export type RuntimeName = + | 'eac_runtime' + | 'battleye_runtime' + | 'comet_dummy_service' + | 'umu' export type RecentGame = { appName: string diff --git a/src/frontend/screens/Settings/components/ExperimentalFeatures.tsx b/src/frontend/screens/Settings/components/ExperimentalFeatures.tsx index ec62bfd0aa..18978204dd 100644 --- a/src/frontend/screens/Settings/components/ExperimentalFeatures.tsx +++ b/src/frontend/screens/Settings/components/ExperimentalFeatures.tsx @@ -7,7 +7,7 @@ import ContextProvider from 'frontend/state/ContextProvider' const ExperimentalFeatures = () => { const { platform } = useContext(ContextProvider) - const FEATURES = ['enableNewDesign', 'enableHelp'] + const FEATURES = ['enableNewDesign', 'enableHelp', 'cometSupport'] if (platform !== 'win32') { FEATURES.push('automaticWinetricksFixes') @@ -24,6 +24,7 @@ const ExperimentalFeatures = () => { enableNewDesign: false, enableHelp: false, automaticWinetricksFixes: true, + cometSupport: true, umuSupport: false } ) @@ -43,6 +44,7 @@ const ExperimentalFeatures = () => { t('setting.experimental_features.enableNewDesign', 'New design') t('setting.experimental_features.enableHelp', 'Help component') t('setting.experimental_features.automaticWinetricksFixes', 'Apply known fixes automatically') + t('setting.experimental_features.cometSupport', 'Comet support') t('setting.experimental_features.umuSupport', 'Use UMU as Proton runtime') */ diff --git a/src/frontend/screens/Settings/sections/SystemInfo/software.tsx b/src/frontend/screens/Settings/sections/SystemInfo/software.tsx index e4f253e5bb..1d0405b823 100644 --- a/src/frontend/screens/Settings/sections/SystemInfo/software.tsx +++ b/src/frontend/screens/Settings/sections/SystemInfo/software.tsx @@ -16,8 +16,13 @@ interface Props { function SoftwareInfo({ software }: Props) { const { t } = useTranslation() - const { heroicVersion, legendaryVersion, gogdlVersion, nileVersion } = - software + const { + heroicVersion, + legendaryVersion, + gogdlVersion, + cometVersion, + nileVersion + } = software return ( @@ -51,6 +56,14 @@ function SoftwareInfo({ software }: Props) { } )}
+ {t( + 'settings.systemInformation.cometVersion', + 'Comet: {{cometVersion}}', + { + cometVersion + } + )} +
{t( 'settings.systemInformation.nileVersion', 'Nile: {{nileVersion}}', diff --git a/src/frontend/state/ContextProvider.tsx b/src/frontend/state/ContextProvider.tsx index 88c7e22d3c..e9f7e59347 100644 --- a/src/frontend/state/ContextProvider.tsx +++ b/src/frontend/state/ContextProvider.tsx @@ -98,6 +98,7 @@ const initialContext: ContextType = { enableNewDesign: false, enableHelp: false, automaticWinetricksFixes: true, + cometSupport: true, umuSupport: false }, handleExperimentalFeatures: () => null, diff --git a/src/frontend/state/GlobalState.tsx b/src/frontend/state/GlobalState.tsx index 5d327a6a3f..3b5facdcb6 100644 --- a/src/frontend/state/GlobalState.tsx +++ b/src/frontend/state/GlobalState.tsx @@ -211,6 +211,7 @@ class GlobalState extends PureComponent { enableNewDesign: false, enableHelp: false, automaticWinetricksFixes: true, + cometSupport: true, umuSupport: false, ...(globalSettings?.experimentalFeatures || {}) },