Skip to content

Commit

Permalink
[Exp]: UMU support (#3724)
Browse files Browse the repository at this point in the history
* [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]>
  • Loading branch information
4 people authored Aug 3, 2024
1 parent b8e7467 commit 6105bf2
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 96 deletions.
3 changes: 2 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,8 @@
"experimental_features": {
"automaticWinetricksFixes": "Apply known fixes automatically",
"enableHelp": "Help component",
"enableNewDesign": "New design"
"enableNewDesign": "New design",
"umuSupport": "Use UMU as Proton runtime"
},
"frameless-window": {
"confirmation": {
Expand Down
17 changes: 15 additions & 2 deletions src/backend/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ export default class CacheStore<ValueType, KeyType extends string = string> {
private using_in_memory: boolean
private current_store: Store | Map<string, ValueType>
private readonly lifespan: number | null
private invalidateCheck: (data: ValueType) => boolean

/**
* Creates a new cache store
* @param filename
* @param max_value_lifespan How long an individual entry in the store will
* be cached (in minutes)
*/
constructor(filename: string, max_value_lifespan: number | null = 60 * 6) {
constructor(
filename: string,
max_value_lifespan: number | null = 60 * 6,
options?: { invalidateCheck?: (data: ValueType) => boolean }
) {
this.store = new Store({
cwd: 'store_cache',
name: filename,
Expand All @@ -23,6 +28,11 @@ export default class CacheStore<ValueType, KeyType extends string = string> {
this.using_in_memory = false
this.current_store = this.store
this.lifespan = max_value_lifespan
if (options && options.invalidateCheck) {
this.invalidateCheck = options.invalidateCheck
} else {
this.invalidateCheck = () => true
}
}

/**
Expand Down Expand Up @@ -54,7 +64,10 @@ export default class CacheStore<ValueType, KeyType extends string = string> {
const updateDate = new Date(lastUpdateTimestamp)
const msSinceUpdate = Date.now() - updateDate.getTime()
const minutesSinceUpdate = msSinceUpdate / 1000 / 60
if (minutesSinceUpdate > this.lifespan) {
if (
minutesSinceUpdate > this.lifespan &&
this.invalidateCheck(this.current_store.get(key) as ValueType)
) {
this.current_store.delete(key)
this.current_store.delete(`__timestamp.${key}`)
return fallback
Expand Down
2 changes: 2 additions & 0 deletions src/backend/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const epicRedistPath = join(toolsPath, 'redist', 'legendary')
const gogRedistPath = join(toolsPath, 'redist', 'gog')
const heroicIconFolder = join(appFolder, 'icons')
const runtimePath = join(toolsPath, 'runtimes')
const defaultUmuPath = join(runtimePath, 'umu', 'umu_run.py')
const userInfo = join(legendaryConfigPath, 'user.json')
const heroicInstallPath = join(userHome, 'Games', 'Heroic')
const defaultWinePrefixDir = join(userHome, 'Games', 'Heroic', 'Prefixes')
Expand Down Expand Up @@ -278,6 +279,7 @@ export {
fontsStore,
isSteamDeckGameMode,
runtimePath,
defaultUmuPath,
isCLIFullscreen,
isCLINoGui,
publicDir,
Expand Down
75 changes: 42 additions & 33 deletions src/backend/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {

import i18next from 'i18next'
import { existsSync, mkdirSync } from 'graceful-fs'
import { join, normalize } from 'path'
import { join, dirname } from 'path'

import {
defaultWinePrefix,
Expand All @@ -29,7 +29,8 @@ import {
isWindows,
isSteamDeckGameMode,
runtimePath,
userHome
userHome,
defaultUmuPath
} from './constants'
import {
constructAndUpdateRPC,
Expand Down Expand Up @@ -71,14 +72,15 @@ import { readFileSync } from 'fs'
import { LegendaryCommand } from './storeManagers/legendary/commands'
import { commandToArgsArray } from './storeManagers/legendary/library'
import { searchForExecutableOnPath } from './utils/os/path'
import { sendFrontendMessage } from './main_window'
import {
createAbortController,
deleteAbortController
} from './utils/aborthandler/aborthandler'
import { download, isInstalled } from './wine/runtimes/runtimes'
import { storeMap } from 'common/utils'
import { runWineCommandOnGame } from './storeManagers/legendary/games'
import { sendFrontendMessage } from './main_window'
import { getUmuPath, isUmuSupported } from './utils/compatibility_layers'

async function prepareLaunch(
gameSettings: GameSettings,
Expand Down Expand Up @@ -228,17 +230,27 @@ async function prepareLaunch(
}
}

if (
(await isUmuSupported(gameSettings.wineVersion.type, false)) &&
!(await isInstalled('umu')) &&
isOnline() &&
(await getUmuPath()) === defaultUmuPath
) {
await download('umu')
}

// If the Steam Runtime is enabled, find a valid one
let steamRuntime: string[] = []
const shouldUseRuntime =
gameSettings.useSteamRuntime &&
(isNative || gameSettings.wineVersion.type === 'proton')
(isNative || !isUmuSupported(gameSettings.wineVersion.type))

if (shouldUseRuntime) {
// Determine which runtime to use based on toolmanifest.vdf which is shipped with proton
let nonNativeRuntime: SteamRuntime['type'] = 'soldier'
if (!isNative) {
try {
const parentPath = normalize(join(gameSettings.wineVersion.bin, '..'))
const parentPath = dirname(gameSettings.wineVersion.bin)
const requiredAppId = VDF.parse(
readFileSync(join(parentPath, 'toolmanifest.vdf'), 'utf-8')
).manifest?.require_tool_appid
Expand All @@ -250,8 +262,8 @@ async function prepareLaunch(
)
}
}
// for native games lets use scout for now
const runtimeType = isNative ? 'sniper' : nonNativeRuntime

const runtimeType = isNative ? 'scout' : nonNativeRuntime
const { path, args } = await getSteamRuntime(runtimeType)
if (!path) {
return {
Expand All @@ -268,6 +280,8 @@ async function prepareLaunch(
}
}

logInfo(`Using Steam ${runtimeType} Runtime`, LogPrefix.Backend)

steamRuntime = [path, ...args]
}

Expand Down Expand Up @@ -302,14 +316,6 @@ async function prepareWineLaunch(
}
}

// Log warning about Proton
if (gameSettings.wineVersion.type === 'proton') {
logWarning(
'You are using Proton, this can lead to some bugs. Please do not open issues with bugs related to games',
LogPrefix.Backend
)
}

// Verify that the CrossOver bottle exists
if (isMac && gameSettings.wineVersion.type === 'crossover') {
const bottleExists = existsSync(
Expand Down Expand Up @@ -497,16 +503,20 @@ function setupWrapperEnvVars(wrapperEnv: WrapperEnv) {

ret.HEROIC_APP_NAME = wrapperEnv.appName
ret.HEROIC_APP_RUNNER = wrapperEnv.appRunner
ret.GAMEID = 'umu-0'

switch (wrapperEnv.appRunner) {
case 'gog':
ret.HEROIC_APP_SOURCE = 'gog'
ret.STORE = 'gog'
break
case 'legendary':
ret.HEROIC_APP_SOURCE = 'epic'
ret.STORE = 'egs'
break
case 'nile':
ret.HEROIC_APP_SOURCE = 'amazon'
ret.STORE = 'amazon'
break
case 'sideload':
ret.HEROIC_APP_SOURCE = 'sideload'
Expand All @@ -527,9 +537,6 @@ function setupWineEnvVars(gameSettings: GameSettings, gameId = '0') {

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

ret.DOTNET_BUNDLE_EXTRACT_BASE_DIR = ''
ret.DOTNET_ROOT = ''

// Add WINEPREFIX / STEAM_COMPAT_DATA_PATH / CX_BOTTLE
const steamInstallPath = join(flatPakHome, '.steam', 'steam')
switch (wineVersion.type) {
Expand All @@ -544,7 +551,7 @@ function setupWineEnvVars(gameSettings: GameSettings, gameId = '0') {
)
if (dllOverridesVar) {
ret[dllOverridesVar.key] =
dllOverridesVar.value + ',' + wmbDisableString
dllOverridesVar.value + ';' + wmbDisableString
} else {
ret.WINEDLLOVERRIDES = wmbDisableString
}
Expand All @@ -553,7 +560,9 @@ function setupWineEnvVars(gameSettings: GameSettings, gameId = '0') {
}
case 'proton':
ret.STEAM_COMPAT_CLIENT_INSTALL_PATH = steamInstallPath
ret.WINEPREFIX = winePrefix
ret.STEAM_COMPAT_DATA_PATH = winePrefix
ret.PROTONPATH = dirname(gameSettings.wineVersion.bin)
break
case 'crossover':
ret.CX_BOTTLE = wineCrossoverBottle
Expand Down Expand Up @@ -757,7 +766,7 @@ export async function verifyWinePrefix(
return { res: { stdout: '', stderr: '' }, updated: false }
}

if (!existsSync(winePrefix)) {
if (!existsSync(winePrefix) && !(await isUmuSupported(wineVersion.type))) {
mkdirSync(winePrefix, { recursive: true })
}

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

const command = runWineCommand({
commandParts: ['wineboot', '--init'],
commandParts: (await isUmuSupported(wineVersion.type))
? ['createprefix']
: ['wineboot', '--init'],
wait: haveToWait,
gameSettings: settings,
protonVerb: 'run',
skipPrefixCheckIKnowWhatImDoing: true
})

Expand Down Expand Up @@ -801,7 +813,6 @@ function launchCleanup(rpcClient?: RpcClient) {
async function runWineCommand({
gameSettings,
commandParts,
gameInstallPath,
wait,
protonVerb = 'run',
installFolderName,
Expand Down Expand Up @@ -856,27 +867,25 @@ async function runWineCommand({

const env_vars = {
...process.env,
...setupEnvVars(settings, gameInstallPath),
...setupWineEnvVars(settings, installFolderName)
}

const isProton = wineVersion.type === 'proton'
if (isProton) {
commandParts.unshift(protonVerb)
GAMEID: 'umu-0',
...setupEnvVars(settings),
...setupWineEnvVars(settings, installFolderName),
PROTON_VERB: protonVerb
}

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

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

return new Promise<{ stderr: string; stdout: string }>((res) => {
const wrappers = options?.wrappers || []
let bin = ''
let bin = runnerBin

if (wrappers.length) {
bin = wrappers.shift()!
commandParts.unshift(...wrappers, wineBin)
} else {
bin = wineBin
commandParts.unshift(...wrappers, runnerBin)
}

const child = spawn(bin, commandParts, {
Expand Down
21 changes: 13 additions & 8 deletions src/backend/storeManagers/gog/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,17 @@ import { t } from 'i18next'
import { showDialogBoxModalAuto } from '../../dialog/dialog'
import { sendFrontendMessage } from '../../main_window'
import { RemoveArgs } from 'common/types/game_manager'
import { getWineFlagsArray } from 'backend/utils/compatibility_layers'
import {
getWineFlagsArray,
isUmuSupported
} from 'backend/utils/compatibility_layers'
import axios, { AxiosError } from 'axios'
import { isOnline, runOnceWhenOnline } from 'backend/online_monitor'
import { readdir, readFile } from 'fs/promises'
import { statSync } from 'fs'
import ini from 'ini'
import { getRequiredRedistList, updateRedist } from './redist'
import { getUmuId } from 'backend/wiki_game_info/umu/utils'

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

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

if (await isUmuSupported(wineType)) {
const umuId = await getUmuId(gameInfo.app_name, gameInfo.runner)
if (umuId) {
commandEnv['GAMEID'] = umuId
}
}

// Fix for people with old config
const wineBin =
wineExec.startsWith("'") && wineExec.endsWith("'")
? wineExec.replaceAll("'", '')
: wineExec

wineFlag = getWineFlagsArray(wineBin, wineType, shlex.join(wrappers))
wineFlag = await getWineFlagsArray(wineBin, wineType, shlex.join(wrappers))
}

const commandParts = [
Expand Down Expand Up @@ -670,12 +681,6 @@ export async function launch(

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

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

const { error, abort } = await runGogdlCommand(commandParts, {
abortId: appName,
env: commandEnv,
Expand Down
18 changes: 10 additions & 8 deletions src/backend/storeManagers/legendary/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ import { sendFrontendMessage } from '../../main_window'
import { RemoveArgs } from 'common/types/game_manager'
import {
AllowedWineFlags,
getWineFlags
getWineFlags,
isUmuSupported
} from 'backend/utils/compatibility_layers'
import {
LegendaryAppName,
Expand All @@ -86,6 +87,7 @@ import {
PositiveInteger
} from './commands/base'
import { LegendaryCommand } from './commands'
import { getUmuId } from 'backend/wiki_game_info/umu/utils'
import thirdParty from './thirdParty'
import { Path } from 'backend/schemas'
import { mkdirSync } from 'fs'
Expand Down Expand Up @@ -912,13 +914,19 @@ export async function launch(

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

if (await isUmuSupported(wineType)) {
const umuId = await getUmuId(gameInfo.app_name, gameInfo.runner)
if (umuId) {
commandEnv['GAMEID'] = umuId
}
}
// Fix for people with old config
const wineBin =
wineExec.startsWith("'") && wineExec.endsWith("'")
? wineExec.replaceAll("'", '')
: wineExec

wineFlags = getWineFlags(wineBin, wineType, shlex.join(wrappers))
wineFlags = await getWineFlags(wineBin, wineType, shlex.join(wrappers))
}

const appNameToLaunch =
Expand Down Expand Up @@ -952,12 +960,6 @@ export async function launch(

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

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

const { error } = await runLegendaryCommand(command, {
abortId: appName,
env: commandEnv,
Expand Down
Loading

0 comments on commit 6105bf2

Please sign in to comment.