Skip to content

[Exp]: UMU support #3724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9e87a99
[Exp]: ULWGL support
imLinguin Jan 30, 2024
ca4b5af
improv: fetch ulwgl id only on Linux
imLinguin Jan 30, 2024
d59aad4
add fallback gameid
imLinguin Jan 30, 2024
f76cb56
less agressive 6h caching
imLinguin Jan 31, 2024
14c4f79
sideload support, additional checks for ulwgl support
imLinguin Feb 1, 2024
d7f6272
use run verb in shutdownWine
imLinguin Feb 3, 2024
d08399b
cache: allow custom data validity checks to be defined
imLinguin Feb 5, 2024
f972c7c
use ulwgl-run
imLinguin Feb 7, 2024
aad0792
lint
imLinguin Feb 8, 2024
217b7d8
update from ulwgl to umu
Etaash-mathamsetty Apr 27, 2024
788d7db
fix lint
Etaash-mathamsetty Apr 27, 2024
62cf6b9
automatically download umu runtime
Etaash-mathamsetty May 31, 2024
43c2149
review suggestions
Etaash-mathamsetty Jun 1, 2024
fd30517
fix import
Etaash-mathamsetty Jun 1, 2024
eeefd1a
review suggestions
Etaash-mathamsetty Jun 2, 2024
5e80536
bug fixes
Etaash-mathamsetty Jun 2, 2024
99b556d
Update launcher.ts
Etaash-mathamsetty Jun 2, 2024
2c3cd0d
Update utils.ts
Etaash-mathamsetty Jun 2, 2024
bb92e40
small refactoring + bug fixes
Etaash-mathamsetty Jun 4, 2024
595f880
Update src/backend/launcher.ts
Etaash-mathamsetty Jun 7, 2024
70e4fcb
fixes
Etaash-mathamsetty Jun 7, 2024
52df541
Merge branch 'main' into umu
Etaash-mathamsetty Jun 29, 2024
2cfda8d
introduce getUmuPath function
Etaash-mathamsetty Jun 29, 2024
3d33254
initial broken winetricks implementation
Etaash-mathamsetty Jun 30, 2024
494b40b
Fixup auto-update
CommandMC Jul 25, 2024
aad2ae0
Make `getUmuPath` search for the `umu-run` binary on $PATH
CommandMC Jul 26, 2024
accd1e5
Remove `PROTON_VERB` env var from Winetricks env
CommandMC Jul 26, 2024
bd0ab0d
Call umu-run with an empty executable when running Winetricks GUI
CommandMC Jul 26, 2024
bfbcbbb
Merge branch 'main' into umu
Etaash-mathamsetty Jul 26, 2024
23c4df4
Merge branch 'main' into umu
Etaash-mathamsetty Jul 26, 2024
460f695
fix getWineFlagsArray
Etaash-mathamsetty Jul 26, 2024
bd374a2
fixes for winetricks
Etaash-mathamsetty Jul 26, 2024
ccb36e7
disable umu runtime updates when running winetricks
Etaash-mathamsetty Jul 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should update the JSDoc above the method with this new option so it's more clear what it's used for

at least from the usage of this in this PR I'm not sure I understand why it's needed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made invalidateCheck to allow us to add additional checks on data. Useful for making sure we invalidate cache only when online for example. In case of umuId store, we use that to make a cache invalid only if the id wasn't found previously.

) {
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') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably still log this if UMU isn't enabled

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',
Copy link
Contributor

@R1kaB3rN R1kaB3rN Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, if you omit the verb, umu will default to using waitforexitandrun verb which will block users from running more than 1 game in the same prefix. However, if heroic wants to allow users to run more than 1 game, then you could default to runinprefix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use waitforexitandrun only for game executables to ensure we run in proper env. There is no reason for users to use same prefix for more than one game.
waitforexitandrun acts as a safeguard to avoid situations where wineserver instance runs with fsync and the game needs that disabled.

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 @@ -558,13 +562,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 @@ -669,12 +680,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 @@ -911,13 +913,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 @@ -951,12 +959,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
Loading