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
1 change: 1 addition & 0 deletions app/src/main/java/app/gamenative/data/LaunchInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class LaunchInfo(
val executable: String,
val arguments: String = "",
val workingDir: String,
val description: String,
val type: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ object GameFixesRegistry {
GOG_Fix_1787707874,
GOG_Fix_1808582759,
GOG_Fix_2147483047,
STEAM_Fix_400,
STEAM_Fix_22300,
STEAM_Fix_22330,
STEAM_Fix_22370,
Expand Down
13 changes: 0 additions & 13 deletions app/src/main/java/app/gamenative/gamefixes/STEAM_400.kt

This file was deleted.

7 changes: 5 additions & 2 deletions app/src/main/java/app/gamenative/service/SteamService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2269,8 +2269,11 @@ class SteamService : Service(), IChallengeUrlChanged {
fun getWindowsLaunchInfos(appId: Int): List<LaunchInfo> {
return getAppInfoOf(appId)?.let { appInfo ->
appInfo.config.launch.filter { launchInfo ->
// since configOS was unreliable and configArch was even more unreliable
launchInfo.executable.endsWith(".exe", ignoreCase = true)
val exe = launchInfo.executable
exe.endsWith(".exe", ignoreCase = true) ||
exe.endsWith(".bat", ignoreCase = true) ||
exe.endsWith(".cmd", ignoreCase = true) ||
exe.contains("://")
Comment thread
playday3008 marked this conversation as resolved.
}
}.orEmpty()
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/app/gamenative/ui/PluviaMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,11 @@ fun PluviaMain(
visible = true,
title = context.getString(R.string.container_config_title),
initialConfig = config,
steamAppId = if (ContainerUtils.extractGameSourceFromContainerId(appId) == GameSource.STEAM) {
Comment thread
playday3008 marked this conversation as resolved.
runCatching { ContainerUtils.extractGameIdFromContainerId(appId) }.getOrNull()
} else {
null
},
onDismissRequest = { openContainerConfigForAppId = null },
onSave = { newConfig ->
scope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -143,6 +145,7 @@ fun ContainerConfigDialog(
default: Boolean = false,
title: String,
initialConfig: ContainerData = ContainerData(),
steamAppId: Int? = null,
onDismissRequest: () -> Unit,
onSave: (ContainerData) -> Unit,
) {
Expand Down Expand Up @@ -941,6 +944,7 @@ fun ContainerConfigDialog(

val state = ContainerConfigState(
config = configState,
steamAppId = steamAppId,
graphicsDrivers = graphicsDriversRef,
bionicWineEntries = bionicWineEntriesRef,
glibcWineEntries = glibcWineEntriesRef,
Expand Down Expand Up @@ -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,
) {
Comment thread
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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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 getInstalledExe() already set something valid, this block is skipped entirely.

If there are specific cases where Goldberg needs a different exe than what Steam's PICS lists as type=default, knowing which games would help — we could maybe filter by launch type or skip the default when Goldberg is active.

}

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)) },
Expand All @@ -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
}
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.winlator.fexcore.FEXCorePreset
*/
class ContainerConfigState(
val config: MutableState<ContainerData>,
val steamAppId: Int? = null,
val graphicsDrivers: MutableState<MutableList<String>>,
val bionicWineEntries: MutableState<List<String>>,
val glibcWineEntries: MutableState<List<String>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,14 @@ fun GeneralTabContent(
ExecutablePathDropdown(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
value = config.executablePath,
onValueChange = { state.config.value = config.copy(executablePath = it) },
onLaunchOptionSelected = { path, args ->
state.config.value = config.copy(
executablePath = path,
execArgs = args ?: config.execArgs,
Comment thread
playday3008 marked this conversation as resolved.
)
Comment thread
playday3008 marked this conversation as resolved.
},
containerData = config,
steamAppId = state.steamAppId,
)
NoExtractOutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,7 @@ abstract class BaseAppScreen {
ContainerConfigDialog(
title = "${displayInfo.name} Config",
initialConfig = containerData,
steamAppId = if (libraryItem.gameSource == GameSource.STEAM) libraryItem.gameId else null,
onDismissRequest = { showConfigDialog = false },
onSave = {
saveContainerConfig(context, libraryItem, it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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 link2ea:// (FIFA, Battlefield, NFS, etc.), Ubisoft games use uplay:// launch URIs. If the user has EA App or Ubisoft Connect installed inside the Wine container, these URIs would invoke the launcher the same way they do on Windows.

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 (cmd /c start /WAIT) is there so it doesn't break if someone does pick one.

renderer.forceFullscreenWMClass = Paths.get(container.executablePath).name
}
}
Expand Down Expand Up @@ -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}\""
Comment thread
playday3008 marked this conversation as resolved.
}
Comment thread
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")
Expand Down Expand Up @@ -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
Expand All @@ -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\""
}
Expand Down
Loading