Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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/DepotInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ data class DepotInfo(
val realm: SteamRealm = SteamRealm.Unknown,
val systemDefined: Boolean = false,
val steamDeck: Boolean = false,
val installScript: String = "",
) {
/** Windows or OS-untagged (neither Linux nor macOS) */
val isWindowsCompatible: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import app.gamenative.utils.ExecutableSelectionUtils
import app.gamenative.utils.LsfgQuickMenuHelper
import app.gamenative.utils.ManifestComponentHelper
import app.gamenative.utils.PreInstallSteps
import app.gamenative.utils.installscript.InstallScriptExecutor
import app.gamenative.utils.SteamTokenLogin
import app.gamenative.utils.SteamUtils
import app.gamenative.utils.WineProcessSnapshotHelper
Expand Down Expand Up @@ -3051,6 +3052,7 @@ private fun setupXEnvironment(
}

var preInstallCommands: List<PreInstallSteps.PreInstallCommand> = emptyList()
var installScriptRunProcessCommands = emptyList<InstallScriptExecutor.RunProcessCommand>()
var gameExecutable = ""

if (container != null) {
Expand All @@ -3059,6 +3061,40 @@ private fun setupXEnvironment(
} catch (e: Exception) {
Timber.tag("GameFixes").w(e, "Game fixes failed before launch")
}
if (gameSource == GameSource.STEAM) {
try {
val numericGameId = ContainerUtils.extractGameIdFromContainerId(appId)
val steamApp = SteamService.getAppInfoOf(numericGameId)
val appInfoObj = runBlocking(Dispatchers.IO) {
SteamService.instance?.appInfoDao?.get(numericGameId)
}
val gameDir = PreInstallSteps.getGameDir(container)
if (steamApp != null && appInfoObj != null && gameDir != null) {
val installDir = "A:"
val scripts = InstallScriptExecutor.collectScripts(
steamApp = steamApp,
appInfo = appInfoObj,
gameDir = gameDir,
installDir = installDir,
language = container.language,
appId = numericGameId,
)
if (scripts.isNotEmpty()) {
Timber.tag("InstallScript").i("Applying registry keys from ${scripts.size} install script(s)")
InstallScriptExecutor.applyRegistryKeys(container, scripts, container.language)

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.

It's valuable to add the registry keys, but the rest I'm not so sure about. We intentionally don't install some things (like xaudio on proton 9, directx on proton < 10). This should align with our preinstallsteps better. What kind of scripts have you seen here? And what errors get fixed?

@playday3008 playday3008 Jun 5, 2026

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.

From the testing with NFS Rivals, the InstallScript Run Process installs EA App (EAAppInstaller.exe). Ubisoft titles similarly declare Ubisoft Connect installer. These are launcher/client prerequisites, not redists like VCRedist/DirectX/PhysX (those are already handled by PreInstallSteps).

Concern about xaudio on proton 9 / directx on proton <10 is valid but may not apply here — InstallScript Run Processes are typically launcher installers, not the same redists that PreInstallSteps handles. That said, some games could declare DirectX/VCRedist in their InstallScript too, which would overlap.

installScriptRunProcessCommands = InstallScriptExecutor.getRunProcessCommands(
container = container,
scripts = scripts,
screenInfo = xServer.screenInfo.toString(),
is64Bit = container.isWoW64Mode,
)
}
}
} catch (e: Exception) {
Timber.tag("InstallScript").w(e, "InstallScript execution failed")
}
}

if (container.startupSelection == Container.STARTUP_SELECTION_AGGRESSIVE) {
if (container.containerVariant.equals(Container.BIONIC)){
Timber.d("Incorrect startup selection detected. Reverting to essential startup selection")
Expand Down Expand Up @@ -3090,8 +3126,9 @@ private fun setupXEnvironment(
xServer.screenInfo.toString(),
containerVariantChanged,
)
guestProgramLauncherComponent.guestExecutable =
preInstallCommands.firstOrNull()?.executable ?: gameExecutable
val firstChainedExecutable = preInstallCommands.firstOrNull()?.executable
?: installScriptRunProcessCommands.firstOrNull()?.executable
guestProgramLauncherComponent.guestExecutable = firstChainedExecutable ?: gameExecutable
guestProgramLauncherComponent.isWoW64Mode = wow64Mode
// Set steam type for selecting appropriate box64rc
guestProgramLauncherComponent.setSteamType(container.getSteamType())
Expand Down Expand Up @@ -3132,7 +3169,7 @@ private fun setupXEnvironment(
containerVariantChanged = containerVariantChanged,
onError = onGameLaunchError
)
if (preInstallCommands.isNotEmpty()) {
if (preInstallCommands.isNotEmpty() || installScriptRunProcessCommands.isNotEmpty()) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
Expand Down Expand Up @@ -3211,35 +3248,51 @@ private fun setupXEnvironment(
PluviaApp.events.emit(AndroidEvent.GuestProgramTerminated)
}

fun chainPreInstallSteps(remaining: List<PreInstallSteps.PreInstallCommand>) {
data class ChainedCommand(
val executable: String,
val onComplete: () -> Unit,
)

val chainedPreInstall = preInstallCommands.map { cmd ->
ChainedCommand(cmd.executable) { PreInstallSteps.markStepDone(container, cmd.marker) }
}
val chainedInstallScript = installScriptRunProcessCommands.map { cmd ->
ChainedCommand(cmd.executable) {
if (cmd.hasRunKey != null) {
InstallScriptExecutor.markRunProcessComplete(container, cmd.hasRunKey)
}
}
}
val allChainedCommands = chainedPreInstall + chainedInstallScript
Comment thread
playday3008 marked this conversation as resolved.
Outdated

fun chainCommands(remaining: List<ChainedCommand>) {
if (remaining.isEmpty()) {
guestProgramLauncherComponent.setGuestExecutable(gameExecutable)
guestProgramLauncherComponent.setTerminationCallback(gameTerminationCallback)
return
}
guestProgramLauncherComponent.setGuestExecutable(remaining.first().executable)
guestProgramLauncherComponent.setTerminationCallback { _ ->
val current = remaining.first()
PreInstallSteps.markStepDone(container, current.marker)
remaining.first().onComplete()
guestProgramLauncherComponent.setPreUnpack(null)
try {
guestProgramLauncherComponent.execShellCommand("wineserver -k")
} catch (e: Exception) {
Timber.w(e, "wineserver -k between pre-install steps (non-fatal)")
Timber.w(e, "wineserver -k between chained commands (non-fatal)")
}
val nextRemaining = remaining.drop(1)
if (nextRemaining.isEmpty()) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
}
chainPreInstallSteps(nextRemaining)
chainCommands(nextRemaining)
guestProgramLauncherComponent.start()
}
}

if (preInstallCommands.isNotEmpty()) {
chainPreInstallSteps(preInstallCommands)
if (allChainedCommands.isNotEmpty()) {
chainCommands(allChainedCommands)
} else {
guestProgramLauncherComponent.setTerminationCallback(gameTerminationCallback)
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/app/gamenative/utils/KeyValueUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ fun KeyValue.generateSteamApp(): SteamApp {
systemDefined = currentDepot["systemdefined"].asBoolean(),
optionalDlcId = currentDepot["config"]["optionaldlc"].asInteger(INVALID_APP_ID),
steamDeck = currentDepot["config"]["steamdeck"].asBoolean(false),
installScript = currentDepot["installscript"].value.orEmpty(),
)
},
branches = this["depots"]["branches"].children.associate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ object PreInstallSteps {
return "wine explorer /desktop=shell,$screenInfo $wrapped"
}

private fun getGameDir(container: Container): File? {
internal fun getGameDir(container: Container): File? {
for (drive in Container.drivesIterator(container.drives)) {
if (drive[0].equals("A", ignoreCase = true)) return File(drive[1])
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package app.gamenative.utils.installscript

data class RegistryValues(
val strings: Map<String, String> = emptyMap(),
val dwords: Map<String, Long> = emptyMap(),
)

data class RegistryAction(
val keyPath: String,
val values: RegistryValues,
val languageOverrides: Map<String, RegistryValues> = emptyMap(),
)

data class OSRequirement(
val is64BitWindows: Boolean? = null,
val osType: String? = null,
)

data class RunProcessAction(
val name: String,
val process: String,
val command: String = "",
val hasRunKey: String? = null,
val noCleanUp: Boolean = false,
val minimumHasRunValue: Int = 0,
val requirementOS: OSRequirement? = null,
val asCurrentUser: Boolean = false,
)

data class InstallScript(
val sourcePath: String,
val registryActions: List<RegistryAction> = emptyList(),
val runProcessActions: List<RunProcessAction> = emptyList(),
)
Loading