-
-
Notifications
You must be signed in to change notification settings - Fork 285
feat: Executable Path dropdown — PICS launch entries, .bat/.cmd, URI schemes #1459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
da125e1
00e373a
8f265f1
f530cfb
63a23a9
4e135e4
0bc89cf
cb37b72
f4caa02
715866c
d22a1de
945012d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ import androidx.compose.material3.DropdownMenuItem | |
| import androidx.compose.material3.ExperimentalMaterial3Api | ||
| import androidx.compose.material3.ExposedDropdownMenuBox | ||
| import androidx.compose.material3.ExposedDropdownMenuDefaults | ||
| import androidx.compose.material3.HorizontalDivider | ||
| import androidx.compose.material3.Icon | ||
| import androidx.compose.material3.IconButton | ||
| import androidx.compose.material3.MaterialTheme | ||
|
|
@@ -83,6 +84,7 @@ import app.gamenative.ui.theme.PluviaTheme | |
| import app.gamenative.ui.theme.settingsTileColors | ||
| import app.gamenative.ui.theme.settingsTileColorsAlt | ||
| import app.gamenative.utils.CustomGameScanner | ||
| import app.gamenative.data.LaunchInfo | ||
| import app.gamenative.utils.ContainerUtils | ||
| import app.gamenative.utils.ManifestComponentHelper | ||
| import app.gamenative.utils.ManifestContentTypes | ||
|
|
@@ -143,6 +145,7 @@ fun ContainerConfigDialog( | |
| default: Boolean = false, | ||
| title: String, | ||
| initialConfig: ContainerData = ContainerData(), | ||
| steamAppId: Int? = null, | ||
| onDismissRequest: () -> Unit, | ||
| onSave: (ContainerData) -> Unit, | ||
| ) { | ||
|
|
@@ -941,6 +944,7 @@ fun ContainerConfigDialog( | |
|
|
||
| val state = ContainerConfigState( | ||
| config = configState, | ||
| steamAppId = steamAppId, | ||
| graphicsDrivers = graphicsDriversRef, | ||
| bionicWineEntries = bionicWineEntriesRef, | ||
| glibcWineEntries = glibcWineEntriesRef, | ||
|
|
@@ -1230,39 +1234,56 @@ private fun Preview_ContainerConfigDialog() { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Editable dropdown for selecting executable paths from the container's A: drive | ||
| */ | ||
| @OptIn(ExperimentalMaterial3Api::class) | ||
| @Composable | ||
| internal fun ExecutablePathDropdown( | ||
| modifier: Modifier = Modifier, | ||
| value: String, | ||
| onValueChange: (String) -> Unit, | ||
| onLaunchOptionSelected: (executablePath: String, execArgs: String?) -> Unit, | ||
| containerData: ContainerData, | ||
| steamAppId: Int? = null, | ||
| ) { | ||
|
playday3008 marked this conversation as resolved.
|
||
| var expanded by remember { mutableStateOf(false) } | ||
| var executables by remember { mutableStateOf<List<String>>(emptyList()) } | ||
| var picsEntries by remember { mutableStateOf<List<LaunchInfo>>(emptyList()) } | ||
| var isLoading by remember { mutableStateOf(true) } | ||
| val context = LocalContext.current | ||
|
|
||
| // Load executables from A: drive when component is first created | ||
| LaunchedEffect(containerData.drives) { | ||
| LaunchedEffect(containerData.drives, steamAppId) { | ||
| isLoading = true | ||
| executables = withContext(Dispatchers.IO) { | ||
| ContainerUtils.scanExecutablesInADrive(containerData.drives) | ||
| val (pics, scanned) = withContext(Dispatchers.IO) { | ||
| val pics = if (steamAppId != null) { | ||
| SteamService.getWindowsLaunchInfos(steamAppId) | ||
| } else { | ||
| emptyList() | ||
| } | ||
| val scanned = ContainerUtils.scanExecutablesInADrive(containerData.drives) | ||
| pics to scanned | ||
| } | ||
| picsEntries = pics | ||
| executables = scanned | ||
|
|
||
| if (pics.isNotEmpty()) { | ||
| val alreadyMatchesPics = pics.any { | ||
| it.executable == value && it.arguments == containerData.execArgs | ||
| } | ||
| val alreadyMatchesScanned = scanned.any { it == value } | ||
| if (!alreadyMatchesPics && !alreadyMatchesScanned) { | ||
| val defaultEntry = pics.firstOrNull { it.type == "default" } ?: pics.first() | ||
| onLaunchOptionSelected(defaultEntry.executable, defaultEntry.arguments) | ||
| } | ||
|
Comment on lines
+1265
to
+1273
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be undone - we should not change the default. We used to use the default exe from steam and games don't always work, since we're using goldberg to launch and not steam.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which games are you seeing break with the PICS default? The auto-default only kicks in when the current exe doesn't match any PICS entry or any scanned file on disk — basically the "nothing selected yet" state. If the user or If there are specific cases where Goldberg needs a different exe than what Steam's PICS lists as |
||
| } | ||
|
|
||
| isLoading = false | ||
| } | ||
|
|
||
| ExposedDropdownMenuBox( | ||
| expanded = expanded, | ||
| onExpandedChange = { expanded = it }, | ||
| modifier = modifier | ||
| modifier = modifier, | ||
| ) { | ||
| NoExtractOutlinedTextField( | ||
| value = value, | ||
| onValueChange = onValueChange, | ||
| onValueChange = { }, | ||
| readOnly = true, | ||
| label = { Text(stringResource(R.string.container_config_executable_path)) }, | ||
| placeholder = { Text(stringResource(R.string.container_config_executable_path_placeholder)) }, | ||
|
|
@@ -1276,35 +1297,70 @@ internal fun ExecutablePathDropdown( | |
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .menuAnchor(), | ||
| singleLine = true | ||
| singleLine = true, | ||
| ) | ||
|
|
||
| if (!isLoading && executables.isNotEmpty()) { | ||
| val hasContent = !isLoading && (picsEntries.isNotEmpty() || executables.isNotEmpty()) | ||
| if (hasContent) { | ||
| ExposedDropdownMenu( | ||
| expanded = expanded, | ||
| onDismissRequest = { expanded = false } | ||
| onDismissRequest = { expanded = false }, | ||
| ) { | ||
| picsEntries.forEach { entry -> | ||
| DropdownMenuItem( | ||
| text = { | ||
| Column { | ||
| Text( | ||
| text = entry.description.ifEmpty { "Default" }, | ||
| style = MaterialTheme.typography.bodyMedium, | ||
| ) | ||
| Text( | ||
| text = entry.executable, | ||
| style = MaterialTheme.typography.bodySmall, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant, | ||
| ) | ||
| if (entry.arguments.isNotEmpty()) { | ||
| Text( | ||
| text = entry.arguments, | ||
| style = MaterialTheme.typography.labelSmall, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant, | ||
| ) | ||
| } | ||
| } | ||
| }, | ||
| onClick = { | ||
| onLaunchOptionSelected(entry.executable, entry.arguments) | ||
| expanded = false | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| if (picsEntries.isNotEmpty() && executables.isNotEmpty()) { | ||
| HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) | ||
| } | ||
|
|
||
| executables.forEach { executable -> | ||
| DropdownMenuItem( | ||
| text = { | ||
| Column { | ||
| Text( | ||
| text = executable.substringAfterLast('\\'), | ||
| style = MaterialTheme.typography.bodyMedium | ||
| text = executable.substringAfterLast('/').substringAfterLast('\\'), | ||
| style = MaterialTheme.typography.bodyMedium, | ||
| ) | ||
| if (executable.contains('\\')) { | ||
| val parent = executable.substringBeforeLast('/').substringBeforeLast('\\') | ||
| if (parent != executable) { | ||
| Text( | ||
| text = executable.substringBeforeLast('\\'), | ||
| text = parent, | ||
| style = MaterialTheme.typography.bodySmall, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant, | ||
| ) | ||
| } | ||
| } | ||
| }, | ||
| onClick = { | ||
| onValueChange(executable) | ||
| onLaunchOptionSelected(executable, null) | ||
| expanded = false | ||
| } | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -265,6 +265,7 @@ private fun normalizeProcessName(name: String): String { | |
|
|
||
| private fun extractExecutableBasename(path: String): String { | ||
| if (path.isBlank()) return "" | ||
| if (ContainerUtils.isUriScheme(path)) return "" | ||
| return normalizeProcessName(path) | ||
| } | ||
|
|
||
|
|
@@ -1689,7 +1690,7 @@ fun XServerScreen( | |
| if (!bootToContainer) { | ||
| renderer.setUnviewableWMClasses("explorer.exe") | ||
| // TODO: make 'force fullscreen' be an option of the app being launched | ||
| if (container.executablePath.isNotBlank()) { | ||
| if (container.executablePath.isNotBlank() && !ContainerUtils.isUriScheme(container.executablePath)) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is it useful to launch a uriScheme? If this doesn't have any practical value, please remove it. Seems pointless, since uri schemes are not going to do anything within a wine container.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some Steam games use URI schemes as their actual launch entry in PICS — EA titles use Right now nothing auto-selects a URI — they just show up in the dropdown for users who have the setup for it. The XServerScreen handling ( |
||
| renderer.forceFullscreenWMClass = Paths.get(container.executablePath).name | ||
| } | ||
| } | ||
|
|
@@ -3716,14 +3717,22 @@ private fun getWineStartCommand( | |
| return "winhandler.exe \"wfm.exe\"" | ||
| } | ||
|
|
||
| // Set working directory to the game folder | ||
| val executableDir = gameFolderPath + "/" + executablePath.substringBeforeLast("/", "") | ||
| // Set working directory to the game folder (URIs have no meaningful subdirectory) | ||
| val executableDir = if (ContainerUtils.isUriScheme(executablePath)) { | ||
| gameFolderPath!! | ||
| } else { | ||
| gameFolderPath + "/" + executablePath.substringBeforeLast("/", "") | ||
| } | ||
| guestProgramLauncherComponent.workingDir = File(executableDir) | ||
|
|
||
| // Normalize path separators (ensure Windows-style backslashes) | ||
| val normalizedPath = executablePath.replace('/', '\\') | ||
| envVars.put("WINEPATH", "A:\\") | ||
| "\"A:\\${normalizedPath}\"" | ||
| when { | ||
| ContainerUtils.isUriScheme(executablePath) -> "cmd /c start /WAIT \"\" \"$executablePath\"" | ||
| ContainerUtils.isBatchScript(executablePath) -> "cmd /c call \"A:\\${normalizedPath}\"" | ||
| else -> "\"A:\\${normalizedPath}\"" | ||
|
playday3008 marked this conversation as resolved.
|
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } else if (container.executablePath.isEmpty()) { | ||
| // For Steam games, we need appLaunchInfo | ||
| Timber.tag("XServerScreen").w("appLaunchInfo is null for Steam game: $appId") | ||
|
|
@@ -3756,11 +3765,15 @@ private fun getWineStartCommand( | |
| } | ||
| if (container.isUseLegacyDRM) { | ||
| val appDirPath = SteamService.getAppDirPath(gameId) | ||
| val executableDir = appDirPath + "/" + executablePath.substringBeforeLast("/", "") | ||
| guestProgramLauncherComponent.workingDir = File(executableDir); | ||
| Timber.i("Working directory is ${executableDir}") | ||
| val executableDir = if (ContainerUtils.isUriScheme(executablePath)) { | ||
| appDirPath | ||
| } else { | ||
| appDirPath + "/" + executablePath.substringBeforeLast("/", "") | ||
| } | ||
| guestProgramLauncherComponent.workingDir = File(executableDir) | ||
| Timber.i("Working directory is $executableDir") | ||
|
|
||
| Timber.i("Final exe path is " + executablePath) | ||
| Timber.i("Final exe path is $executablePath") | ||
| val drives = container.drives | ||
| val driveIndex = drives.indexOf(appDirPath) | ||
| // greater than 1 since there is the drive character and the colon before the app dir path | ||
|
|
@@ -3773,7 +3786,11 @@ private fun getWineStartCommand( | |
| if (appLaunchInfo != null){ | ||
| envVars.put("WINEPATH", "$drive:/${appLaunchInfo.workingDir}") | ||
| } | ||
| "\"$drive:/${executablePath}\"" | ||
| when { | ||
| ContainerUtils.isUriScheme(executablePath) -> "cmd /c start /WAIT \"\" \"$executablePath\"" | ||
| ContainerUtils.isBatchScript(executablePath) -> "cmd /c call \"$drive:/${executablePath}\"" | ||
| else -> "\"$drive:/${executablePath}\"" | ||
| } | ||
| } else { | ||
| "\"C:\\\\Program Files (x86)\\\\Steam\\\\steamclient_loader_x64.exe\"" | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.