Skip to content
Draft
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
525 changes: 525 additions & 0 deletions app/src/main/java/app/gamenative/gamefixes/EaAppGameFix.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,15 @@ object GameFixesRegistry {
else -> gameId
}
Timber.i("GameFixesRegistry: Applying fixes for game: $source $catalogId if available")
val fix = fixesProvider()[source to catalogId] ?: return
val keyedFix = fixesProvider()[source to catalogId]
// Master behavior for everything without a fix: return before touching
// path resolution. EA App titles (Steam only) are detected by their
// installer bundle and get the family fix without registration.
if (keyedFix == null && source != GameSource.STEAM) return
val (installPath, installPathWindows) = resolvePaths(context, source, gameId) ?: return
val fix = keyedFix
?: EaAppGameFix.takeIf { isEaAppGame(installPath) }
?: return
fix.apply(context, catalogId, installPath, installPathWindows, container)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import app.gamenative.PrefManager
import app.gamenative.SteamBootstrap
import app.gamenative.data.GameSource
import app.gamenative.gamefixes.GameFixesRegistry
import app.gamenative.gamefixes.isEaAppGame
import app.gamenative.gamefixes.GameInputCompatibility
import app.gamenative.data.LaunchInfo
import app.gamenative.data.LibraryItem
Expand Down Expand Up @@ -3783,6 +3784,22 @@ private fun setupXEnvironment(
var preInstallCommands: List<PreInstallSteps.PreInstallCommand> = emptyList()
var gameExecutable = ""

// EA App prefixes need the gamefix re-applied right before the game
// session: installer sessions overwrite its registry work on their
// wineserver flush, and on a cold install the EA binaries it targets only
// exist after the installer has run.
val reapplyEaGameFixes = {
val gameDir = ContainerUtils.extractGameIdFromContainerId(appId)
?.let { SteamService.getAppDirPath(it) }.orEmpty()
if (gameDir.isNotEmpty() && isEaAppGame(gameDir)) {
try {
GameFixesRegistry.applyFor(context, appId, container)
} catch (e: Exception) {
Timber.tag("GameFixes").w(e, "EA gamefix reapply failed")
}
}
}

if (container != null) {
try {
GameFixesRegistry.applyFor(context, appId, container)
Expand Down Expand Up @@ -3879,6 +3896,7 @@ private fun setupXEnvironment(
if (preInstallCommands.isNotEmpty()) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
} else {
reapplyEaGameFixes()
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
}
}
Expand Down Expand Up @@ -3967,7 +3985,7 @@ private fun setupXEnvironment(
guestProgramLauncherComponent.setGuestExecutable(remaining.first().executable)
guestProgramLauncherComponent.setTerminationCallback { _ ->
val current = remaining.first()
PreInstallSteps.markStepDone(container, current.marker)
current.marker?.let { PreInstallSteps.markStepDone(container, it) }
guestProgramLauncherComponent.setPreUnpack(null)
try {
guestProgramLauncherComponent.execShellCommand("wineserver -k")
Expand All @@ -3976,6 +3994,7 @@ private fun setupXEnvironment(
}
val nextRemaining = remaining.drop(1)
if (nextRemaining.isEmpty()) {
reapplyEaGameFixes()
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
Expand Down Expand Up @@ -4503,12 +4522,11 @@ private fun getWineStartCommand(
// and will monitor the game via nativeWaitAppExit.
val appDirPath = SteamService.getAppDirPath(gameId)
val exePath = container.executablePath.ifEmpty { SteamService.getInstalledExe(gameId) }
val normalizedExe = exePath.replace('/', '\\').trimStart('\\')
val executableDir = appDirPath + "/" + exePath.substringBeforeLast("/", "")
guestProgramLauncherComponent.workingDir = File(executableDir)
Timber.i("Bionic-Steam working directory is $executableDir")
val gameFolderName = appDirPath.substringAfterLast('/').ifEmpty { gameId.toString() }
"\"C:\\\\Program Files (x86)\\\\Steam\\\\steamapps\\\\common\\\\$gameFolderName\\\\$normalizedExe\""
SteamUtils.buildBionicSteamLaunchCommand(gameFolderName, exePath, appLaunchInfo, gameId, appDirPath)
} else if (container.isLaunchRealSteam) {
// Launch Steam with the applaunch parameter to start the game
"\"C:\\\\Program Files (x86)\\\\Steam\\\\steam.exe\" -silent -vgui -tcp " +
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/app/gamenative/utils/PreInstallSteps.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.io.File
*/
object PreInstallSteps {
data class PreInstallCommand(
val marker: Marker,
val marker: Marker?,
val executable: String,
)

Expand All @@ -30,11 +30,12 @@ object PreInstallSteps {
XnaFrameworkStep,
GogScriptInterpreterStep,
UbisoftConnectStep,
SteamInstallScriptStep,
)

private var stepsProvider: () -> List<PreInstallStep> = { steps }
private fun currentSteps(): List<PreInstallStep> = stepsProvider()
private fun allMarkers(): List<Marker> = currentSteps().map { it.marker }.distinct()
private fun allMarkers(): List<Marker> = currentSteps().mapNotNull { it.marker }.distinct()

/**
* Returns a list of pre-install commands (marker + guest executable). Each entry is a
Expand Down
52 changes: 51 additions & 1 deletion app/src/main/java/app/gamenative/utils/SteamUtils.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package app.gamenative.utils

import app.gamenative.gamefixes.EA_LINK2EA_LAUNCH_SCRIPT_WINDOWS_PATH
import app.gamenative.gamefixes.isEaAppGame
import android.annotation.SuppressLint
import android.content.Context
import android.provider.Settings
Expand Down Expand Up @@ -56,6 +58,49 @@ object SteamUtils {
val exeRunDirOverride: String? = null,
)

internal fun buildBionicSteamLaunchCommand(
gameFolderName: String,
executablePath: String,
appLaunchInfo: LaunchInfo?,
gameId: Int,
appDirPath: String,
): String {
// EA App titles must launch through EA: Steam hands the game to EA via
// its link2ea:// protocol, and EA authenticates the session with an
// exchange token minted from the Steam login.
// Only EA App titles take the protocol path; every other game launches
// exactly as before. Steam's launch config carries link2ea:// for most
// EA titles; fall back to building the URL from the appid.
val isEaApp = isEaAppGame(appDirPath)
val protocolTarget = if (!isEaApp) {
null
} else {
(appLaunchInfo?.executable ?: "link2ea://launchgame/$gameId?platform=steam")
.trim()
.takeIf { target ->
target.matches(Regex("^[A-Za-z][A-Za-z0-9+.-]*://[^\\s\\\"]+$"))
}
?: "link2ea://launchgame/$gameId?platform=steam"
}
if (protocolTarget != null) {
// A gamefix-owned launch script can gate the protocol handoff (for
// example waiting for EABackgroundService before Link2EA fires).
// The registry protocol handler is not a reliable place for that
// gate: EA rewrites its own protocol keys on every service start.
// The script path must not contain spaces: the command goes through
// winhandler's argument parsing, which breaks on cmd's nested-quote
// form, so the script is passed as a bare token.
if (isEaApp) {
return "\"C:\\\\windows\\\\system32\\\\cmd.exe\" /d /c " +
"$EA_LINK2EA_LAUNCH_SCRIPT_WINDOWS_PATH \"$protocolTarget\""
}
return "\"C:\\\\windows\\\\system32\\\\start.exe\" \"$protocolTarget\""
}

val normalizedExe = executablePath.replace('/', '\\').trimStart('\\')
return "\"C:\\\\Program Files (x86)\\\\Steam\\\\steamapps\\\\common\\\\$gameFolderName\\\\$normalizedExe\""
}

/**
* True when a stored Steam session exists (offline-launch gate).
* Matches GOG/Epic/Amazon AuthManager.hasStoredCredentials convention.
Expand Down Expand Up @@ -885,6 +930,12 @@ object SteamUtils {
cfgFile.writeText("BootStrapperInhibitAll=Enable\nBootStrapperForceSelfUpdate=False")
}

val steamAccountId = getSteam3AccountId()?.toString()
if (steamAccountId == null) {
Timber.w("Deferring restoreSteamApi for appId $appId: Steam user is not available")
return
}

// Update or modify localconfig.vdf
val steam3AccountId = getSteam3AccountId()?.toString().orEmpty()
updateOrModifyLocalConfig(imageFs, container, steamAppId.toString(), steam3AccountId)
Expand Down Expand Up @@ -1669,4 +1720,3 @@ object SteamUtils {
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.winlator.container.Container
import java.io.File

interface PreInstallStep {
val marker: Marker
val marker: Marker?

fun appliesTo(
container: Container,
Expand Down
Loading
Loading