Skip to content

Commit 5874237

Browse files
committed
fix: address fourth round of PR review findings
PluviaMain SDK-cloud-bridge dialog - relaunch() now takes a suppressPrompt flag; only set skipBridgePrompt=true when persistence actually succeeded. Previous behavior unconditionally suppressed the prompt for the next launch even when container.saveData() threw and the subdir wasn't actually saved — bridge ended up "off but prompt hidden" instead of either fully on or normal. - Tightened the prompt-fire gate to also skip when preferredSave != None or ignorePendingOperations is true. Those flags come from conflict- resolution dialogs; firing the bridge prompt during a conflict re-entry would drop the user's chosen save / dismiss-pending choice via the relaunch lambda. SteamUtils.writeSharedAppManifest - Added LastOwner to the early-return reuse criteria. Previously, depots/buildId/scripts could all match while the existing acf was attributed to a different signed-in account; reusing it would leave the manifest pointing at a stale LastOwner and break cloud/ownership checks for the current user. Force-rewrite when the owner doesn't match. New helper parseAcfLastOwner() and acfLastOwnerRegex. SteamUtils.autoLoginUserChanges (loginusers.vdf) - Ensure the Steam config dir exists before writeText(). On a fresh container the Wine prefix skeleton is laid down but Steam's own drive_c/Program Files (x86)/Steam/config/ subdir hasn't been populated yet — without mkdirs() the write throws FileNotFoundException and the auto-login fails silently. ContainerUtils.createNewContainer - Replace the open-coded ContainerData(...) construction in the default branch with getDefaultContainerData().copy(drives = drives, dxwrapper = initialDxWrapper). The manual builder was missing several fields that getDefaultContainerData() carries — localSavesOnly, useSteamInput, sharpnessEffect/Level/Denoise, vibrationMode/Intensity, lsfgEnabled — so new containers inherited Kotlin defaults instead of the user's PrefManager defaults. Inheriting through copy() also means future fields don't need to be added in two places. Findings flagged but not changed: - verifyRestoredState true on no DLLs: the function is meant to detect "DLLs are NOT the pipe-DLL" (i.e., they're the original Valve DLLs or absent). Returning false on absent would force re-extraction of pipe DLLs into game folders that don't ship them, which is wrong for non-Steamworks games. - SDK cloud mirror non-recursive (4th time): by design for Dead Cells flat saves; recursive expansion is scope creep. - matchesGlob unit-test nitpick: skipping; no test infra changes.
1 parent 5bc7a69 commit 5874237

3 files changed

Lines changed: 55 additions & 73 deletions

File tree

app/src/main/java/app/gamenative/ui/PluviaMain.kt

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -792,11 +792,15 @@ fun PluviaMain(
792792
}
793793

