Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 36 additions & 1 deletion app/src/main/feature/stores/steam/utils/SteamLaunchOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>, val gameArgs: String)
Expand All @@ -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)) {
Expand All @@ -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<String, String>): 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

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/runtime/display/XServerDisplayActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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()) {
Expand Down