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/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 @@ -105,7 +105,7 @@ import app.gamenative.utils.CustomGameScanner
import app.gamenative.utils.ExecutableSelectionUtils
import app.gamenative.utils.LsfgQuickMenuHelper
import app.gamenative.utils.ManifestComponentHelper
import app.gamenative.utils.PreInstallSteps
import app.gamenative.utils.PreLaunchSetup
import app.gamenative.utils.SteamTokenLogin
import app.gamenative.utils.SteamUtils
import app.gamenative.utils.WineProcessSnapshotHelper
Expand Down Expand Up @@ -3050,15 +3050,16 @@ private fun setupXEnvironment(
)
}

var preInstallCommands: List<PreInstallSteps.PreInstallCommand> = emptyList()
var gameExecutable = ""
var allChainedCommands: List<PreLaunchSetup.ChainedCommand> = emptyList()

if (container != null) {
try {
GameFixesRegistry.applyFor(context, appId, container)
} catch (e: Exception) {
Timber.tag("GameFixes").w(e, "Game fixes failed before launch")
}

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 All @@ -3083,15 +3084,14 @@ private fun setupXEnvironment(
gameExecutable = "wine explorer /desktop=shell," + xServer.screenInfo + " " +
getWineStartCommand(context, appId, container, bootToContainer, testGraphics, appLaunchInfo, envVars, guestProgramLauncherComponent, gameSource) +
(if (container.execArgs.isNotEmpty()) " " + container.execArgs else "")
preInstallCommands = PreInstallSteps.getPreInstallCommands(
container,
appId,
gameSource,
xServer.screenInfo.toString(),
containerVariantChanged,
allChainedCommands = PreLaunchSetup.buildChain(
container = container,
appId = appId,
gameSource = gameSource,
screenInfo = xServer.screenInfo.toString(),
containerVariantChanged = containerVariantChanged,
)
guestProgramLauncherComponent.guestExecutable =
preInstallCommands.firstOrNull()?.executable ?: gameExecutable
guestProgramLauncherComponent.guestExecutable = allChainedCommands.firstOrNull()?.executable ?: gameExecutable
guestProgramLauncherComponent.isWoW64Mode = wow64Mode
// Set steam type for selecting appropriate box64rc
guestProgramLauncherComponent.setSteamType(container.getSteamType())
Expand Down Expand Up @@ -3132,7 +3132,7 @@ private fun setupXEnvironment(
containerVariantChanged = containerVariantChanged,
onError = onGameLaunchError
)
if (preInstallCommands.isNotEmpty()) {
if (allChainedCommands.isNotEmpty()) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
Expand Down Expand Up @@ -3211,35 +3211,34 @@ private fun setupXEnvironment(
PluviaApp.events.emit(AndroidEvent.GuestProgramTerminated)
}

fun chainPreInstallSteps(remaining: List<PreInstallSteps.PreInstallCommand>) {
fun chainCommands(remaining: List<PreLaunchSetup.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
97 changes: 97 additions & 0 deletions app/src/main/java/app/gamenative/utils/PreLaunchSetup.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package app.gamenative.utils

import app.gamenative.data.GameSource
import app.gamenative.service.SteamService
import app.gamenative.utils.installscript.InstallScriptExecutor
import com.winlator.container.Container
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import timber.log.Timber

object PreLaunchSetup {

data class ChainedCommand(
val executable: String,
val onComplete: () -> Unit,
)

fun buildChain(
container: Container,
appId: String,
gameSource: GameSource,
screenInfo: String,
containerVariantChanged: Boolean,
): List<ChainedCommand> {
val chain = mutableListOf<ChainedCommand>()

val preInstallCmds = PreInstallSteps.getPreInstallCommands(
container = container,
appId = appId,
gameSource = gameSource,
screenInfo = screenInfo,
containerVariantChanged = containerVariantChanged,
)
chain += preInstallCmds.map { cmd ->
ChainedCommand(cmd.executable) {
PreInstallSteps.markStepDone(container, cmd.marker)
}
}

if (gameSource == GameSource.STEAM) {
chain += collectInstallScriptCommands(container, appId, screenInfo)
}

return chain
}

private fun collectInstallScriptCommands(
container: Container,
appId: String,
screenInfo: String,
): List<ChainedCommand> {
return try {
val numericGameId = ContainerUtils.extractGameIdFromContainerId(appId)
val steamApp = SteamService.getAppInfoOf(numericGameId) ?: return emptyList()
val appInfo = runBlocking(Dispatchers.IO) {
SteamService.instance?.appInfoDao?.get(numericGameId)
} ?: return emptyList()
val gameDir = PreInstallSteps.getGameDir(container) ?: return emptyList()

val scripts = InstallScriptExecutor.collectScripts(
steamApp = steamApp,
appInfo = appInfo,
gameDir = gameDir,
installDir = "A:",
language = container.language,
appId = numericGameId,
)
if (scripts.isEmpty()) return emptyList()

Timber.tag("InstallScript").i("Applying registry keys from ${scripts.size} install script(s)")
InstallScriptExecutor.applyRegistryKeys(container, scripts, container.language)

InstallScriptExecutor.getRunProcessCommands(
container = container,
scripts = scripts,
screenInfo = screenInfo,
is64Bit = container.isWoW64Mode,
).map { cmd ->
ChainedCommand(cmd.executable) {
if (cmd.hasRunKey != null) {
val exitCode = InstallScriptExecutor.readExitCode(container)
if (exitCode == 0) {
InstallScriptExecutor.markRunProcessComplete(container, cmd.hasRunKey)
} else {
Timber.tag("InstallScript").w(
"Run process exited with code $exitCode, will retry next launch",
)
}
}
}
}
} catch (e: Exception) {
Timber.tag("InstallScript").w(e, "InstallScript execution failed")
emptyList()
}
}
}
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