From dfac5806ea45196279a482f0f260b0bf43c3d6c4 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sat, 11 Jul 2026 03:56:00 +0000 Subject: [PATCH 1/3] Fix component install sessions and store mono/gecko auto-install - Dependency-install sessions no longer join or publish background sessions, fixing the hang/black screen after an installer finished and the error on manual exit; component installs fail fast if a session is already running - Mono/gecko auto-install runs for Steam, GOG and Epic launches and is skipped when any version is already installed (including component-list installs) - Mono detection also probes nested files/lib/wine layouts and records the actually installed version - Auto-install wine-gecko 2.47.4 (x86 + x86_64) alongside mono --- .../stores/steam/SteamClientManager.kt | 65 +++++++++- .../content/component/ComponentInstaller.kt | 4 + .../display/XServerDisplayActivity.java | 112 +++++++++++++++--- 3 files changed, 156 insertions(+), 25 deletions(-) diff --git a/app/src/main/feature/stores/steam/SteamClientManager.kt b/app/src/main/feature/stores/steam/SteamClientManager.kt index 1d44b8a8d..53042afcf 100644 --- a/app/src/main/feature/stores/steam/SteamClientManager.kt +++ b/app/src/main/feature/stores/steam/SteamClientManager.kt @@ -321,19 +321,26 @@ object SteamClientManager { val candidates = mutableListOf() + // Proton-style trees nest the wine root under files/. + val mscoreeSubPaths = + listOf( + "lib/wine/aarch64-windows/mscoree.dll", + "lib/wine/x86_64-windows/mscoree.dll", + "lib/wine/i386-windows/mscoree.dll", + "files/lib/wine/aarch64-windows/mscoree.dll", + "files/lib/wine/x86_64-windows/mscoree.dll", + "files/lib/wine/i386-windows/mscoree.dll", + ) + if (containerWinePath != null) { val wineDir = File(containerWinePath) - candidates.add(File(wineDir, "lib/wine/aarch64-windows/mscoree.dll")) - candidates.add(File(wineDir, "lib/wine/x86_64-windows/mscoree.dll")) - candidates.add(File(wineDir, "lib/wine/i386-windows/mscoree.dll")) + mscoreeSubPaths.forEach { candidates.add(File(wineDir, it)) } Log.d(TAG, "Mono detection: prioritizing container Wine path: $containerWinePath") } for (typeDir in listOf(File(contentsDir, "Wine"), File(contentsDir, "Proton"))) { typeDir.listFiles()?.forEach { buildDir -> - candidates.add(File(buildDir, "lib/wine/aarch64-windows/mscoree.dll")) - candidates.add(File(buildDir, "lib/wine/x86_64-windows/mscoree.dll")) - candidates.add(File(buildDir, "lib/wine/i386-windows/mscoree.dll")) + mscoreeSubPaths.forEach { candidates.add(File(buildDir, it)) } } } @@ -529,6 +536,52 @@ object SteamClientManager { return "Z:\\opt\\mono-gecko-offline\\${msiFile.name}" } + const val GECKO_VERSION = "2.47.4" + + // Return the Wine Z:\ paths to the Gecko MSIs (x86 + x86_64), downloading them if needed. + @JvmStatic + fun getGeckoMsiWinePaths(context: Context): List { + val geckoDir = File(ImageFs.find(context).rootDir, "opt/mono-gecko-offline") + geckoDir.mkdirs() + val out = mutableListOf() + for (name in listOf("wine_gecko-$GECKO_VERSION-x86.msi", "wine_gecko-$GECKO_VERSION-x86_64.msi")) { + val msiFile = File(geckoDir, name) + if (!msiFile.exists() || msiFile.length() == 0L) { + val downloadUrl = "https://dl.winehq.org/wine/wine-gecko/$GECKO_VERSION/$name" + Log.i(TAG, "Downloading Gecko from $downloadUrl") + try { + val tmpFile = File(geckoDir, "$name.tmp") + val connection = URL(downloadUrl).openConnection() as HttpURLConnection + connection.connectTimeout = 30_000 + connection.readTimeout = 60_000 + connection.instanceFollowRedirects = true + val code = connection.responseCode + if (code != 200) { + Log.w(TAG, "Gecko $name -> HTTP $code") + connection.disconnect() + continue + } + connection.inputStream.use { input -> + FileOutputStream(tmpFile).use { output -> input.copyTo(output, bufferSize = 65536) } + } + connection.disconnect() + if (!tmpFile.renameTo(msiFile)) { + Log.e(TAG, "Rename failed for $name.tmp") + tmpFile.delete() + continue + } + Log.i(TAG, "Gecko $name downloaded (${msiFile.length()} B)") + } catch (e: Exception) { + Log.w(TAG, "Gecko $name download error: ${e.message}") + continue + } + } + chmodIfExists(msiFile) + out.add("Z:\\opt\\mono-gecko-offline\\$name") + } + return out + } + // Blocking Java wrapper for encrypted app tickets. @JvmStatic fun getEncryptedAppTicketBase64Blocking(appId: Int): String? { diff --git a/app/src/main/runtime/content/component/ComponentInstaller.kt b/app/src/main/runtime/content/component/ComponentInstaller.kt index 989e13c10..072b7b62b 100644 --- a/app/src/main/runtime/content/component/ComponentInstaller.kt +++ b/app/src/main/runtime/content/component/ComponentInstaller.kt @@ -7,6 +7,7 @@ import com.winlator.cmod.runtime.container.Container import com.winlator.cmod.runtime.content.Downloader import com.winlator.cmod.runtime.display.XServerDisplayActivity import com.winlator.cmod.runtime.display.environment.ImageFs +import com.winlator.cmod.runtime.system.SessionKeepAliveService import com.winlator.cmod.runtime.wine.WineRegistryEditor import com.winlator.cmod.shared.io.FileUtils import com.winlator.cmod.shared.io.TarCompressorUtils @@ -405,6 +406,9 @@ class ComponentInstaller( bootExe: String, bootArgs: String, ): Int { + if (SessionKeepAliveService.isSessionActive() && SessionKeepAliveService.getActiveEnvironment() != null) { + throw InstallException("A session is still running — close it before installing components.") + } DependencyInstallBridge.begin() val intent = Intent(context, XServerDisplayActivity::class.java).apply { diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index cc095b365..85a0ec55b 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -1540,7 +1540,8 @@ public void handleOnBackPressed() { cachedPreloaderSubtitle = container != null ? container.getName() : ""; showLaunchPreloader(getString(R.string.preloader_initializing)); - if (preferences.getBoolean("enable_background_session", false)) { + // Dependency-install sessions must not become background/reattachable sessions. + if (!isDependencyInstall && preferences.getBoolean("enable_background_session", false)) { SessionKeepAliveService.startSession(this); } @@ -6447,7 +6448,10 @@ private static boolean safeEquals(String a, String b) { } private void setupXEnvironment() throws PackageManager.NameNotFoundException { - if (SessionKeepAliveService.isSessionActive()) { + // Never reattach for a dependency install: the boot exe must run in a fresh + // environment, and a stale activeEnvironment from a just-closed session would + // swallow the launch (black screen, bridge never completes). + if (!isDependencyInstall && SessionKeepAliveService.isSessionActive()) { XEnvironment existingEnv = SessionKeepAliveService.getActiveEnvironment(); XServer existingXServer = SessionKeepAliveService.getActiveXServer(); if (existingEnv != null && existingXServer != null) { @@ -6572,7 +6576,8 @@ private void setupXEnvironment() throws PackageManager.NameNotFoundException { guestProgramLauncherComponent.setFEXCorePreset(effectiveFEXCorePreset); // Steam preUnpack installs prerequisites before game launch. - boolean isSteamGameForUnpack = shortcut != null && "STEAM".equals(shortcut.getExtra("game_source")); + String prereqGameSource = shortcut != null ? shortcut.getExtra("game_source") : null; + boolean isSteamGameForUnpack = "STEAM".equals(prereqGameSource); if (isSteamGameForUnpack) { guestProgramLauncherComponent.setPreUnpack(() -> { try { @@ -6591,6 +6596,15 @@ private void setupXEnvironment() throws PackageManager.NameNotFoundException { Log.e("XServerDisplayActivity", "preUnpack failed", e); } }); + } else if ("GOG".equals(prereqGameSource) || "EPIC".equals(prereqGameSource)) { + guestProgramLauncherComponent.setPreUnpack(() -> { + try { + installMonoIfNeeded(guestProgramLauncherComponent); + installGeckoIfNeeded(guestProgramLauncherComponent); + } catch (Exception e) { + Log.e("XServerDisplayActivity", "preUnpack failed", e); + } + }); } } @@ -6917,8 +6931,10 @@ private void setupXEnvironment() throws PackageManager.NameNotFoundException { preloaderDialog.setStepOnUiThread(R.string.preloader_starting_wine); } environment.startEnvironmentComponents(); - SessionKeepAliveService.setActiveEnvironment(environment); - SessionKeepAliveService.setActiveXServer(xServer); + if (!isDependencyInstall) { + SessionKeepAliveService.setActiveEnvironment(environment); + SessionKeepAliveService.setActiveXServer(xServer); + } } winHandler.start(); @@ -9363,6 +9379,8 @@ private void runPreGameSetup(GuestProgramLauncherComponent launcher, boolean needsUnpacking, boolean unpackFiles) { boolean monoReady = installMonoIfNeeded(launcher); + installGeckoIfNeeded(launcher); + installRedistributablesIfNeeded(launcher); if (!unpackFiles) { @@ -9516,6 +9534,17 @@ private boolean doesUnpackedExeExist() { } private boolean installMonoIfNeeded(GuestProgramLauncherComponent launcher) { + // Any installed Mono is kept as-is; prefix repair clears the marker to force a reinstall. + String installedVersion = container.getExtra("mono_version", null); + if (installedVersion != null) { + Log.d("XServerDisplayActivity", "Mono v" + installedVersion + " already installed in container " + container.id + ", skipping"); + return true; + } + if (hasInstalledComponentPrefix("mono")) { + Log.d("XServerDisplayActivity", "Mono already installed via components in container " + container.id + ", skipping"); + return true; + } + String winePath = wineInfo != null ? wineInfo.path : null; String requiredVersion = SteamClientManager.detectRequiredMonoVersion(this, winePath); @@ -9524,32 +9553,31 @@ private boolean installMonoIfNeeded(GuestProgramLauncherComponent launcher) { return false; } - String installedVersion = container.getExtra("mono_version", null); - if (requiredVersion.equals(installedVersion)) { - Log.d("XServerDisplayActivity", "Mono v" + installedVersion + " already installed in container " + container.id + ", skipping"); - return true; - } - - if (installedVersion != null) { - Log.w("XServerDisplayActivity", "Mono version mismatch in container " + container.id - + ": installed v" + installedVersion + " but need v" + requiredVersion + " — reinstalling"); - } - String monoWinePath = SteamClientManager.getMonoMsiWinePath(this, winePath); if (monoWinePath == null) { Log.w("XServerDisplayActivity", "Mono MSI not available (no internet?), will retry next launch"); return false; } + // The MSI actually resolved may be a fallback version; record what really got installed. + String actualVersion = requiredVersion; + java.util.regex.Matcher monoMsiMatcher = + java.util.regex.Pattern.compile("wine-mono-(\\d+\\.\\d+\\.\\d+)").matcher(monoWinePath); + if (monoMsiMatcher.find()) actualVersion = monoMsiMatcher.group(1); + if (!actualVersion.equals(requiredVersion)) { + Log.w("XServerDisplayActivity", "Mono fallback: required v" + requiredVersion + + " but installing v" + actualVersion + " (" + monoWinePath + ")"); + } + try { - Log.d("XServerDisplayActivity", "Installing Wine Mono v" + requiredVersion + Log.d("XServerDisplayActivity", "Installing Wine Mono v" + actualVersion + " (" + monoWinePath + ") in container " + container.id + "..."); String monoCmd = "wine msiexec /i " + monoWinePath + " && wineserver -k"; launcher.execShellCommand(monoCmd); container.putExtra("mono_installed", "true"); - container.putExtra("mono_version", requiredVersion); + container.putExtra("mono_version", actualVersion); container.saveData(); - Log.d("XServerDisplayActivity", "Mono v" + requiredVersion + " installed in container " + container.id); + Log.d("XServerDisplayActivity", "Mono v" + actualVersion + " installed in container " + container.id); return true; } catch (Exception e) { Log.w("XServerDisplayActivity", "Mono msiexec failed, will retry next launch", e); @@ -9557,6 +9585,52 @@ private boolean installMonoIfNeeded(GuestProgramLauncherComponent launcher) { } } + private boolean hasInstalledComponentPrefix(String prefix) { + for (String name : com.winlator.cmod.runtime.content.component.ComponentInstaller + .installedComponents(container)) { + if (name.startsWith(prefix)) return true; + } + return false; + } + + private void installGeckoIfNeeded(GuestProgramLauncherComponent launcher) { + String installedGecko = container.getExtra("gecko_version", null); + if (installedGecko != null) { + Log.d("XServerDisplayActivity", "Gecko v" + installedGecko + " already installed in container " + + container.id + ", skipping"); + return; + } + if (hasInstalledComponentPrefix("gecko")) { + Log.d("XServerDisplayActivity", "Gecko already installed via components in container " + + container.id + ", skipping"); + return; + } + String geckoVersion = SteamClientManager.GECKO_VERSION; + + java.util.List geckoWinePaths = SteamClientManager.getGeckoMsiWinePaths(this); + if (geckoWinePaths.size() < 2) { + Log.w("XServerDisplayActivity", "Gecko MSIs not available (no internet?), will retry next launch"); + return; + } + + try { + Log.d("XServerDisplayActivity", "Installing Wine Gecko v" + geckoVersion + + " in container " + container.id + "..."); + StringBuilder geckoCmd = new StringBuilder(); + for (String p : geckoWinePaths) { + if (geckoCmd.length() > 0) geckoCmd.append(" && "); + geckoCmd.append("wine msiexec /i ").append(p); + } + geckoCmd.append(" && wineserver -k"); + launcher.execShellCommand(geckoCmd.toString()); + container.putExtra("gecko_version", geckoVersion); + container.saveData(); + Log.d("XServerDisplayActivity", "Gecko v" + geckoVersion + " installed in container " + container.id); + } catch (Exception e) { + Log.w("XServerDisplayActivity", "Gecko msiexec failed, will retry next launch", e); + } + } + // Installs _CommonRedist once per game/container. private void installRedistributablesIfNeeded(GuestProgramLauncherComponent launcher) { if (shortcut == null || !"STEAM".equals(shortcut.getExtra("game_source"))) return; From 232fb78fcccac32a1da8604e70b3faa4dfe6e18f Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sat, 11 Jul 2026 04:22:09 +0000 Subject: [PATCH 2/3] Add VKD3D_SHADER_MODEL to variables dropdown with preset and custom values New SELECT_CUSTOM env var type: value dropdown offers 6_9/6_6/6_0/5_0 plus a Custom entry that switches to a free-text field --- app/src/main/feature/library/GameSettings.kt | 111 ++++++++++++++++++ .../main/shared/ui/widget/EnvVarsView.java | 1 + 2 files changed, 112 insertions(+) diff --git a/app/src/main/feature/library/GameSettings.kt b/app/src/main/feature/library/GameSettings.kt index 8e4aa0e86..d029297ee 100644 --- a/app/src/main/feature/library/GameSettings.kt +++ b/app/src/main/feature/library/GameSettings.kt @@ -3065,6 +3065,14 @@ private fun EnvVarValueEditor( onSelected = onValueChange ) } + "SELECT_CUSTOM" -> { + val options = known!!.drop(2) + EnvValueDropdownWithCustom( + current = value, + options = options, + onChanged = onValueChange + ) + } "SELECT_MULTIPLE" -> { val options = known!!.drop(2) EnvValueMultiDropdown( @@ -3140,6 +3148,109 @@ private fun EnvValueDropdown( } } +@Composable +private fun EnvValueDropdownWithCustom( + current: String, + options: List, + onChanged: (String) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + val menuOffset = rememberSmartDropdownOffset() + var customMode by remember { mutableStateOf(current.isNotEmpty() && current !in options) } + Box { + if (customMode) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.weight(1f)) { + EnvValueTextField(current, onChanged) + } + Spacer(Modifier.width(4.dp)) + Box( + modifier = Modifier + .size(EnvVarControlHeight) + .clip(RoundedCornerShape(8.dp)) + .background(InputSurface) + .border(1.dp, AccentBlue.copy(alpha = 0.3f), RoundedCornerShape(8.dp)) + .paneNavItem(cornerRadius = 8.dp, onActivate = { expanded = true }, highlightColor = NavHighlight) + .smartDropdownAnchor(offset = menuOffset) { expanded = true }, + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Outlined.KeyboardArrowDown, + contentDescription = null, + tint = TextSecondary, + modifier = Modifier.size(16.dp) + ) + } + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(EnvVarControlHeight) + .clip(RoundedCornerShape(8.dp)) + .background(InputSurface) + .border(1.dp, AccentBlue.copy(alpha = 0.3f), RoundedCornerShape(8.dp)) + .paneNavItem(cornerRadius = 8.dp, onActivate = { expanded = true }, highlightColor = NavHighlight) + .smartDropdownAnchor(offset = menuOffset) { expanded = true } + .padding(horizontal = SettingFieldHorizontalPadding), + contentAlignment = Alignment.CenterStart + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + if (current.isEmpty()) options.firstOrNull().orEmpty() else current, + color = TextPrimary, + fontSize = SettingValueSize, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Icon( + Icons.Outlined.KeyboardArrowDown, + contentDescription = null, + tint = TextSecondary, + modifier = Modifier.size(16.dp) + ) + } + } + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + offset = menuOffset.value, + shape = RoundedCornerShape(8.dp), + containerColor = CardSurface, + modifier = Modifier.width(220.dp) + ) { + options.forEach { opt -> + DropdownMenuItem( + text = { Text(opt, color = TextPrimary, fontSize = SettingValueSize) }, + onClick = { + customMode = false + onChanged(opt) + expanded = false + } + ) + } + Box(Modifier.fillMaxWidth().height(1.dp).background(DividerColor)) + DropdownMenuItem( + text = { + Text( + stringResource(R.string.common_ui_custom), + color = AccentBlue, + fontSize = SettingValueSize, + fontWeight = FontWeight.Medium + ) + }, + onClick = { + customMode = true + onChanged("") + expanded = false + } + ) + } + } +} + @Composable private fun EnvValueMultiDropdown( current: String, diff --git a/app/src/main/shared/ui/widget/EnvVarsView.java b/app/src/main/shared/ui/widget/EnvVarsView.java index 58642070b..21fc460e6 100644 --- a/app/src/main/shared/ui/widget/EnvVarsView.java +++ b/app/src/main/shared/ui/widget/EnvVarsView.java @@ -98,6 +98,7 @@ public class EnvVarsView extends FrameLayout { "samplers" }, {"DXVK_DISABLE_TIMELINE_SEMAPHORES", "CHECKBOX", "0", "1"}, + {"VKD3D_SHADER_MODEL", "SELECT_CUSTOM", "6_9", "6_6", "6_0", "5_0"}, {"MESA_EXTENSION_MAX_YEAR", "TEXT"}, {"WRAPPER_MAX_IMAGE_COUNT", "TEXT"}, {"MESA_GL_VERSION_OVERRIDE", "TEXT"}, From a0846e2b897dd1606d2004f47a123f720982fa3e Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sat, 11 Jul 2026 04:50:46 +0000 Subject: [PATCH 3/3] Variables: allow duplicate names, reorder VKD3D entry, shorten ALSA var names - Name dropdown no longer greys out used names; duplicate rows survive save/reload via row-preserving parse (last row wins at launch) - VKD3D_SHADER_MODEL moved next to WINEDLLOVERRIDES in the catalog - WINNATIVE_ALSA_* dropdown entries renamed to ALSA_*; reader accepts bare names first and keeps old prefixes as fallback --- app/src/main/feature/library/GameSettings.kt | 57 ++++++++----------- .../ContainerSettingsComposeDialog.kt | 7 +-- .../ShortcutSettingsComposeDialog.kt | 16 ++---- .../runtime/audio/alsaserver/ALSAClient.java | 26 +++++++-- .../main/shared/ui/widget/EnvVarsView.java | 10 ++-- 5 files changed, 57 insertions(+), 59 deletions(-) diff --git a/app/src/main/feature/library/GameSettings.kt b/app/src/main/feature/library/GameSettings.kt index d029297ee..c6ac635a9 100644 --- a/app/src/main/feature/library/GameSettings.kt +++ b/app/src/main/feature/library/GameSettings.kt @@ -327,6 +327,13 @@ private fun Modifier.smartDropdownAnchor( data class WinComponentItem(val key: String, val label: String, val selectedIndex: Int) data class EnvVarItem(val key: String, val value: String) + +// Row-preserving parse: duplicate names stay as separate rows (EnvVars would collapse them into a map). +fun parseEnvVarItems(envVarsStr: String?): List = + envVarsStr.orEmpty().split(" ").mapNotNull { part -> + val index = part.indexOf('=') + if (index <= 0) null else EnvVarItem(part.substring(0, index), part.substring(index + 1)) + } data class ExtraArgGroup(val header: String, val args: List) data class DriveItem( val letter: String, @@ -2572,16 +2579,10 @@ private fun VariablesSection( EnvVarRow( name = envVar.key, value = envVar.value, - excludeOtherNames = state.envVars.value - .filterIndexed { i, _ -> i != index } - .map { it.key } - .toSet(), onNameChange = { newKey -> val normalizedKey = newKey.trim() val list = state.envVars.value.toMutableList() - val isUnique = normalizedKey.isEmpty() || - state.envVars.value.none { it.key == normalizedKey } - if (index in list.indices && isUnique) { + if (index in list.indices) { list[index] = EnvVarItem(normalizedKey, envVar.value) state.envVars.value = list } @@ -2850,7 +2851,6 @@ private fun DriveLetterSelector( private fun EnvVarRow( name: String, value: String, - excludeOtherNames: Set, onNameChange: (String) -> Unit, onValueChange: (String) -> Unit, onRemove: (() -> Unit)?, @@ -2964,33 +2964,22 @@ private fun EnvVarRow( ) Box(Modifier.fillMaxWidth().height(1.dp).background(DividerColor)) - val allKnown = EnvVarsView.knownEnvVars.map { it[0] } - val unselected = allKnown - .filter { it !in excludeOtherNames && it != name } - .sortedBy { it.uppercase() } - val selected = allKnown - .filter { it in excludeOtherNames } + EnvVarsView.knownEnvVars + .map { it[0] } .sortedBy { it.uppercase() } - - (unselected + selected).forEach { knownName -> - val disabled = knownName != name && knownName in excludeOtherNames - DropdownMenuItem( - enabled = !disabled, - text = { - Text( - knownName, - color = if (disabled) TextDim else TextPrimary, - fontSize = SettingValueSize - ) - }, - onClick = { - isCustomMode = false - customText = "" - onNameChange(knownName) - nameMenuExpanded = false - } - ) - } + .forEach { knownName -> + DropdownMenuItem( + text = { + Text(knownName, color = TextPrimary, fontSize = SettingValueSize) + }, + onClick = { + isCustomMode = false + customText = "" + onNameChange(knownName) + nameMenuExpanded = false + } + ) + } } } Spacer(Modifier.width(6.dp)) diff --git a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt index 43bd09c32..4f8c15efe 100644 --- a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt +++ b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt @@ -34,6 +34,7 @@ import com.winlator.cmod.feature.library.GameSettingsContent import com.winlator.cmod.feature.library.GameSettingsNav import com.winlator.cmod.feature.library.GameSettingsStateHolder import com.winlator.cmod.feature.library.WinComponentItem +import com.winlator.cmod.feature.library.parseEnvVarItems import com.winlator.cmod.runtime.compat.box64.Box64Preset import com.winlator.cmod.runtime.compat.box64.Box64PresetManager import com.winlator.cmod.runtime.container.Container @@ -452,10 +453,8 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( state.generalComponents.value = general val envVarsStr = c?.getEnvVars() ?: Container.DEFAULT_ENV_VARS - val envVars = EnvVars(envVarsStr) - val items = mutableListOf() - for (key in envVars) items.add(EnvVarItem(key, envVars.get(key))) - state.sdl2Compatibility.value = envVars.get("SDL_XINPUT_ENABLED") == "1" + val items = parseEnvVarItems(envVarsStr) + state.sdl2Compatibility.value = EnvVars(envVarsStr).get("SDL_XINPUT_ENABLED") == "1" state.envVars.value = if (state.sdl2Compatibility.value) { items.filterNot { it.key in SDL2_KEYS } } else items diff --git a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt index e7e40b1bf..05604ba5d 100644 --- a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt +++ b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt @@ -32,6 +32,7 @@ import com.winlator.cmod.R import com.winlator.cmod.app.PluviaApp import com.winlator.cmod.feature.library.DriveItem import com.winlator.cmod.feature.library.EnvVarItem +import com.winlator.cmod.feature.library.parseEnvVarItems import androidx.compose.runtime.getValue import com.winlator.cmod.feature.library.GameSettingsCallbacks import com.winlator.cmod.feature.library.GameSettingsContent @@ -943,15 +944,11 @@ class ShortcutSettingsComposeDialog private constructor( "envVars", container?.getEnvVars() ?: Container.DEFAULT_ENV_VARS ) - val envVars = EnvVars(envVarsStr) - val items = mutableListOf() - for (key in envVars) { - items.add(EnvVarItem(key, envVars.get(key))) - } + val items = parseEnvVarItems(envVarsStr) state.envVars.value = items // Hide SDL2 keys from the user-visible list when the toggle is on. - state.sdl2Compatibility.value = envVars.get("SDL_XINPUT_ENABLED") == "1" + state.sdl2Compatibility.value = EnvVars(envVarsStr).get("SDL_XINPUT_ENABLED") == "1" if (state.sdl2Compatibility.value) { state.envVars.value = items.filterNot { item -> sdl2EnvVars.any { it.first == item.key } @@ -2205,10 +2202,9 @@ class ShortcutSettingsComposeDialog private constructor( state.directXComponents.value = directX state.generalComponents.value = general - val envVars = EnvVars(container.getEnvVars() ?: Container.DEFAULT_ENV_VARS) - val items = mutableListOf() - for (key in envVars) items.add(EnvVarItem(key, envVars.get(key))) - state.sdl2Compatibility.value = envVars.get("SDL_XINPUT_ENABLED") == "1" + val containerEnvVarsStr = container.getEnvVars() ?: Container.DEFAULT_ENV_VARS + val items = parseEnvVarItems(containerEnvVarsStr) + state.sdl2Compatibility.value = EnvVars(containerEnvVarsStr).get("SDL_XINPUT_ENABLED") == "1" state.envVars.value = if (state.sdl2Compatibility.value) { items.filterNot { item -> sdl2EnvVars.any { it.first == item.key } } } else items diff --git a/app/src/main/runtime/audio/alsaserver/ALSAClient.java b/app/src/main/runtime/audio/alsaserver/ALSAClient.java index fa7f95f41..ea4174887 100644 --- a/app/src/main/runtime/audio/alsaserver/ALSAClient.java +++ b/app/src/main/runtime/audio/alsaserver/ALSAClient.java @@ -59,25 +59,36 @@ public static Options fromEnvVars(EnvVars envVars) { options.latencyMillis = parseInt( - firstNonEmpty(envVars.get("ANDROID_ALSA_LATENCY_MS"), envVars.get("WINNATIVE_ALSA_LATENCY_MS")), + firstNonEmpty( + envVars.get("ALSA_LATENCY_MS"), + envVars.get("ANDROID_ALSA_LATENCY_MS"), + envVars.get("WINNATIVE_ALSA_LATENCY_MS")), DEFAULT_LATENCY_MILLIS); options.latencyMillis = Math.max(0, options.latencyMillis); options.volume = parseFloat( - firstNonEmpty(envVars.get("ANDROID_ALSA_VOLUME"), envVars.get("WINNATIVE_ALSA_VOLUME")), + firstNonEmpty( + envVars.get("ALSA_VOLUME"), + envVars.get("ANDROID_ALSA_VOLUME"), + envVars.get("WINNATIVE_ALSA_VOLUME")), DEFAULT_VOLUME); options.volume = Math.max(0.0f, Math.min(options.volume, MAX_VOLUME)); options.bassBoost = parseFloat( - firstNonEmpty(envVars.get("ANDROID_ALSA_BASS_BOOST"), envVars.get("WINNATIVE_ALSA_BASS_BOOST")), + firstNonEmpty( + envVars.get("ALSA_BASS_BOOST"), + envVars.get("ANDROID_ALSA_BASS_BOOST"), + envVars.get("WINNATIVE_ALSA_BASS_BOOST")), DEFAULT_BASS_BOOST); options.bassBoost = Math.max(0.0f, Math.min(options.bassBoost, MAX_BASS_BOOST)); String performanceMode = firstNonEmpty( - envVars.get("ANDROID_ALSA_PERFORMANCE_MODE"), envVars.get("WINNATIVE_ALSA_PERFORMANCE_MODE")); + envVars.get("ALSA_PERFORMANCE_MODE"), + envVars.get("ANDROID_ALSA_PERFORMANCE_MODE"), + envVars.get("WINNATIVE_ALSA_PERFORMANCE_MODE")); if (performanceMode.equalsIgnoreCase("low_latency") || performanceMode.equals("1")) { options.performanceMode = AudioTrack.PERFORMANCE_MODE_LOW_LATENCY; } else if (performanceMode.equalsIgnoreCase("power_saving") || performanceMode.equals("2")) { @@ -89,8 +100,11 @@ public static Options fromEnvVars(EnvVars envVars) { return options; } - private static String firstNonEmpty(String first, String second) { - return first != null && !first.isEmpty() ? first : (second != null ? second : ""); + private static String firstNonEmpty(String... values) { + for (String value : values) { + if (value != null && !value.isEmpty()) return value; + } + return ""; } private static int parseInt(String value, int fallback) { diff --git a/app/src/main/shared/ui/widget/EnvVarsView.java b/app/src/main/shared/ui/widget/EnvVarsView.java index 21fc460e6..8c684e1ad 100644 --- a/app/src/main/shared/ui/widget/EnvVarsView.java +++ b/app/src/main/shared/ui/widget/EnvVarsView.java @@ -98,18 +98,18 @@ public class EnvVarsView extends FrameLayout { "samplers" }, {"DXVK_DISABLE_TIMELINE_SEMAPHORES", "CHECKBOX", "0", "1"}, - {"VKD3D_SHADER_MODEL", "SELECT_CUSTOM", "6_9", "6_6", "6_0", "5_0"}, {"MESA_EXTENSION_MAX_YEAR", "TEXT"}, {"WRAPPER_MAX_IMAGE_COUNT", "TEXT"}, {"MESA_GL_VERSION_OVERRIDE", "TEXT"}, {"PULSE_LATENCY_MSEC", "NUMBER"}, - {"WINNATIVE_ALSA_LATENCY_MS", "NUMBER"}, - {"WINNATIVE_ALSA_VOLUME", "DECIMAL"}, - {"WINNATIVE_ALSA_BASS_BOOST", "DECIMAL"}, - {"WINNATIVE_ALSA_PERFORMANCE_MODE", "SELECT", "low_latency", "none", "power_saving"}, + {"ALSA_LATENCY_MS", "NUMBER"}, + {"ALSA_VOLUME", "DECIMAL"}, + {"ALSA_BASS_BOOST", "DECIMAL"}, + {"ALSA_PERFORMANCE_MODE", "SELECT", "low_latency", "none", "power_saving"}, {"WINE_DO_NOT_CREATE_DXGI_DEVICE_MANAGER", "CHECKBOX", "0", "1"}, {"WINE_NEW_MEDIASOURCE", "CHECKBOX", "0", "1"}, {"WINE_LARGE_ADDRESS_AWARE", "CHECKBOX", "0", "1"}, + {"VKD3D_SHADER_MODEL", "SELECT_CUSTOM", "6_9", "6_6", "6_0", "5_0"}, {"WINEDLLOVERRIDES", "TEXT"}, {"GALLIUM_HUD", "SELECT_MULTIPLE", "simple", "fps", "frametime"} };