Skip to content

[Exp]: ULWGL support #3480

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

Closed
wants to merge 13 commits into from
3 changes: 2 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,8 @@
"experimental_features": {
"automaticWinetricksFixes": "Apply known fixes automatically",
"enableHelp": "Help component",
"enableNewDesign": "New design"
"enableNewDesign": "New design",
"ulwglSupport": "Use ULWGL 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
66 changes: 36 additions & 30 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 Down Expand Up @@ -70,14 +70,14 @@ 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 { isUlwglSupported } from './utils/compatibility_layers'

async function prepareLaunch(
gameSettings: GameSettings,
Expand Down Expand Up @@ -223,13 +223,14 @@ async function prepareLaunch(
let steamRuntime: string[] = []
const shouldUseRuntime =
gameSettings.useSteamRuntime &&
(isNative || gameSettings.wineVersion.type === 'proton')
(isNative || !isUlwglSupported(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 @@ -241,8 +242,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 Down Expand Up @@ -293,14 +294,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 @@ -336,11 +329,6 @@ async function prepareWineLaunch(
)
if (runner === 'gog') {
await gogSetup(appName)
sendFrontendMessage('gameStatusUpdate', {
appName,
runner: 'gog',
status: 'launching'
})
}
if (runner === 'nile') {
await nileSetup(appName)
Expand Down Expand Up @@ -496,16 +484,20 @@ function setupWrapperEnvVars(wrapperEnv: WrapperEnv) {

ret.HEROIC_APP_NAME = wrapperEnv.appName
ret.HEROIC_APP_RUNNER = wrapperEnv.appRunner
ret.GAMEID = 'ulwgl-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 Down Expand Up @@ -552,7 +544,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 @@ -739,7 +733,7 @@ export async function verifyWinePrefix(
return { res: { stdout: '', stderr: '' }, updated: false }
}

if (!existsSync(winePrefix)) {
if (!existsSync(winePrefix) && wineVersion.type !== 'proton') {
mkdirSync(winePrefix, { recursive: true })
}

Expand All @@ -751,9 +745,15 @@ export async function verifyWinePrefix(
const haveToWait = !existsSync(systemRegPath)

const command = runWineCommand({
commandParts: ['wineboot', '--init'],
commandParts:
wineVersion.type === 'proton' &&
GlobalConfig.get().getSettings().experimentalFeatures?.ulwglSupport !==
false
? ['createprefix']
: ['wineboot', '--init'],
wait: haveToWait,
gameSettings: settings,
protonVerb: 'run',
skipPrefixCheckIKnowWhatImDoing: true
})

Expand Down Expand Up @@ -783,7 +783,6 @@ function launchCleanup(rpcClient?: RpcClient) {
async function runWineCommand({
gameSettings,
commandParts,
gameInstallPath,
wait,
protonVerb = 'run',
installFolderName,
Expand Down Expand Up @@ -838,13 +837,10 @@ 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: 'ulwgl-0',
...setupEnvVars(settings),
...setupWineEnvVars(settings, installFolderName),
PROTON_VERB: protonVerb
}

const wineBin = wineVersion.bin.replaceAll("'", '')
Expand All @@ -854,11 +850,21 @@ async function runWineCommand({
return new Promise<{ stderr: string; stdout: string }>((res) => {
const wrappers = options?.wrappers || []
let bin = ''
const ulwglSupported = isUlwglSupported(wineVersion.type)

if (wrappers.length) {
bin = wrappers.shift()!
commandParts.unshift(...wrappers, wineBin)
if (ulwglSupported) {
const ulwglBin = join(runtimePath, 'ulwgl', 'ulwgl-run')
commandParts.unshift(...wrappers, ulwglBin)
} else {
commandParts.unshift(...wrappers, wineBin)
}
} else {
bin = wineBin
if (ulwglSupported) {
bin = join(runtimePath, 'ulwgl', 'ulwgl-run')
}
}

const child = spawn(bin, commandParts, {
Expand Down
14 changes: 8 additions & 6 deletions src/backend/storeManagers/gog/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import { readdir, readFile } from 'fs/promises'
import { statSync } from 'fs'
import ini from 'ini'
import { getRequiredRedistList, updateRedist } from './redist'
import { getUlwglId } from 'backend/wiki_game_info/ulwgl/utils'

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

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

if (wineType === 'proton') {
const ulwglId = await getUlwglId(gameInfo.app_name, gameInfo.runner)
if (ulwglId) {
commandEnv['GAMEID'] = ulwglId
}
}

// Fix for people with old config
const wineBin =
wineExec.startsWith("'") && wineExec.endsWith("'")
Expand Down Expand Up @@ -663,12 +671,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
13 changes: 7 additions & 6 deletions src/backend/storeManagers/legendary/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
PositiveInteger
} from './commands/base'
import { LegendaryCommand } from './commands'
import { getUlwglId } from 'backend/wiki_game_info/ulwgl/utils'

/**
* Alias for `LegendaryLibrary.listUpdateableGames`
Expand Down Expand Up @@ -833,6 +834,12 @@ export async function launch(

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

if (wineType === 'proton') {
const ulwglId = await getUlwglId(gameInfo.app_name, gameInfo.runner)
if (ulwglId) {
commandEnv['GAMEID'] = ulwglId
}
}
// Fix for people with old config
const wineBin =
wineExec.startsWith("'") && wineExec.endsWith("'")
Expand Down Expand Up @@ -872,12 +879,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
14 changes: 8 additions & 6 deletions src/backend/storeManagers/nile/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
import { removeNonSteamGame } from 'backend/shortcuts/nonesteamgame/nonesteamgame'
import { sendFrontendMessage } from 'backend/main_window'
import setup from './setup'
import { getUlwglId } from 'backend/wiki_game_info/ulwgl/utils'

export async function getSettings(appName: string): Promise<GameSettings> {
const gameConfig = GameConfig.get(appName)
Expand Down Expand Up @@ -378,6 +379,13 @@ export async function launch(

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

if (wineType === 'proton') {
const ulwglId = await getUlwglId(gameInfo.app_name, gameInfo.runner)
if (ulwglId) {
commandEnv['GAMEID'] = ulwglId
}
}

// Fix for people with old config
const wineBin =
wineExec.startsWith("'") && wineExec.endsWith("'")
Expand Down Expand Up @@ -410,12 +418,6 @@ export async function launch(

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

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

const { error } = await runNileCommand(commandParts, {
abortId: appName,
env: commandEnv,
Expand Down
2 changes: 1 addition & 1 deletion src/backend/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ async function shutdownWine(gameSettings: GameSettings) {
gameSettings,
commandParts: ['wineboot', '-k'],
wait: true,
protonVerb: 'waitforexitandrun'
protonVerb: 'run'
})
}
}
Expand Down
20 changes: 19 additions & 1 deletion src/backend/utils/compatibility_layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
configPath,
getSteamLibraries,
isMac,
runtimePath,
toolsPath,
userHome
} from 'backend/constants'
Expand Down Expand Up @@ -430,6 +431,7 @@ export function getWineFlags(
wrapper: string
): AllowedWineFlags {
let partialCommand: AllowedWineFlags = {}
const ulwglSupported = isUlwglSupported(wineType)
switch (wineType) {
case 'wine':
case 'toolkit':
Expand All @@ -439,7 +441,14 @@ export function getWineFlags(
case 'proton':
partialCommand = {
'--no-wine': true,
'--wrapper': NonEmptyString.parse(`${wrapper} '${wineBin}' run`)
'--wrapper': NonEmptyString.parse(
`${wrapper} "${wineBin}" waitforexitandrun`
)
}
if (ulwglSupported) {
partialCommand['--wrapper'] = NonEmptyString.parse(
`${wrapper} "${join(runtimePath, 'ulwgl', 'ulwgl-run')}"`
)
}
break
case 'crossover':
Expand Down Expand Up @@ -471,3 +480,12 @@ export function getWineFlagsArray(
}
return commandArray
}

export function isUlwglSupported(wineType: WineInstallation['type']): boolean {
return (
wineType === 'proton' &&
GlobalConfig.get().getSettings().experimentalFeatures?.ulwglSupport !==
false &&
existsSync(join(runtimePath, 'ulwgl', 'ulwgl-run'))
)
}
Loading
Loading