794794
DialogType.SDK_CLOUD_BRIDGE_SUGGESTION -> {
795-
val relaunch = {
795+
val relaunch = { suppressPrompt: Boolean ->
796796
preLaunchApp(
797797
context = context,
798798
appId = state.launchedAppId,
799-
skipBridgePrompt = true,
799+
// Only suppress the prompt next time when we actually persisted a subdir
800+
// (or the user picked "Don't ask again"). If persistence failed, leave
801+
// the prompt enabled so it fires again on the next attempt rather than
802+
// silently disabling itself in memory.
803+
skipBridgePrompt = suppressPrompt,
800804
setLoadingDialogVisible = viewModel::setLoadingDialogVisible,
801805
setLoadingProgress = viewModel::setLoadingDialogProgress,
802806
setLoadingMessage = viewModel::setLoadingDialogMessage,
@@ -813,36 +817,37 @@ fun PluviaMain(
813817
val rec = runCatching {
814818
SteamUtils.getRecommendedSdkCloudSaveSubdirAsync(context, gameId)
815819
}.getOrNull()
816-
if (rec != null) {
817-
runCatching {
818-
val container = ContainerUtils.getContainer(context, state.launchedAppId)
819-
container.sdkCloudSaveSubdir = rec.subdir
820-
container.saveData()
821-
}.onFailure { Timber.w(it, "Failed to persist sdkCloudSaveSubdir=${rec.subdir}") }
822-
}
823-
withContext(Dispatchers.Main) { relaunch() }
820+
val persisted = rec != null && runCatching {
821+
val container = ContainerUtils.getContainer(context, state.launchedAppId)
822+
container.sdkCloudSaveSubdir = rec.subdir
823+
container.saveData()
824+
}.onFailure { Timber.w(it, "Failed to persist sdkCloudSaveSubdir=${rec.subdir}") }
825+
.isSuccess
826+
withContext(Dispatchers.Main) { relaunch(persisted) }
824827
}
825828
}
826829
onDismissClick = {
827-
// Skip this time — continue launch without setting the field.
830+
// Skip this time — continue launch without setting the field. Don't suppress
831+
// the prompt on the next launch since we didn't persist anything.
828832
msgDialogState = MessageDialogState(false)
829-
relaunch()
833+
relaunch(false)
830834
}
831835
onActionClick = {
832836
// Don't ask again for this game.
833837
msgDialogState = MessageDialogState(false)
834838
CoroutineScope(Dispatchers.IO).launch {
835-
runCatching {
839+
val persisted = runCatching {
836840
val container = ContainerUtils.getContainer(context, state.launchedAppId)
837841
container.putExtra("sdkCloudBridgePromptDismissed", "1")
838842
container.saveData()
839843
}.onFailure { Timber.w(it, "Failed to persist sdkCloudBridgePromptDismissed") }
840-
withContext(Dispatchers.Main) { relaunch() }
844+
.isSuccess
845+
withContext(Dispatchers.Main) { relaunch(persisted) }
841846
}
842847
}
843848
onDismissRequest = {
844849
msgDialogState = MessageDialogState(false)
845-
relaunch()
850+
relaunch(false)
846851
}
847852
}
848853

@@ -1639,12 +1644,16 @@ fun preLaunchApp(
16391644
// That intersection is a reliable Pattern B signal and keeps false positives low.
16401645
//
16411646
// Skip the prompt for non-game launches (Open Container / temporary override / explicit
1642-
// skipCloudSync). The post-prompt `relaunch` lambda doesn't carry those flags forward,
1643-
// so firing the prompt for them would silently turn an Open-Container into a game launch.
1647+
// skipCloudSync) and for re-entries from conflict resolution (preferredSave set, or
1648+
// ignorePendingOperations true). The post-prompt `relaunch` lambda doesn't carry those
1649+
// flags forward, so firing the prompt for them would silently drop the user's
1650+
// conflict-resolution choice.
16441651
if (!skipBridgePrompt &&
16451652
!bootToContainer &&
16461653
!useTemporaryOverride &&
16471654
!skipCloudSync &&
1655+
!ignorePendingOperations &&
1656+
preferredSave == SaveLocation.None &&
16481657
gameSource == GameSource.STEAM &&
16491658
container.isLaunchRealSteam &&
16501659
container.sdkCloudSaveSubdir.isBlank() &&

app/src/main/java/app/gamenative/utils/ContainerUtils.kt

Lines changed: 8 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -827,62 +827,15 @@ object ContainerUtils {
827827
customConfig
828828
}
829829
} else {
830-
// Use default config with drives
831-
ContainerData(
832-
screenSize = PrefManager.screenSize,
833-
envVars = PrefManager.envVars,
834-
cpuList = PrefManager.cpuList,
835-
cpuListWoW64 = PrefManager.cpuListWoW64,
836-
graphicsDriver = PrefManager.graphicsDriver,
837-
graphicsDriverVersion = PrefManager.graphicsDriverVersion,
838-
graphicsDriverConfig = PrefManager.graphicsDriverConfig,
839-
dxwrapper = initialDxWrapper,
840-
dxwrapperConfig = PrefManager.dxWrapperConfig,
841-
audioDriver = PrefManager.audioDriver,
842-
wincomponents = PrefManager.winComponents,
830+
// Build the default config from PrefManager and override only what this
831+
// create-site needs to vary (drives + the per-game initial dxwrapper). Inheriting
832+
// from getDefaultContainerData() means new containers automatically pick up
833+
// any new fields added there (localSavesOnly, useSteamInput, sharpnessEffect/Level/
834+
// Denoise, vibrationMode/Intensity, lsfgEnabled, ...) without this branch needing
835+
// to be edited every time.
836+
getDefaultContainerData().copy(
843837
drives = drives,
844-
execArgs = PrefManager.execArgs,
845-
showFPS = false,
846-
launchRealSteam = PrefManager.launchRealSteam,
847-
disableSteamOverlay = PrefManager.disableSteamOverlay,
848-
wow64Mode = PrefManager.wow64Mode,
849-
startupSelection = PrefManager.startupSelection.toByte(),
850-
box86Version = PrefManager.box86Version,
851-
box64Version = PrefManager.box64Version,
852-
box86Preset = PrefManager.box86Preset,
853-
box64Preset = PrefManager.box64Preset,
854-
desktopTheme = WineThemeManager.DEFAULT_DESKTOP_THEME,
855-
language = PrefManager.containerLanguage,
856-
containerVariant = PrefManager.containerVariant,
857-
wineVersion = PrefManager.wineVersion,
858-
emulator = PrefManager.emulator,
859-
fexcoreVersion = PrefManager.fexcoreVersion,
860-
fexcoreTSOMode = PrefManager.fexcoreTSOMode,
861-
fexcoreX87Mode = PrefManager.fexcoreX87Mode,
862-
fexcoreMultiBlock = PrefManager.fexcoreMultiBlock,
863-
fexcorePreset = PrefManager.fexcorePreset,
864-
renderer = PrefManager.renderer,
865-
csmt = PrefManager.csmt,
866-
videoPciDeviceID = PrefManager.videoPciDeviceID,
867-
offScreenRenderingMode = PrefManager.offScreenRenderingMode,
868-
strictShaderMath = PrefManager.strictShaderMath,
869-
useDRI3 = PrefManager.useDRI3,
870-
videoMemorySize = PrefManager.videoMemorySize,
871-
mouseWarpOverride = PrefManager.mouseWarpOverride,
872-
enableXInput = PrefManager.xinputEnabled,
873-
enableDInput = PrefManager.dinputEnabled,
874-
dinputMapperType = PrefManager.dinputMapperType.toByte(),
875-
disableMouseInput = PrefManager.disableMouseInput,
876-
forceDlc = PrefManager.forceDlc,
877-
steamOfflineMode = PrefManager.steamOfflineMode,
878-
useLegacyDRM = PrefManager.useLegacyDRM,
879-
unpackFiles = PrefManager.unpackFiles,
880-
suspendPolicy = PrefManager.suspendPolicy,
881-
portraitMode = PrefManager.portraitMode,
882-
externalDisplayMode = PrefManager.externalDisplayInputMode,
883-
externalDisplaySwap = PrefManager.externalDisplaySwap,
884-
vibrationMode = PrefManager.vibrationMode,
885-
vibrationIntensity = PrefManager.vibrationIntensity,
838+
dxwrapper = initialDxWrapper,
886839
)
887840
}
888841

app/src/main/java/app/gamenative/utils/SteamUtils.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,12 @@ object SteamUtils {
596596
?: File(imageFs.wineprefix)
597597
val steamConfigDir = File(winePrefixBase, "drive_c/Program Files (x86)/Steam/config")
598598
try {
599+
// Ensure the Steam config dir exists. On a fresh container the Wine prefix
600+
// skeleton is in place but Steam's own subdir hasn't been laid down yet —
601+
// writeText() would fail with FileNotFoundException without this.
602+
if (!steamConfigDir.isDirectory && !steamConfigDir.mkdirs()) {
603+
Timber.w("autoLoginUserChanges: failed to create $steamConfigDir; loginusers.vdf write may fail")
604+
}
599605
File(steamConfigDir, "loginusers.vdf").writeText(vdfFileText)
600606
val userRegFile = File(winePrefixBase, "user.reg")
601607
val steamRoot = "C:\\Program Files (x86)\\Steam"
@@ -1108,10 +1114,15 @@ object SteamUtils {
11081114
val existingBuildId = parseAcfBuildId(staleAcf)
11091115
val existingDepots = parseAcfInstalledDepotIds(staleAcf)
11101116
val existingScripts = parseAcfInstallScriptDepotIds(staleAcf)
1117+
val existingOwner = parseAcfLastOwner(staleAcf)
11111118
val depotsMatch = existingDepots == presentDepotIds
11121119
val buildIdMatch = existingBuildId == sharedBuildId
11131120
val scriptsMatch = existingScripts == presentScriptDepotIds
1114-
if (depotsMatch && buildIdMatch && scriptsMatch) {
1121+
// LastOwner mismatch means the manifest was last written by a different
1122+
// signed-in account; reusing it would attribute the install to that account
1123+
// and break cloud/ownership checks for the current user. Force a rewrite.
1124+
val lastOwnerMatch = existingOwner == lastOwner
1125+
if (depotsMatch && buildIdMatch && scriptsMatch && lastOwnerMatch) {
11151126
val updateResult = parseAcfUpdateResult(staleAcf)
11161127
if (updateResult == 0L) return
11171128
staleAcf.delete()
@@ -1217,6 +1228,7 @@ object SteamUtils {
12171228

12181229
private val acfBuildIdRegex = Regex("\"buildid\"\\s*\"(\\d+)\"")
12191230
private val acfUpdateResultRegex = Regex("\"UpdateResult\"\\s*\"(\\d+)\"")
1231+
private val acfLastOwnerRegex = Regex("\"LastOwner\"\\s*\"(\\d+)\"")
12201232

12211233
// Matches `"<depotId>" { "manifest" "<gid>" ... }` entries inside the
12221234
// InstalledDepots block of our own acf output. Tolerant of whitespace /
@@ -1244,6 +1256,14 @@ object SteamUtils {
12441256
}
12451257
}
12461258

1259+
private fun parseAcfLastOwner(acf: File): String {
1260+
return try {
1261+
acfLastOwnerRegex.find(acf.readText())?.groupValues?.get(1).orEmpty()
1262+
} catch (_: Exception) {
1263+
""
1264+
}
1265+
}
1266+
12471267
private fun parseAcfInstalledDepotIds(acf: File): Set<Int> {
12481268
return try {
12491269
val text = acf.readText()

0 commit comments

Comments
 (0)