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
12 changes: 12 additions & 0 deletions app/src/main/java/app/gamenative/PrefManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,18 @@ object PrefManager {
setPref(LOCAL_SAVES_ONLY, value)
}

// app IDs whose cloud cache was cleared because local-saves-only was toggled off
// (not an upgrade) — checked in SteamAutoCloud to avoid showing upgrade conflict text
private val PENDING_CLOUD_RESYNC = stringPreferencesKey("pending_cloud_resync")
var pendingCloudResync: Set<Int>
get() {
val raw = getPref(PENDING_CLOUD_RESYNC, "")
return if (raw.isEmpty()) emptySet() else raw.split(',').mapNotNull { it.toIntOrNull() }.toSet()
}
set(value) {
setPref(PENDING_CLOUD_RESYNC, value.joinToString(","))
}

private val STEAM_OFFLINE_MODE = booleanPreferencesKey("steam_offline_mode")
var steamOfflineMode: Boolean
get() = getPref(STEAM_OFFLINE_MODE, false)
Expand Down
17 changes: 9 additions & 8 deletions app/src/main/java/app/gamenative/service/SteamAutoCloud.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.gamenative.service

import androidx.room.withTransaction
import app.gamenative.PrefManager
import app.gamenative.data.PostSyncInfo
import app.gamenative.data.SaveFilePattern
import app.gamenative.data.SteamApp
Expand Down Expand Up @@ -887,13 +888,10 @@ object SteamAutoCloud {
// check if local state is byte-identical to remote — this is
// the "cache-wiped by destructive migration, nothing actually
// changed" case and should be silent. key by absolute filesystem
// path: cloud stores files as (pathPrefixIndex, basename) while
// local scan stores filename as subdir-relative path with a
// single pattern prefix, so basename-only keys won't match for
// nested files.
// windows paths are case-insensitive; steam cloud and wine may
// disagree on case. lowercase the keys so content-identical
// files compare equal regardless.
// path (cloud stores (pathPrefixIndex, basename), local scan
// stores subpath-relative filename; basename-only won't match
// for nested files). lowercase since windows paths are
// case-insensitive and wine/cloud may disagree on case.
val localByPath = allLocalUserFiles.associate {
it.getAbsPath(prefixToPath).toString().lowercase() to it.sha
}
Expand All @@ -918,9 +916,12 @@ object SteamAutoCloud {
rehydratedSilently = true
} else {
hasLocalChanges = true
conflictUfsVersion = CURRENT_UFS_PARSE_VERSION
remoteTimestamp = appFileListChange.files.map { it.timestamp.time }.maxOrNull() ?: 0L
localTimestamp = allLocalUserFiles.map { it.timestamp }.maxOrNull() ?: 0L
// show upgrade-specific text unless this was a local-saves toggle-off
if (appInfo.id !in PrefManager.pendingCloudResync) {
conflictUfsVersion = CURRENT_UFS_PARSE_VERSION
}
}
}

Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/app/gamenative/service/SteamService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ import app.gamenative.utils.generateSteamApp
import app.gamenative.workshop.WorkshopManager
import com.winlator.container.Container
import com.winlator.xenvironment.ImageFs
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import `in`.dragonbra.javasteam.depotdownloader.DepotDownloader
import `in`.dragonbra.javasteam.depotdownloader.IDownloadListener
import `in`.dragonbra.javasteam.depotdownloader.data.AppItem
Expand Down Expand Up @@ -2673,6 +2677,25 @@ class SteamService : Service(), IChallengeUrlChanged {
else -> false
}

@EntryPoint
@InstallIn(SingletonComponent::class)
interface CloudSyncCacheEntryPoint {
fun fileChangeListsDao(): FileChangeListsDao
}

// runBlocking is intentional: must complete before pendingCloudResync write that follows
fun clearCloudSyncCache(context: Context, appId: Int) {
val dao = instance?.fileChangeListsDao
?: EntryPointAccessors.fromApplication(
context.applicationContext,
CloudSyncCacheEntryPoint::class.java,
).fileChangeListsDao()
runBlocking {
dao.deleteByAppId(appId)
}
Timber.i("Cleared cloud sync cache for appId=$appId")
}

