diff --git a/app/src/main/feature/stores/steam/utils/SteamLaunchOptions.kt b/app/src/main/feature/stores/steam/utils/SteamLaunchOptions.kt index d1668b534..878a3cc69 100644 --- a/app/src/main/feature/stores/steam/utils/SteamLaunchOptions.kt +++ b/app/src/main/feature/stores/steam/utils/SteamLaunchOptions.kt @@ -3,6 +3,9 @@ package com.winlator.cmod.feature.stores.steam.utils /** Parses Steam-style launch options: KEY=VALUE before %command% become env vars; args after become game args. */ object SteamLaunchOptions { private val ENV_KEY = Regex("[A-Za-z_][A-Za-z0-9_]*") + // No-%command% autodetect is UPPERCASE-only so bare game args (-dx11) and lowercase + // key=value args stay game args, while real env vars (DXVK_HUD, WINEDLLOVERRIDES) are caught. + private val ENV_KEY_UPPER = Regex("[A-Z_][A-Z0-9_]*") private const val COMMAND = "%command%" data class Parsed(val env: LinkedHashMap, val gameArgs: String) @@ -13,7 +16,7 @@ object SteamLaunchOptions { val raw = execArgs?.trim().orEmpty() if (raw.isEmpty()) return Parsed(env, "") val idx = raw.indexOf(COMMAND) - if (idx < 0) return Parsed(env, raw) + if (idx < 0) return Parsed(env, raw.substring(consumeLeadingEnv(raw, env)).trim()) val before = raw.substring(0, idx).trim() val after = raw.substring(idx + COMMAND.length).trim() for (token in tokenize(before)) { @@ -25,6 +28,38 @@ object SteamLaunchOptions { return Parsed(env, after) } + // Without %command%, a leading run of UPPERCASE KEY=VALUE tokens is env vars (shell-style + // prefix); returns the offset where the first non-env token begins (game args from there, + // kept verbatim so quotes survive). == raw.length when every token was an env var. + private fun consumeLeadingEnv(raw: String, env: LinkedHashMap): Int { + var i = 0 + val n = raw.length + while (i < n) { + while (i < n && raw[i].isWhitespace()) i++ + if (i >= n) return n + val start = i + val sb = StringBuilder() + var quote: Char? = null + while (i < n && (quote != null || !raw[i].isWhitespace())) { + val c = raw[i] + when { + quote != null -> if (c == quote) quote = null else sb.append(c) + c == '"' || c == '\'' -> quote = c + else -> sb.append(c) + } + i++ + } + val token = sb.toString() + val eq = token.indexOf('=') + if (eq > 0 && ENV_KEY_UPPER.matches(token.substring(0, eq))) { + env[token.substring(0, eq)] = token.substring(eq + 1) + } else { + return start + } + } + return n + } + @JvmStatic fun gameArgs(execArgs: String?): String = parse(execArgs).gameArgs diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index c8508b6f3..00275ffd9 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -8518,6 +8518,8 @@ private String getWineStartCommand(GuestProgramLauncherComponent launcherCompone if (extraArgs == null || extraArgs.isEmpty()) { extraArgs = getIntent().getStringExtra("extra_exec_args"); } + // Strip %command%/env tokens like the Steam path so args stay consistent across stores. + extraArgs = com.winlator.cmod.feature.stores.steam.utils.SteamLaunchOptions.gameArgs(extraArgs); extraArgs = (extraArgs != null && !extraArgs.isEmpty()) ? " " + extraArgs : ""; String gameInstallPath = shortcut.getExtra("game_install_path"); @@ -8590,6 +8592,8 @@ && new File(storeInstallPath).exists()) { Log.d("XServerDisplayActivity", gameSource + " game launch: " + args); } else { String extraArgs = shortcut.getSettingExtra("execArgs", container.getExecArgs()); + // Strip %command%/env tokens like the Steam path so args stay consistent across stores. + extraArgs = com.winlator.cmod.feature.stores.steam.utils.SteamLaunchOptions.gameArgs(extraArgs); extraArgs = (extraArgs != null && !extraArgs.isEmpty()) ? " " + extraArgs : ""; String customResolvedPath = resolveCustomExecutableWinPath(shortcut); if (customResolvedPath != null && !customResolvedPath.isEmpty()) {