Skip to content

Commit 6105bf2

Browse files
Etaash-mathamsettyimLinguinarieljCommandMC
authored
[Exp]: UMU support (#3724)
* [Exp]: ULWGL support * improv: fetch ulwgl id only on Linux * add fallback gameid * less agressive 6h caching * sideload support, additional checks for ulwgl support * use run verb in shutdownWine * cache: allow custom data validity checks to be defined useful for use cases where we want to make sure that data won't be removed when offline etc.. in case of ULWGL we invalidate entries that are null * use ulwgl-run * lint * update from ulwgl to umu * fix lint * automatically download umu runtime * review suggestions Co-authored-by: Ariel Juodziukynas <[email protected]> * fix import * review suggestions Co-authored-by: Ariel Juodziukynas <[email protected]> * bug fixes * Update launcher.ts * Update utils.ts * small refactoring + bug fixes * Update src/backend/launcher.ts Co-authored-by: Ariel Juodziukynas <[email protected]> * fixes * introduce getUmuPath function * initial broken winetricks implementation * Fixup auto-update - `isInstalled('umu')` was missing an await - `isUmuSupported` would return false if UMU wasn't installed, so the auto-install would never actually install UMU if it was missing * Make `getUmuPath` search for the `umu-run` binary on $PATH * Remove `PROTON_VERB` env var from Winetricks env This seems to completely break it, not sure why yet but it's not necessary anyways * Call umu-run with an empty executable when running Winetricks GUI Running it with "" makes it try to run a file called "", which we don't want * fix getWineFlagsArray * fixes for winetricks * disable umu runtime updates when running winetricks --------- Co-authored-by: Paweł Lidwin <[email protected]> Co-authored-by: Ariel Juodziukynas <[email protected]> Co-authored-by: Mathis Dröge <[email protected]>
1 parent b8e7467 commit 6105bf2

File tree

19 files changed

+237
-96
lines changed

19 files changed

+237
-96
lines changed

public/locales/en/translation.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,8 @@
635635
"experimental_features": {
636636
"automaticWinetricksFixes": "Apply known fixes automatically",
637637
"enableHelp": "Help component",
638-
"enableNewDesign": "New design"
638+
"enableNewDesign": "New design",
639+
"umuSupport": "Use UMU as Proton runtime"
639640
},
640641
"frameless-window": {
641642
"confirmation": {

src/backend/cache.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ export default class CacheStore<ValueType, KeyType extends string = string> {
66
private using_in_memory: boolean
77
private current_store: Store | Map<string, ValueType>
88
private readonly lifespan: number | null
9+
private invalidateCheck: (data: ValueType) => boolean
910

1011
/**
1112
* Creates a new cache store
1213
* @param filename
1314
* @param max_value_lifespan How long an individual entry in the store will
1415
* be cached (in minutes)
1516
*/
16-
constructor(filename: string, max_value_lifespan: number | null = 60 * 6) {
17+
constructor(
18+
filename: string,
19+
max_value_lifespan: number | null = 60 * 6,
20+
options?: { invalidateCheck?: (data: ValueType) => boolean }
21+
) {
1722
this.store = new Store({
1823
cwd: 'store_cache',
1924
name: filename,
@@ -23,6 +28,11 @@ export default class CacheStore<ValueType, KeyType extends string = string> {
2328
this.using_in_memory = false
2429
this.current_store = this.store
2530
this.lifespan = max_value_lifespan
31+
if (options && options.invalidateCheck) {
32+
this.invalidateCheck = options.invalidateCheck
33+
} else {
34+
this.invalidateCheck = () => true
35+
}
2636
}
2737

2838
/**
@@ -54,7 +64,10 @@ export default class CacheStore<ValueType, KeyType extends string = string> {
5464
const updateDate = new Date(lastUpdateTimestamp)
5565
const msSinceUpdate = Date.now() - updateDate.getTime()
5666
const minutesSinceUpdate = msSinceUpdate / 1000 / 60
57-
if (minutesSinceUpdate > this.lifespan) {
67+
if (
68+
minutesSinceUpdate > this.lifespan &&
69+
this.invalidateCheck(this.current_store.get(key) as ValueType)
70+
) {
5871
this.current_store.delete(key)
5972
this.current_store.delete(`__timestamp.${key}`)
6073
return fallback

src/backend/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const epicRedistPath = join(toolsPath, 'redist', 'legendary')
6666
const gogRedistPath = join(toolsPath, 'redist', 'gog')
6767
const heroicIconFolder = join(appFolder, 'icons')
6868
const runtimePath = join(toolsPath, 'runtimes')
69+
const defaultUmuPath = join(runtimePath, 'umu', 'umu_run.py')
6970
const userInfo = join(legendaryConfigPath, 'user.json')
7071
const heroicInstallPath = join(userHome, 'Games', 'Heroic')
7172
const defaultWinePrefixDir = join(userHome, 'Games', 'Heroic', 'Prefixes')
@@ -278,6 +279,7 @@ export {
278279
fontsStore,
279280
isSteamDeckGameMode,
280281
runtimePath,
282+
defaultUmuPath,
281283
isCLIFullscreen,
282284
isCLINoGui,
283285
publicDir,

src/backend/launcher.ts

+42-33
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818

1919
import i18next from 'i18next'
2020
import { existsSync, mkdirSync } from 'graceful-fs'
21-
import { join, normalize } from 'path'
21+
import { join, dirname } from 'path'
2222

2323
import {
2424
defaultWinePrefix,
@@ -29,7 +29,8 @@ import {
2929
isWindows,
3030
isSteamDeckGameMode,
3131
runtimePath,
32-
userHome
32+
userHome,
33+
defaultUmuPath
3334
} from './constants'
3435
import {
3536
constructAndUpdateRPC,
@@ -71,14 +72,15 @@ import { readFileSync } from 'fs'
7172
import { LegendaryCommand } from './storeManagers/legendary/commands'
7273
import { commandToArgsArray } from './storeManagers/legendary/library'
7374
import { searchForExecutableOnPath } from './utils/os/path'
74-
import { sendFrontendMessage } from './main_window'
7575
import {
7676
createAbortController,
7777
deleteAbortController
7878
} from './utils/aborthandler/aborthandler'
7979
import { download, isInstalled } from './wine/runtimes/runtimes'
8080
import { storeMap } from 'common/utils'
8181
import { runWineCommandOnGame } from './storeManagers/legendary/games'
82+
import { sendFrontendMessage } from './main_window'
83+
import { getUmuPath, isUmuSupported } from './utils/compatibility_layers'
8284

8385
async function prepareLaunch(
8486
gameSettings: GameSettings,
@@ -228,17 +230,27 @@ async function prepareLaunch(
228230
}
229231
}
230232

233+
if (
234+
(await isUmuSupported(gameSettings.wineVersion.type, false)) &&
235+
!(await isInstalled('umu')) &&
236+
isOnline() &&
237+
(await getUmuPath()) === defaultUmuPath
238+
) {
239+
await download('umu')
240+
}
241+
231242
// If the Steam Runtime is enabled, find a valid one
232243
let steamRuntime: string[] = []
233244
const shouldUseRuntime =
234245
gameSettings.useSteamRuntime &&
235-
(isNative || gameSettings.wineVersion.type === 'proton')
246+
(isNative || !isUmuSupported(gameSettings.wineVersion.type))
247+
236248
if (shouldUseRuntime) {
237249
// Determine which runtime to use based on toolmanifest.vdf which is shipped with proton
238250
let nonNativeRuntime: SteamRuntime['type'] = 'soldier'
239251
if (!isNative) {
240252
try {
241-
const parentPath = normalize(join(gameSettings.wineVersion.bin, '..'))
253+
const parentPath = dirname(gameSettings.wineVersion.bin)
242254
const requiredAppId = VDF.parse(
243255
readFileSync(join(parentPath, 'toolmanifest.vdf'), 'utf-8')
244256
).manifest?.require_tool_appid
@@ -250,8 +262,8 @@ async function prepareLaunch(
250262
)
251263
}
252264
}
253-
// for native games lets use scout for now
254-
const runtimeType = isNative ? 'sniper' : nonNativeRuntime
265+
266+
const runtimeType = isNative ? 'scout' : nonNativeRuntime
255267
const { path, args } = await getSteamRuntime(runtimeType)
256268
if (!path) {
257269
return {
@@ -268,6 +280,8 @@ async function prepareLaunch(
268280
}
269281
}
270282

283+
logInfo(`Using Steam ${runtimeType} Runtime`, LogPrefix.Backend)
284+
271285
steamRuntime = [path, ...args]
272286
}
273287

@@ -302,14 +316,6 @@ async function prepareWineLaunch(
302316
}
303317
}
304318

305-
// Log warning about Proton
306-
if (gameSettings.wineVersion.type === 'proton') {
307-
logWarning(
308-
'You are using Proton, this can lead to some bugs. Please do not open issues with bugs related to games',
309-
LogPrefix.Backend
310-
)
311-
}
312-
313319
// Verify that the CrossOver bottle exists
314320
if (isMac && gameSettings.wineVersion.type === 'crossover') {
315321
const bottleExists = existsSync(
@@ -497,16 +503,20 @@ function setupWrapperEnvVars(wrapperEnv: WrapperEnv) {
497503

498504
ret.HEROIC_APP_NAME = wrapperEnv.appName
499505
ret.HEROIC_APP_RUNNER = wrapperEnv.appRunner
506+
ret.GAMEID = 'umu-0'
500507

501508
switch (wrapperEnv.appRunner) {
502509
case 'gog':
503510
ret.HEROIC_APP_SOURCE = 'gog'
511+
ret.STORE = 'gog'
504512
break
505513
case 'legendary':
506514
ret.HEROIC_APP_SOURCE = 'epic'
515+
ret.STORE = 'egs'
507516
break
508517
case 'nile':
509518
ret.HEROIC_APP_SOURCE = 'amazon'
519+
ret.STORE = 'amazon'
510520
break
511521
case 'sideload':
512522
ret.HEROIC_APP_SOURCE = 'sideload'
@@ -527,9 +537,6 @@ function setupWineEnvVars(gameSettings: GameSettings, gameId = '0') {
527537

528538
const ret: Record<string, string> = {}
529539

530-
ret.DOTNET_BUNDLE_EXTRACT_BASE_DIR = ''
531-
ret.DOTNET_ROOT = ''
532-
533540
// Add WINEPREFIX / STEAM_COMPAT_DATA_PATH / CX_BOTTLE
534541
const steamInstallPath = join(flatPakHome, '.steam', 'steam')
535542
switch (wineVersion.type) {
@@ -544,7 +551,7 @@ function setupWineEnvVars(gameSettings: GameSettings, gameId = '0') {
544551
)
545552
if (dllOverridesVar) {
546553
ret[dllOverridesVar.key] =
547-
dllOverridesVar.value + ',' + wmbDisableString
554+
dllOverridesVar.value + ';' + wmbDisableString
548555
} else {
549556
ret.WINEDLLOVERRIDES = wmbDisableString
550557
}
@@ -553,7 +560,9 @@ function setupWineEnvVars(gameSettings: GameSettings, gameId = '0') {
553560
}
554561
case 'proton':
555562
ret.STEAM_COMPAT_CLIENT_INSTALL_PATH = steamInstallPath
563+
ret.WINEPREFIX = winePrefix
556564
ret.STEAM_COMPAT_DATA_PATH = winePrefix
565+
ret.PROTONPATH = dirname(gameSettings.wineVersion.bin)
557566
break
558567
case 'crossover':
559568
ret.CX_BOTTLE = wineCrossoverBottle
@@ -757,7 +766,7 @@ export async function verifyWinePrefix(
757766
return { res: { stdout: '', stderr: '' }, updated: false }
758767
}
759768

760-
if (!existsSync(winePrefix)) {
769+
if (!existsSync(winePrefix) && !(await isUmuSupported(wineVersion.type))) {
761770
mkdirSync(winePrefix, { recursive: true })
762771
}
763772

@@ -769,9 +778,12 @@ export async function verifyWinePrefix(
769778
const haveToWait = !existsSync(systemRegPath)
770779

771780
const command = runWineCommand({
772-
commandParts: ['wineboot', '--init'],
781+
commandParts: (await isUmuSupported(wineVersion.type))
782+
? ['createprefix']
783+
: ['wineboot', '--init'],
773784
wait: haveToWait,
774785
gameSettings: settings,
786+
protonVerb: 'run',
775787
skipPrefixCheckIKnowWhatImDoing: true
776788
})
777789

@@ -801,7 +813,6 @@ function launchCleanup(rpcClient?: RpcClient) {
801813
async function runWineCommand({
802814
gameSettings,
803815
commandParts,
804-
gameInstallPath,
805816
wait,
806817
protonVerb = 'run',
807818
installFolderName,
@@ -856,27 +867,25 @@ async function runWineCommand({
856867

857868
const env_vars = {
858869
...process.env,
859-
...setupEnvVars(settings, gameInstallPath),
860-
...setupWineEnvVars(settings, installFolderName)
861-
}
862-
863-
const isProton = wineVersion.type === 'proton'
864-
if (isProton) {
865-
commandParts.unshift(protonVerb)
870+
GAMEID: 'umu-0',
871+
...setupEnvVars(settings),
872+
...setupWineEnvVars(settings, installFolderName),
873+
PROTON_VERB: protonVerb
866874
}
867875

868876
const wineBin = wineVersion.bin.replaceAll("'", '')
877+
const umuSupported = await isUmuSupported(wineVersion.type)
878+
const runnerBin = umuSupported ? await getUmuPath() : wineBin
869879

870880
logDebug(['Running Wine command:', commandParts.join(' ')], LogPrefix.Backend)
871881

872882
return new Promise<{ stderr: string; stdout: string }>((res) => {
873883
const wrappers = options?.wrappers || []
874-
let bin = ''
884+
let bin = runnerBin
885+
875886
if (wrappers.length) {
876887
bin = wrappers.shift()!
877-
commandParts.unshift(...wrappers, wineBin)
878-
} else {
879-
bin = wineBin
888+
commandParts.unshift(...wrappers, runnerBin)
880889
}
881890

882891
const child = spawn(bin, commandParts, {

src/backend/storeManagers/gog/games.ts

+13-8
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,17 @@ import { t } from 'i18next'
9494
import { showDialogBoxModalAuto } from '../../dialog/dialog'
9595
import { sendFrontendMessage } from '../../main_window'
9696
import { RemoveArgs } from 'common/types/game_manager'
97-
import { getWineFlagsArray } from 'backend/utils/compatibility_layers'
97+
import {
98+
getWineFlagsArray,
99+
isUmuSupported
100+
} from 'backend/utils/compatibility_layers'
98101
import axios, { AxiosError } from 'axios'
99102
import { isOnline, runOnceWhenOnline } from 'backend/online_monitor'
100103
import { readdir, readFile } from 'fs/promises'
101104
import { statSync } from 'fs'
102105
import ini from 'ini'
103106
import { getRequiredRedistList, updateRedist } from './redist'
107+
import { getUmuId } from 'backend/wiki_game_info/umu/utils'
104108

105109
export async function getExtraInfo(appName: string): Promise<ExtraInfo> {
106110
const gameInfo = getGameInfo(appName)
@@ -559,13 +563,20 @@ export async function launch(
559563

560564
const { bin: wineExec, type: wineType } = gameSettings.wineVersion
561565

566+
if (await isUmuSupported(wineType)) {
567+
const umuId = await getUmuId(gameInfo.app_name, gameInfo.runner)
568+
if (umuId) {
569+
commandEnv['GAMEID'] = umuId
570+
}
571+
}
572+
562573
// Fix for people with old config
563574
const wineBin =
564575
wineExec.startsWith("'") && wineExec.endsWith("'")
565576
? wineExec.replaceAll("'", '')
566577
: wineExec
567578

568-
wineFlag = getWineFlagsArray(wineBin, wineType, shlex.join(wrappers))
579+
wineFlag = await getWineFlagsArray(wineBin, wineType, shlex.join(wrappers))
569580
}
570581

571582
const commandParts = [
@@ -670,12 +681,6 @@ export async function launch(
670681

671682
sendGameStatusUpdate({ appName, runner: 'gog', status: 'playing' })
672683

673-
sendGameStatusUpdate({
674-
appName,
675-
runner: 'gog',
676-
status: 'playing'
677-
})
678-
679684
const { error, abort } = await runGogdlCommand(commandParts, {
680685
abortId: appName,
681686
env: commandEnv,

src/backend/storeManagers/legendary/games.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ import { sendFrontendMessage } from '../../main_window'
7777
import { RemoveArgs } from 'common/types/game_manager'
7878
import {
7979
AllowedWineFlags,
80-
getWineFlags
80+
getWineFlags,
81+
isUmuSupported
8182
} from 'backend/utils/compatibility_layers'
8283
import {
8384
LegendaryAppName,
@@ -86,6 +87,7 @@ import {
8687
PositiveInteger
8788
} from './commands/base'
8889
import { LegendaryCommand } from './commands'
90+
import { getUmuId } from 'backend/wiki_game_info/umu/utils'
8991
import thirdParty from './thirdParty'
9092
import { Path } from 'backend/schemas'
9193
import { mkdirSync } from 'fs'
@@ -912,13 +914,19 @@ export async function launch(
912914

913915
const { bin: wineExec, type: wineType } = gameSettings.wineVersion
914916

917+
if (await isUmuSupported(wineType)) {
918+
const umuId = await getUmuId(gameInfo.app_name, gameInfo.runner)
919+
if (umuId) {
920+
commandEnv['GAMEID'] = umuId
921+
}
922+
}
915923
// Fix for people with old config
916924
const wineBin =
917925
wineExec.startsWith("'") && wineExec.endsWith("'")
918926
? wineExec.replaceAll("'", '')
919927
: wineExec
920928

921-
wineFlags = getWineFlags(wineBin, wineType, shlex.join(wrappers))
929+
wineFlags = await getWineFlags(wineBin, wineType, shlex.join(wrappers))
922930
}
923931

924932
const appNameToLaunch =
@@ -952,12 +960,6 @@ export async function launch(
952960

953961
sendGameStatusUpdate({ appName, runner: 'legendary', status: 'playing' })
954962

955-
sendGameStatusUpdate({
956-
appName,
957-
runner: 'legendary',
958-
status: 'playing'
959-
})
960-
961963
const { error } = await runLegendaryCommand(command, {
962964
abortId: appName,
963965
env: commandEnv,

0 commit comments

Comments
 (0)