fun clearDatabase(clearCloudSyncState: Boolean = false) {
with(instance!!) {
scope.launch {
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/app/gamenative/ui/PluviaMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2006,6 +2006,9 @@ fun preLaunchApp(

when (postSyncInfo.syncResult) {
SyncResult.Conflict -> {
// clear pending resync flag if present — it already prevented
// SteamAutoCloud from setting conflictUfsVersion
PrefManager.pendingCloudResync = PrefManager.pendingCloudResync - gameId
val localDate = Date(postSyncInfo.localTimestamp).toString()
val remoteDate = Date(postSyncInfo.remoteTimestamp).toString()
val (conflictTitle, conflictMessage) = postSyncInfo.conflictUfsVersion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,13 @@ fun GeneralTabContent(
state = config.forceDlc,
onCheckedChange = { state.config.value = config.copy(forceDlc = it) },
)
// SettingsSwitch(
// colors = settingsTileColorsAlt(),
// title = { Text(text = stringResource(R.string.local_saves_only)) },
// subtitle = { Text(text = stringResource(R.string.local_saves_only_description)) },
// state = config.localSavesOnly,
// onCheckedChange = { state.config.value = config.copy(localSavesOnly = it) },
// )
SettingsSwitch(
colors = settingsTileColorsAlt(),
title = { Text(text = stringResource(R.string.local_saves_only)) },
subtitle = { Text(text = stringResource(R.string.local_saves_only_description)) },
state = config.localSavesOnly,
onCheckedChange = { state.config.value = config.copy(localSavesOnly = it) },
)
SettingsSwitch(
colors = settingsTileColorsAlt(),
title = { Text(text = stringResource(R.string.use_legacy_drm)) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.ContextCompat
import app.gamenative.PrefManager
import app.gamenative.PluviaApp

import app.gamenative.PrefManager
import app.gamenative.R
import app.gamenative.data.LibraryItem
import app.gamenative.enums.Marker
import app.gamenative.enums.PathType
import app.gamenative.enums.SaveLocation
import app.gamenative.enums.SyncResult
import app.gamenative.events.AndroidEvent
import app.gamenative.service.DownloadService
Expand All @@ -53,6 +53,7 @@ import app.gamenative.ui.data.AppMenuOption
import app.gamenative.ui.data.GameDisplayInfo
import app.gamenative.ui.enums.AppOptionMenuType
import app.gamenative.ui.enums.DialogType
import java.util.Date
import app.gamenative.utils.ContainerUtils
import app.gamenative.utils.MarkerUtils
import app.gamenative.utils.SteamUtils
Expand All @@ -61,6 +62,7 @@ import app.gamenative.workshop.WorkshopManager
import app.gamenative.NetworkMonitor
import com.google.android.play.core.splitcompat.SplitCompat
import com.posthog.PostHog
import com.winlator.container.Container
import com.winlator.container.ContainerData
import com.winlator.container.ContainerManager
import com.winlator.fexcore.FEXCoreManager
Expand Down Expand Up @@ -214,6 +216,76 @@ class SteamAppScreen : BaseAppScreen() {
fun getPendingUpdateVerifyOperation(gameId: Int): AppOptionMenuType? {
return pendingUpdateVerifyOperations[gameId]
}

fun performForceCloudSync(
context: Context,
appId: String,
gameId: Int,
preferredSave: SaveLocation = SaveLocation.None,
container: Container? = null,
) {
CoroutineScope(Dispatchers.IO).launch {
val steamId = SteamService.userSteamId
if (steamId == null) {
SnackbarManager.show(context.getString(R.string.steam_not_logged_in))
return@launch
}

val resolvedContainer = container ?: ContainerUtils.getOrCreateContainer(context, appId)
val containerManager = ContainerManager(context)
containerManager.activateContainer(resolvedContainer)

val prefixToPath: (String) -> String = { prefix ->
PathType.from(prefix).toAbsPath(context, gameId, steamId.accountID)
}
val syncResult = SteamService.forceSyncUserFiles(
appId = gameId,
prefixToPath = prefixToPath,
preferredSave = preferredSave,
).await()

when (syncResult.syncResult) {
SyncResult.Success -> {
SnackbarManager.show(context.getString(R.string.library_cloud_sync_success))
}

SyncResult.UpToDate -> {
SnackbarManager.show(context.getString(R.string.library_cloud_sync_up_to_date))
}

SyncResult.InProgress -> {
SnackbarManager.show(context.getString(R.string.library_cloud_sync_in_progress))
}

SyncResult.Conflict -> {
val localDate = Date(syncResult.localTimestamp).toString()
val remoteDate = Date(syncResult.remoteTimestamp).toString()
withContext(Dispatchers.Main) {
showInstallDialog(
gameId,
MessageDialogState(
visible = true,
type = DialogType.SYNC_CONFLICT,
title = context.getString(R.string.main_save_conflict_title),
message = context.getString(R.string.main_save_conflict_message, localDate, remoteDate),
confirmBtnText = context.getString(R.string.main_keep_remote),
dismissBtnText = context.getString(R.string.main_keep_local),
),
Comment thread
jeremybernstein marked this conversation as resolved.
)
}
}

else -> {
SnackbarManager.show(
context.getString(
R.string.library_cloud_sync_error,
syncResult.syncResult,
),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
}
}

@Composable
Expand Down Expand Up @@ -712,15 +784,20 @@ class SteamAppScreen : BaseAppScreen() {
AppMenuOption(
AppOptionMenuType.VerifyFiles,
onClick = {
// Show confirmation dialog before verifying
setPendingUpdateVerifyOperation(gameId, AppOptionMenuType.VerifyFiles)
val container = ContainerUtils.getOrCreateContainer(context, appId)
val verifyMessage = if (container.isLocalSavesOnly) {
context.getString(R.string.steam_verify_files_message_local_saves)
} else {
context.getString(R.string.steam_verify_files_message)
}
showInstallDialog(
gameId,
MessageDialogState(
visible = true,
type = DialogType.UPDATE_VERIFY_CONFIRM,
title = context.getString(R.string.steam_verify_files_title),
message = context.getString(R.string.steam_verify_files_message),
message = verifyMessage,
confirmBtnText = context.getString(R.string.steam_continue),
dismissBtnText = context.getString(R.string.cancel),
),
Expand Down Expand Up @@ -760,45 +837,21 @@ class SteamAppScreen : BaseAppScreen() {
properties = mapOf("game_name" to appInfo.name),
)
}
CoroutineScope(Dispatchers.IO).launch {
SnackbarManager.show(context.getString(R.string.library_cloud_sync_starting))

val steamId = SteamService.userSteamId
if (steamId == null) {
SnackbarManager.show(context.getString(R.string.steam_not_logged_in))
return@launch
}

val containerManager = ContainerManager(context)
val container = ContainerUtils.getOrCreateContainer(context, appId)
containerManager.activateContainer(container)

val prefixToPath: (String) -> String = { prefix ->
PathType.from(prefix).toAbsPath(context, gameId, steamId.accountID)
}
val syncResult = SteamService.forceSyncUserFiles(
appId = gameId,
prefixToPath = prefixToPath,
).await()

when (syncResult.syncResult) {
SyncResult.Success -> {
SnackbarManager.show(context.getString(R.string.library_cloud_sync_success))
}

SyncResult.UpToDate -> {
SnackbarManager.show(context.getString(R.string.library_cloud_sync_up_to_date))
}

else -> {
SnackbarManager.show(
context.getString(
R.string.library_cloud_sync_error,
syncResult.syncResult,
),
)
}
}
val container = ContainerUtils.getOrCreateContainer(context, appId)
if (container.isLocalSavesOnly) {
showInstallDialog(
gameId,
MessageDialogState(
visible = true,
type = DialogType.SYNC_CONFLICT,
title = context.getString(R.string.cloud_sync_local_saves_only_title),
message = context.getString(R.string.cloud_sync_local_saves_only_message),
confirmBtnText = context.getString(R.string.main_keep_remote),
dismissBtnText = context.getString(R.string.main_keep_local),
),
)
} else {
performForceCloudSync(context, appId, gameId, container = container)
}
},
),
Expand Down Expand Up @@ -1068,18 +1121,21 @@ class SteamAppScreen : BaseAppScreen() {

if (operation == AppOptionMenuType.VerifyFiles) {
MarkerUtils.clearInstalledPrerequisiteMarkers(getAppDirPath(gameId))
val steamId = SteamService.userSteamId
if (steamId != null) {
val prefixToPath: (String) -> String = { prefix ->
PathType.from(prefix).toAbsPath(context, gameId, steamId.accountID)
// skip cloud sync if local saves only — verify is about game files, not saves
if (!container.isLocalSavesOnly) {
val steamId = SteamService.userSteamId
if (steamId != null) {
val prefixToPath: (String) -> String = { prefix ->
PathType.from(prefix).toAbsPath(context, gameId, steamId.accountID)
}
SteamService.forceSyncUserFiles(
appId = gameId,
prefixToPath = prefixToPath,
overrideLocalChangeNumber = -1,
).await()
} else {
SnackbarManager.show(context.getString(R.string.steam_not_logged_in))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
SteamService.forceSyncUserFiles(
appId = gameId,
prefixToPath = prefixToPath,
overrideLocalChangeNumber = -1,
).await()
} else {
SnackbarManager.show(context.getString(R.string.steam_not_logged_in))
}
}

Expand Down Expand Up @@ -1130,14 +1186,32 @@ class SteamAppScreen : BaseAppScreen() {
}
}

DialogType.SYNC_CONFLICT -> {
{
hideInstallDialog(gameId)
PrefManager.pendingCloudResync = PrefManager.pendingCloudResync - gameId
performForceCloudSync(context, libraryItem.appId, gameId, SaveLocation.Remote)
}
}

else -> null
}
val effectiveDismissClick: (() -> Unit)? = when (installDialogState.type) {
DialogType.SYNC_CONFLICT -> {
{
hideInstallDialog(gameId)
PrefManager.pendingCloudResync = PrefManager.pendingCloudResync - gameId
performForceCloudSync(context, libraryItem.appId, gameId, SaveLocation.Local)
}
}
else -> onDismissClick
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

MessageDialog(
visible = installDialogState.visible,
onDismissRequest = onDismissRequest,
onConfirmClick = onConfirmClick,
onDismissClick = onDismissClick,
onDismissClick = effectiveDismissClick,
confirmBtnText = installDialogState.confirmBtnText,
dismissBtnText = installDialogState.dismissBtnText,
title = installDialogState.title,
Expand Down
Loading
Loading