fix dll overrides & add disable UMU
This commit is contained in:
parent
5d5ca345f1
commit
8c6db38fab
12 changed files with 295 additions and 45 deletions
144
src/runner.js
144
src/runner.js
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const { spawn } = require('child_process')
|
||||
const { DIRS } = require('./paths')
|
||||
const { prefixPath } = require('./prefixes')
|
||||
const { prefixPath, ensureUnifiedPrefix } = require('./prefixes')
|
||||
const { prefixNameForGame } = require('./library')
|
||||
|
||||
// The fake Steam install Proton is pointed at through
|
||||
// STEAM_COMPAT_CLIENT_INSTALL_PATH. Both folders Proton links into its prefix
|
||||
// The fake Steam install PLWL falls back to when no real Steam client is
|
||||
// found (see findSteamClientDir). Both folders Proton links into its prefix
|
||||
// have to exist: it symlinks <prefix>/drive_c/Program Files (x86)/Steam/
|
||||
// appcache -> <fake>/appcache while setting the prefix up, and a link into a
|
||||
// missing folder is a dangling link in a tree that gets synced.
|
||||
|
|
@ -19,6 +20,73 @@ function ensureFakeSteam() {
|
|||
return fake
|
||||
}
|
||||
|
||||
// Proton expects the Steam client it is normally started by at
|
||||
// STEAM_COMPAT_CLIENT_INSTALL_PATH: it copies the client's legacy DLLs from
|
||||
// <client>/legacycompat into the prefix, its built-in steam.exe loads the
|
||||
// client's own steamclient.so, and media libs come from <client>/ubuntu12_*.
|
||||
// A client installed on this machine is worth using (read-only) - without one
|
||||
// a Valve build's launch dies in a "steamclient_main.c" assert dialog - and
|
||||
// only when there is none does PLWL fall back to the empty fake folder. GE
|
||||
// builds don't need any of it: their UMU path never starts steam.exe.
|
||||
const STEAM_CLIENT_MARKERS = [
|
||||
['legacycompat', 'steamclient.dll'],
|
||||
['ubuntu12_32', 'steamclient.so']
|
||||
]
|
||||
|
||||
function findSteamClientDir() {
|
||||
const home = os.homedir()
|
||||
const dataHome = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share')
|
||||
const candidates = [
|
||||
process.env.STEAM_COMPAT_CLIENT_INSTALL_PATH,
|
||||
path.join(home, '.steam', 'steam'),
|
||||
path.join(home, '.steam', 'root'),
|
||||
path.join(dataHome, 'Steam'),
|
||||
// flatpak Steam keeps its install inside the sandbox's data dir
|
||||
path.join(home, '.var', 'app', 'com.valvesoftware.Steam', '.local', 'share', 'Steam')
|
||||
]
|
||||
for (const dir of candidates) {
|
||||
if (!dir) continue
|
||||
for (const marker of STEAM_CLIENT_MARKERS) {
|
||||
if (fs.existsSync(path.join(dir, ...marker))) return dir
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function steamClientDir() {
|
||||
return findSteamClientDir() || ensureFakeSteam()
|
||||
}
|
||||
|
||||
// Module names travel in a semicolon-separated WINEDLLOVERRIDES list where
|
||||
// '=' splits name from setting and ',' separates names - anything else in a
|
||||
// name would corrupt the entries around it. A trailing .dll is what guides
|
||||
// write; Wine strips it itself, so PLWL stores the bare name.
|
||||
function sanitizeDllName(name) {
|
||||
const clean = String(name || '').trim().replace(/\.dll$/i, '')
|
||||
return /^[^;=\s]+$/.test(clean) ? clean : ''
|
||||
}
|
||||
|
||||
// Wine replaces an earlier entry for the same module with a later one, so an
|
||||
// inherited WINEDLLOVERRIDES goes FIRST and the app's own overrides are
|
||||
// appended after it. An empty setting is Wine's "disabled" and has to survive
|
||||
// as `name=`, not be dropped. Proton appends its own dxvk/nvapi/steam.exe
|
||||
// defaults after everything - an app override for one of those modules only
|
||||
// takes effect where Proton itself has no default.
|
||||
function dllOverridesFor(game, env) {
|
||||
const parts = []
|
||||
const inherited = String(env.WINEDLLOVERRIDES || '').trim().replace(/;+$/, '')
|
||||
if (inherited) parts.push(inherited)
|
||||
for (const entry of game.dllOverrides || []) {
|
||||
if (!entry) continue
|
||||
const dll = sanitizeDllName(entry.dll)
|
||||
if (!dll) continue
|
||||
const setting = String(entry.setting == null ? '' : entry.setting).trim()
|
||||
if (!/^[a-z,]*$/i.test(setting)) continue
|
||||
parts.push(`${dll}=${setting}`)
|
||||
}
|
||||
return parts.join(';')
|
||||
}
|
||||
|
||||
// Wine only ever repairs links that are BROKEN: a prefix that came from
|
||||
// another machine (or from an older PLWL) can still hold absolute links into
|
||||
// its build that happen to resolve here, and those would be synced out again.
|
||||
|
|
@ -74,23 +142,41 @@ function buildEnv({ game, wine }) {
|
|||
const isProton = wine && wine.type === 'proton'
|
||||
// same fallback the games service snapshots into its running entries
|
||||
// (library.prefixNameForGame), so in-use checks always name this folder
|
||||
const prefixDir = prefixPath(prefixNameForGame(game))
|
||||
const prefixName = prefixNameForGame(game)
|
||||
// a prefix with no pfx yet gets the shared-link layout, so every launch -
|
||||
// wine, Proton, tools - lands on that prefix's ONE C: drive; an existing
|
||||
// nested layout is left as it is (see ensureUnifiedPrefix)
|
||||
try {
|
||||
ensureUnifiedPrefix(prefixName)
|
||||
} catch {}
|
||||
const prefixDir = prefixPath(prefixName)
|
||||
|
||||
if (isProton) {
|
||||
const fakeSteam = ensureFakeSteam()
|
||||
const buildRoot = path.resolve(wine.path, '..')
|
||||
env.STEAM_COMPAT_CLIENT_INSTALL_PATH = fakeSteam
|
||||
env.STEAM_COMPAT_DATA_PATH = path.join(DIRS.DATA, 'compat', game.id)
|
||||
env.STEAM_COMPAT_APP_ID = '0'
|
||||
env.SteamAppId = '0'
|
||||
env.SteamGameId = '0'
|
||||
// Launch like the UMU protocol does (UMU/PortProton/Heroic): with UMU_ID
|
||||
// set, newer Protons (incl. DW Proton 11, GE-Proton 10+) run the game via
|
||||
// their built-in umu.exe instead of the steam.exe helper, which would
|
||||
// otherwise try to load Steam's steamclient.so and crash with an
|
||||
// "Assertion failed \"!status\"" dialog (steamclient_main.c) when no
|
||||
// Steam client is installed.
|
||||
if (!env.UMU_ID) env.UMU_ID = 'umu-0'
|
||||
// Proton's session init reads STEAM_COMPAT_CLIENT_INSTALL_PATH on EVERY
|
||||
// verb: its appcache sync (g_compatdata.sync_appcache) runs even for
|
||||
// runinprefix and does os.environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] as
|
||||
// soon as SteamGameId is set, so the variable must never be dropped - a
|
||||
// missing one aborts the launch with a KeyError before wine even starts.
|
||||
// Only what it POINTS AT depends on the mode: a UMU run may use a real
|
||||
// client found on this machine, a disableUmu run always gets the empty
|
||||
// fake folder (runinprefix copies nothing out of it; no steam.exe and no
|
||||
// umu.exe is started there).
|
||||
if (game.disableUmu) {
|
||||
delete env.UMU_ID
|
||||
env.STEAM_COMPAT_CLIENT_INSTALL_PATH = ensureFakeSteam()
|
||||
} else {
|
||||
env.STEAM_COMPAT_CLIENT_INSTALL_PATH = steamClientDir()
|
||||
// Launch like the UMU protocol does (UMU/PortProton/Heroic): with UMU_ID
|
||||
// set, newer Protons (incl. GE-Proton 9+) run the game through their
|
||||
// built-in umu.exe instead of the steam.exe helper, which needs the
|
||||
// Steam client's DLLs and dies in a "steamclient_main.c" assert dialog
|
||||
// without them (see steamClientDir).
|
||||
if (!env.UMU_ID) env.UMU_ID = 'umu-0'
|
||||
}
|
||||
env.WINEPREFIX = path.join(env.STEAM_COMPAT_DATA_PATH, 'pfx')
|
||||
env.PROTON_LOG_DIR = path.join(DIRS.DATA, 'logs')
|
||||
fs.mkdirSync(env.PROTON_LOG_DIR, { recursive: true })
|
||||
|
|
@ -108,16 +194,25 @@ function buildEnv({ game, wine }) {
|
|||
env.WINEPREFIX = prefixDir
|
||||
}
|
||||
|
||||
env.WINEDLLOVERRIDES = (game.dllOverrides || [])
|
||||
.filter(o => o && o.dll && o.setting)
|
||||
.map(o => `${o.dll}=${o.setting}`)
|
||||
.join(';')
|
||||
if (!env.WINEDLLOVERRIDES) delete env.WINEDLLOVERRIDES
|
||||
const overrides = dllOverridesFor(game, env)
|
||||
if (overrides) env.WINEDLLOVERRIDES = overrides
|
||||
else delete env.WINEDLLOVERRIDES
|
||||
|
||||
// the app's own environment comes last: it overrides everything above,
|
||||
// including a WINEDLLOVERRIDES configured here
|
||||
for (const [k, v] of Object.entries(game.env || {})) env[k] = v
|
||||
return { env, prefixDir }
|
||||
}
|
||||
|
||||
// Proton verbs: 'run' goes through the build's umu.exe/steam.exe launcher,
|
||||
// while 'runinprefix' runs the build's wine directly (same session env, no
|
||||
// umu/steam launcher ever starts - see game.disableUmu). runinprefix also skips
|
||||
// Proton's prefix setup, so a prefix that never saw a normal Proton run stays
|
||||
// a plain wine prefix instead of gaining Proton's default DLLs.
|
||||
function protonVerb(game) {
|
||||
return game.disableUmu ? 'runinprefix' : 'run'
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const out = []
|
||||
const re = /"([^"]*)"|'([^']*)'|(\S+)/g
|
||||
|
|
@ -167,7 +262,7 @@ async function runGame({ game, wine, systemWine, onExit }) {
|
|||
let base
|
||||
if (isProton) {
|
||||
cmd = wine.wineBin || path.join(wine.path, 'proton')
|
||||
base = ['run', exe, ...argList]
|
||||
base = [protonVerb(game), exe, ...argList]
|
||||
} else {
|
||||
cmd = systemWine ? 'wine' : wine.wineBin
|
||||
base = [exe, ...argList]
|
||||
|
|
@ -200,6 +295,13 @@ async function runGame({ game, wine, systemWine, onExit }) {
|
|||
started = true
|
||||
const logFile = latestLogPath(game.id)
|
||||
const ws = openLatestLog(game.id)
|
||||
// a launch line of our own before the raw wine output: when a game
|
||||
// refuses to start, "which prefix, which overrides, umu or steam.exe"
|
||||
// is exactly what has to be answerable from the log
|
||||
ws.write(`[plwl] ${cmd} ${base.join(' ')}\n`)
|
||||
ws.write(`[plwl] WINEPREFIX=${env.WINEPREFIX}\n`)
|
||||
if (env.WINEDLLOVERRIDES) ws.write(`[plwl] WINEDLLOVERRIDES=${env.WINEDLLOVERRIDES}\n`)
|
||||
if (isProton) ws.write(`[plwl] proton launcher: ${game.disableUmu ? 'direct wine (no UMU, no Steam)' : `umu.exe (UMU_ID=${env.UMU_ID})`}\n`)
|
||||
child.stdout.pipe(ws)
|
||||
child.stderr.pipe(ws)
|
||||
child.unref()
|
||||
|
|
@ -237,7 +339,7 @@ async function runTool({ game, wine, tool, args, systemWine }) {
|
|||
let base
|
||||
if (isProton) {
|
||||
cmd = wine.wineBin || path.join(wine.path, 'proton')
|
||||
base = ['run', tool, ...argList]
|
||||
base = [protonVerb(game), tool, ...argList]
|
||||
} else if (systemWine) {
|
||||
cmd = tool
|
||||
base = argList
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue