Skip to content
Merged
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
168 changes: 134 additions & 34 deletions app/src/main/feature/library/GameSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnvVarItem> =
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<String>)
data class DriveItem(
val letter: String,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -2850,7 +2851,6 @@ private fun DriveLetterSelector(
private fun EnvVarRow(
name: String,
value: String,
excludeOtherNames: Set<String>,
onNameChange: (String) -> Unit,
onValueChange: (String) -> Unit,
onRemove: (() -> Unit)?,
Expand Down Expand Up @@ -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 }
EnvVarsView.knownEnvVars
.map { it[0] }
.sortedBy { it.uppercase() }
val selected = allKnown
.filter { it in excludeOtherNames }
.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))
Expand Down Expand Up @@ -3065,6 +3054,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(
Expand Down Expand Up @@ -3140,6 +3137,109 @@ private fun EnvValueDropdown(
}
}

@Composable
private fun EnvValueDropdownWithCustom(
current: String,
options: List<String>,
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<EnvVarItem>()
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
Expand Down
16 changes: 6 additions & 10 deletions app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -943,15 +944,11 @@ class ShortcutSettingsComposeDialog private constructor(
"envVars",
container?.getEnvVars() ?: Container.DEFAULT_ENV_VARS
)
val envVars = EnvVars(envVarsStr)
val items = mutableListOf<EnvVarItem>()
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 }
Expand Down Expand Up @@ -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<EnvVarItem>()
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
Expand Down
65 changes: 59 additions & 6 deletions app/src/main/feature/stores/steam/SteamClientManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -321,19 +321,26 @@ object SteamClientManager {

val candidates = mutableListOf<File>()

// 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)) }
}
}

Expand Down Expand Up @@ -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<String> {
val geckoDir = File(ImageFs.find(context).rootDir, "opt/mono-gecko-offline")
geckoDir.mkdirs()
val out = mutableListOf<String>()
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? {
Expand Down
Loading