Skip to content

Commit f076cc2

Browse files
flavioislimahellowodlBrettClearyLinux4Good
authored
[Tech] download and install hyperplay game (#152)
* types: add valist types * feat: add electron stores for library and install * wip: download and install app * other: use electron-dl instead of axios * feat: store installed game as sideloaded app * feat: add uninstall method * other: improve sideloaded types * feat: complemented install functions feat: implemented install in store API feat: exposed store API to store webview * feat: added download manager integration * fix: types and lint * other: define hp own methods * tech: replace electron-dl with axios * feat: add uninstall and shortcuts api handlers * other: add more methods and ipc calls * more fixes and types * update test store url * fix types and other errors * prettier * fix hp web games not installed and not available * fix browser game add to library and launching * fix launching browser games with wallet unconnected * update hyperplay library on refresh * prevent adding duplicate games, add refresh library after game added * fix uninstall browser game * fix: native download, install size, platform handling * chore:use curl to download instead of axios * add hyperplayGameOs type, fix some types * other: store install information on installe store * fix: platforms handler * fix: should not complete installation if download fails * fix: download file * fix axios download abort controller * add hp stop game, rm duplicate aborts * fix installed state after kill during download * fix extraction for game with spaces in name * fix game launch * fix desktop game import * add rudderstack env key * update links * fix alternative exe for sideload and hp games * use bin exe for unreal games if possible * fix uninstall and macOS launching * labels and strings * fix button borders * update store url * add getInfo handler * Update hyperplay_store_preload.ts * try to get unreal bin exec on import * add legacy enable provider method * chore: bump version * fix: download speed + abort again --------- Co-authored-by: Maximiliaan van Dijk (hellowodl) <max@maxilibrium.com> Co-authored-by: BrettCleary <27568879+BrettCleary@users.noreply.github.com> Co-authored-by: Linux4Good <linux4good@protonmail.com>
1 parent 41d5848 commit f076cc2

File tree

58 files changed

+1397
-182
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1397
-182
lines changed

.github/workflows/release_linux.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
env:
1010
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
1111
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
12+
RUDDERSTACK_CLIENT_ID: ${{ secrets.RUDDERSTACK_CLIENT_ID }}
1213

1314
jobs:
1415
draft-releases:

.github/workflows/release_macOS.yml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ env:
1515
APPLE_ID: ${{ secrets.APPLEID }}
1616
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
1717
TEAMID: ${{ secrets.TEAMID }}
18+
RUDDERSTACK_CLIENT_ID: ${{ secrets.RUDDERSTACK_CLIENT_ID }}
1819

1920
jobs:
2021
build-and-release:

.github/workflows/release_windows.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
env:
1010
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
1111
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
12+
RUDDERSTACK_CLIENT_ID: ${{ secrets.RUDDERSTACK_CLIENT_ID }}
1213

1314
jobs:
1415
draft-releases:

appveyor.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ test: off
2121

2222
artifacts:
2323
- path: dist\HyperPlayWin.zip
24-
name: Heroic_setup
24+
name: hyperplay_setup
2525

2626
deploy:
2727
- provider: Webhook

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hyperplay",
3-
"version": "0.0.5",
3+
"version": "0.0.6",
44
"private": true,
55
"main": "build/electron/main.js",
66
"homepage": "./",
@@ -30,7 +30,8 @@
3030
"build/win_icon.ico",
3131
"build/trayIconLight24x24.png",
3232
"build/trayIconDark24x24.png",
33-
"build/extensions"
33+
"build/extensions",
34+
"build/hyperplay_store_preload.js"
3435
],
3536
"protocols": [
3637
{
@@ -185,6 +186,7 @@
185186
"simple-keyboard": "^3.4.136",
186187
"source-map-support": "^0.5.21",
187188
"steam-shortcut-editor": "^3.1.1",
189+
"stream": "^0.0.2",
188190
"systeminformation": "^5.15.0",
189191
"ts-prune": "^0.10.3",
190192
"tslib": "^2.4.0",

src/backend/api/helpers.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ export const getDefaultSavePath = async (
6262
runner,
6363
alreadyDefinedGogSaves
6464
)
65-
export const getGameInfo = async (appName: string, runner: Runner) =>
66-
ipcRenderer.invoke('getGameInfo', appName, runner)
65+
export const getGameInfo = async (appName: string, runner: Runner) => {
66+
return ipcRenderer.invoke('getGameInfo', appName, runner)
67+
}
68+
6769
export const getExtraInfo = async (appName: string, runner: Runner) =>
6870
ipcRenderer.invoke('getExtraInfo', appName, runner)
6971

src/backend/api/library.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
LaunchParams,
66
SideloadGame,
77
ImportGameArgs,
8-
GameStatus
8+
GameStatus,
9+
HyperPlayGameOS
910
} from 'common/types'
1011

1112
export const removeFolder = (args: [path: string, folderName: string]) =>
@@ -25,8 +26,12 @@ export const uninstall = async (
2526
game_name: appName,
2627
store_name: runner
2728
})
28-
if (runner === 'sideload') {
29-
return ipcRenderer.invoke('removeApp', { appName, shouldRemovePrefix })
29+
if (runner === 'sideload' || runner === 'hyperplay') {
30+
return ipcRenderer.invoke('removeApp', {
31+
appName,
32+
shouldRemovePrefix,
33+
runner
34+
})
3035
} else {
3136
return ipcRenderer.invoke(
3237
'uninstall',
@@ -95,5 +100,12 @@ export const handleRecentGamesChanged = (callback: any) => {
95100
export const addNewApp = (args: SideloadGame) =>
96101
ipcRenderer.send('addNewApp', args)
97102

98-
export const launchApp = async (appName: string): Promise<boolean> =>
99-
ipcRenderer.invoke('launchApp', appName)
103+
export const launchApp = async (
104+
appName: string,
105+
runner: 'hyperplay' | 'sideload'
106+
): Promise<boolean> => ipcRenderer.invoke('launchApp', appName, runner)
107+
108+
export const getHyperPlayInstallInfo = async (
109+
appName: string,
110+
platform: HyperPlayGameOS
111+
) => ipcRenderer.invoke('getHyperPlayInstallInfo', appName, platform)

src/backend/config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ abstract class GlobalConfig {
169169

170170
const winePaths = new Set<string>()
171171

172-
// search for wine installed on $HOME/Library/Application Support/heroic/tools/wine
172+
// search for wine installed on $HOME/Library/Application Support/hyperplay/tools/wine
173173
const wineToolsPath = `${toolsPath}/wine/`
174174
if (existsSync(wineToolsPath)) {
175175
readdirSync(wineToolsPath).forEach((path) => {

src/backend/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const sidInfoUrl =
7171
const githubURL = 'https://github.com/G7DAO/HyperPlay/releases/latest'
7272
const GITHUB_API = 'https://api.github.com/repos/G7DAO/HyperPlay/releases'
7373
const supportURL = 'https://github.com/G7DAO/HyperPlay/blob/main/Support.md'
74-
const discordLink = 'https://discord.gg/Vx4ky6ZbAK'
74+
const discordLink = 'https://discord.gg/eRVDbGUhKD'
7575
const wikiLink = 'https://github.com/G7DAO/HyperPlay/wiki'
7676
const weblateUrl =
7777
'https://hosted.weblate.org/projects/hyperplay-games-launcher'

src/backend/downloadmanager/downloadqueue.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getFileSize, getGame } from '../utils'
44
import { DMQueueElement } from 'common/types'
55
import { installQueueElement, updateQueueElement } from './utils'
66
import { sendFrontendMessage } from '../main_window'
7+
import { getHyperPlayGameInstallInfo } from 'backend/hyperplay/library'
78

89
const downloadManager = new TypeCheckedStoreBackend('downloadManager', {
910
cwd: 'store',
@@ -54,10 +55,20 @@ async function initQueue() {
5455
while (element) {
5556
const queuedElements = downloadManager.get('queue', [])
5657
sendFrontendMessage('changedDMQueueInformation', queuedElements)
57-
const game = getGame(element.params.appName, element.params.runner)
58-
const installInfo = await game.getInstallInfo(
59-
element.params.platformToInstall
60-
)
58+
const { appName, runner, platformToInstall } = element.params
59+
const isHpGame = runner === 'hyperplay'
60+
let game = null
61+
let installInfo = isHpGame
62+
? // @ts-expect-error TS wont know how to handle the type of installInfo
63+
getHyperPlayGameInstallInfo(appName, platformToInstall)
64+
: null
65+
66+
if (runner !== 'hyperplay') {
67+
game = getGame(appName, runner)
68+
// @ts-expect-error TS wont know how to handle the type of installInfo
69+
installInfo = await game.getInstallInfo(platformToInstall)
70+
}
71+
6172
element.params.size = installInfo?.manifest?.download_size
6273
? getFileSize(installInfo?.manifest?.download_size)
6374
: '?? MB'

src/backend/downloadmanager/utils.ts

+28-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { logError, logInfo, LogPrefix, logWarning } from '../logger/logger'
22
import { getGame, isEpicServiceOffline } from '../utils'
3-
import { InstallParams } from 'common/types'
3+
import { AppPlatforms, InstallParams, InstallPlatform } from 'common/types'
44
import i18next from 'i18next'
55
import { notify, showDialogBoxModalAuto } from '../dialog/dialog'
66
import { isOnline } from '../online_monitor'
77
import { sendFrontendMessage } from '../main_window'
8+
import { installHyperPlayGame } from 'backend/hyperplay/library'
89
import { trackEvent } from 'backend/api/metrics'
910

1011
async function installQueueElement(params: InstallParams): Promise<{
@@ -46,13 +47,13 @@ async function installQueueElement(params: InstallParams): Promise<{
4647
}
4748
}
4849

49-
trackEvent({
50+
/* trackEvent({
5051
event: 'Game Install Started',
5152
properties: {
5253
game_name: appName,
5354
store_name: runner
5455
}
55-
})
56+
}) */
5657

5758
sendFrontendMessage('gameStatusUpdate', {
5859
appName,
@@ -81,13 +82,30 @@ async function installQueueElement(params: InstallParams): Promise<{
8182
}
8283

8384
try {
84-
const { status, error } = await game.install({
85-
path: path.replaceAll("'", ''),
86-
installDlcs,
87-
sdlList,
88-
platformToInstall,
89-
installLanguage
90-
})
85+
let installInstance
86+
87+
if (runner === 'hyperplay') {
88+
const installPlatform = platformToInstall as AppPlatforms
89+
installInstance = async () =>
90+
installHyperPlayGame({
91+
appName,
92+
// @ts-expect-error TODO: Fix this
93+
platformToInstall: installPlatform,
94+
dirpath: path
95+
})
96+
} else {
97+
const installPlatform = platformToInstall as InstallPlatform
98+
installInstance = async () =>
99+
game.install({
100+
path: path.replaceAll("'", ''),
101+
installDlcs,
102+
sdlList,
103+
platformToInstall: installPlatform,
104+
installLanguage
105+
})
106+
}
107+
108+
const { status, error } = await installInstance()
91109

92110
if (status === 'abort') {
93111
logWarning(
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { TypeCheckedStoreBackend } from './../electron_store'
2+
3+
export const hpLibraryStore = new TypeCheckedStoreBackend('hpLibraryStore', {
4+
cwd: 'hp_store',
5+
name: 'library',
6+
clearInvalidConfig: true
7+
})
8+
9+
export const hpInstalledGamesStore = new TypeCheckedStoreBackend(
10+
'hpInstalledGamesStore',
11+
{
12+
cwd: 'hp_store',
13+
name: 'installed'
14+
}
15+
)

src/backend/hyperplay/games.ts

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
getBinExecIfExists,
3+
getHyperPlayGameInfo
4+
} from 'backend/hyperplay/library'
5+
import { existsSync } from 'graceful-fs'
6+
import { isWindows, isMac, isLinux } from '../constants'
7+
import { killPattern } from 'backend/utils'
8+
import { InstallPlatform, Runner } from 'common/types'
9+
import { hpLibraryStore } from './electronStore'
10+
import { sendFrontendMessage } from 'backend/main_window'
11+
import * as path from 'path'
12+
import * as fs from 'fs'
13+
import { LogPrefix, logInfo } from 'backend/logger/logger'
14+
15+
export const isHpGameAvailable = (appName: string) => {
16+
const hpGameInfo = getHyperPlayGameInfo(appName)
17+
if (hpGameInfo && hpGameInfo.install.platform === 'web') {
18+
return true
19+
}
20+
21+
if (hpGameInfo.install && hpGameInfo.install.executable) {
22+
return existsSync(hpGameInfo.install.executable)
23+
}
24+
return false
25+
}
26+
27+
export function isHpGameNative(appName: string): boolean {
28+
const {
29+
install: { platform }
30+
} = getHyperPlayGameInfo(appName)
31+
if (platform) {
32+
if (platform === 'web') {
33+
return true
34+
}
35+
36+
if (isWindows) {
37+
return true
38+
}
39+
40+
if (isMac && platform === 'Mac') {
41+
return true
42+
}
43+
44+
// small hack, but needs to fix the typings
45+
const plat = platform.toLowerCase()
46+
if (isLinux && plat === 'linux') {
47+
return true
48+
}
49+
}
50+
51+
return false
52+
}
53+
54+
export async function stopHpGame(appName: string): Promise<void> {
55+
const {
56+
install: { executable = undefined }
57+
} = getHyperPlayGameInfo(appName)
58+
59+
if (executable) {
60+
const split = executable.split('/')
61+
const exe = split[split.length - 1]
62+
killPattern(exe)
63+
}
64+
}
65+
66+
export async function importGame(
67+
appName: string,
68+
pathName: string,
69+
runner: Runner,
70+
platform: InstallPlatform
71+
) {
72+
const currentLibrary = hpLibraryStore.get('games', [])
73+
74+
// TODO refactor this to constant time check with a set
75+
// not important for alpha release
76+
const gameInLibrary = currentLibrary.find((val) => {
77+
return val.app_name === appName
78+
})
79+
80+
if (gameInLibrary === undefined) {
81+
logInfo('Cannot find game in library so cannot import', LogPrefix.HyperPlay)
82+
return
83+
}
84+
85+
let exec = ''
86+
const files = await fs.promises.readdir(pathName)
87+
files.forEach((val) => {
88+
const splitFile = val.split('.')
89+
const extension = splitFile[splitFile.length - 1]
90+
if (extension === 'exe') {
91+
exec = val
92+
}
93+
})
94+
95+
const executableFullPath = path.join(pathName, exec)
96+
const binExec = getBinExecIfExists(executableFullPath)
97+
gameInLibrary.install = {
98+
install_path: pathName,
99+
executable: binExec === '' ? executableFullPath : binExec,
100+
install_size: '0 GiB',
101+
is_dlc: false,
102+
version: '-1',
103+
platform: platform
104+
}
105+
106+
gameInLibrary.is_installed = true
107+
hpLibraryStore.set('games', currentLibrary)
108+
109+
sendFrontendMessage('refreshLibrary')
110+
}

0 commit comments

Comments
 (0)