@@ -40,6 +40,7 @@ import app.gamenative.R
4040import app.gamenative.data.LibraryItem
4141import app.gamenative.enums.Marker
4242import app.gamenative.enums.PathType
43+ import app.gamenative.enums.SaveLocation
4344import app.gamenative.enums.SyncResult
4445import app.gamenative.events.AndroidEvent
4546import app.gamenative.service.DownloadService
@@ -60,6 +61,7 @@ import app.gamenative.workshop.WorkshopManager
6061import app.gamenative.NetworkMonitor
6162import com.google.android.play.core.splitcompat.SplitCompat
6263import com.posthog.PostHog
64+ import com.winlator.container.Container
6365import com.winlator.container.ContainerData
6466import com.winlator.container.ContainerManager
6567import com.winlator.fexcore.FEXCoreManager
@@ -213,6 +215,78 @@ class SteamAppScreen : BaseAppScreen() {
213215 fun getPendingUpdateVerifyOperation (gameId : Int ): AppOptionMenuType ? {
214216 return pendingUpdateVerifyOperations[gameId]
215217 }
218+
219+ fun performForceCloudSync (
220+ context : Context ,
221+ appId : String ,
222+ gameId : Int ,
223+ preferredSave : SaveLocation = SaveLocation .None ,
224+ container : Container ? = null,
225+ ) {
226+ CoroutineScope (Dispatchers .IO ).launch {
227+ val steamId = SteamService .userSteamId
228+ if (steamId == null ) {
229+ SnackbarManager .show(context.getString(R .string.steam_not_logged_in))
230+ return @launch
231+ }
232+
233+ val resolvedContainer = container ? : ContainerUtils .getOrCreateContainer(context, appId)
234+ val containerManager = ContainerManager (context)
235+ containerManager.activateContainer(resolvedContainer)
236+
237+ val prefixToPath: (String ) -> String = { prefix ->
238+ PathType .from(prefix).toAbsPath(context, gameId, steamId.accountID)
239+ }
240+ val syncResult = SteamService .forceSyncUserFiles(
241+ appId = gameId,
242+ prefixToPath = prefixToPath,
243+ preferredSave = preferredSave,
244+ ).await()
245+
246+ when (syncResult.syncResult) {
247+ SyncResult .Success -> {
248+ SnackbarManager .show(context.getString(R .string.steam_cloud_sync_success))
249+ }
250+
251+ SyncResult .UpToDate -> {
252+ SnackbarManager .show(context.getString(R .string.steam_cloud_sync_up_to_date))
253+ }
254+
255+ SyncResult .Conflict -> {
256+ val fmt = java.text.DateFormat .getDateTimeInstance(
257+ java.text.DateFormat .MEDIUM ,
258+ java.text.DateFormat .SHORT ,
259+ )
260+ withContext(Dispatchers .Main ) {
261+ showInstallDialog(
262+ gameId,
263+ MessageDialogState (
264+ visible = true ,
265+ type = DialogType .CLOUD_SYNC_CONFLICT ,
266+ title = context.getString(R .string.main_save_conflict_title),
267+ message = context.getString(
268+ R .string.main_save_conflict_message,
269+ fmt.format(java.util.Date (syncResult.localTimestamp)),
270+ fmt.format(java.util.Date (syncResult.remoteTimestamp)),
271+ ),
272+ confirmBtnText = context.getString(R .string.main_keep_remote),
273+ dismissBtnText = context.getString(R .string.main_keep_local),
274+ ),
275+ )
276+ }
277+ }
278+
279+ else -> {
280+ SnackbarManager .show(
281+ context.getString(
282+ R .string.steam_cloud_sync_failed,
283+ syncResult.syncResult,
284+ ),
285+ )
286+ }
287+ }
288+ }
289+ }
216290 }
217291
218292 @Composable
@@ -709,15 +783,20 @@ class SteamAppScreen : BaseAppScreen() {
709783 AppMenuOption (
710784 AppOptionMenuType .VerifyFiles ,
711785 onClick = {
712- // Show confirmation dialog before verifying
713786 setPendingUpdateVerifyOperation(gameId, AppOptionMenuType .VerifyFiles )
787+ val container = ContainerUtils .getOrCreateContainer(context, appId)
788+ val verifyMessage = if (container.isLocalSavesOnly) {
789+ context.getString(R .string.steam_verify_files_message_local_saves)
790+ } else {
791+ context.getString(R .string.steam_verify_files_message)
792+ }
714793 showInstallDialog(
715794 gameId,
716795 MessageDialogState (
717796 visible = true ,
718797 type = DialogType .UPDATE_VERIFY_CONFIRM ,
719798 title = context.getString(R .string.steam_verify_files_title),
720- message = context.getString( R .string.steam_verify_files_message) ,
799+ message = verifyMessage ,
721800 confirmBtnText = context.getString(R .string.steam_continue),
722801 dismissBtnText = context.getString(R .string.cancel),
723802 ),
@@ -755,43 +834,21 @@ class SteamAppScreen : BaseAppScreen() {
755834 event = " cloud_sync_forced" ,
756835 properties = mapOf (" game_name" to appInfo.name),
757836 )
758- CoroutineScope (Dispatchers .IO ).launch {
759- val steamId = SteamService .userSteamId
760- if (steamId == null ) {
761- SnackbarManager .show(context.getString(R .string.steam_not_logged_in))
762- return @launch
763- }
764-
765- val containerManager = ContainerManager (context)
766- val container = ContainerUtils .getOrCreateContainer(context, appId)
767- containerManager.activateContainer(container)
768-
769- val prefixToPath: (String ) -> String = { prefix ->
770- PathType .from(prefix).toAbsPath(context, gameId, steamId.accountID)
771- }
772- val syncResult = SteamService .forceSyncUserFiles(
773- appId = gameId,
774- prefixToPath = prefixToPath,
775- ).await()
776-
777- when (syncResult.syncResult) {
778- SyncResult .Success -> {
779- SnackbarManager .show(context.getString(R .string.steam_cloud_sync_success))
780- }
781-
782- SyncResult .UpToDate -> {
783- SnackbarManager .show(context.getString(R .string.steam_cloud_sync_up_to_date))
784- }
785-
786- else -> {
787- SnackbarManager .show(
788- context.getString(
789- R .string.steam_cloud_sync_failed,
790- syncResult.syncResult,
791- ),
792- )
793- }
794- }
837+ val container = ContainerUtils .getOrCreateContainer(context, appId)
838+ if (container.isLocalSavesOnly) {
839+ showInstallDialog(
840+ gameId,
841+ MessageDialogState (
842+ visible = true ,
843+ type = DialogType .CLOUD_SYNC_CONFLICT ,
844+ title = context.getString(R .string.cloud_sync_local_saves_only_title),
845+ message = context.getString(R .string.cloud_sync_local_saves_only_message),
846+ confirmBtnText = context.getString(R .string.main_keep_remote),
847+ dismissBtnText = context.getString(R .string.main_keep_local),
848+ ),
849+ )
850+ } else {
851+ performForceCloudSync(context, appId, gameId, container = container)
795852 }
796853 },
797854 ),
@@ -1058,18 +1115,21 @@ class SteamAppScreen : BaseAppScreen() {
10581115
10591116 if (operation == AppOptionMenuType .VerifyFiles ) {
10601117 MarkerUtils .clearInstalledPrerequisiteMarkers(getAppDirPath(gameId))
1061- val steamId = SteamService .userSteamId
1062- if (steamId != null ) {
1063- val prefixToPath: (String ) -> String = { prefix ->
1064- PathType .from(prefix).toAbsPath(context, gameId, steamId.accountID)
1118+ // skip cloud sync if local saves only — verify is about game files, not saves
1119+ if (! container.isLocalSavesOnly) {
1120+ val steamId = SteamService .userSteamId
1121+ if (steamId != null ) {
1122+ val prefixToPath: (String ) -> String = { prefix ->
1123+ PathType .from(prefix).toAbsPath(context, gameId, steamId.accountID)
1124+ }
1125+ SteamService .forceSyncUserFiles(
1126+ appId = gameId,
1127+ prefixToPath = prefixToPath,
1128+ overrideLocalChangeNumber = - 1 ,
1129+ ).await()
1130+ } else {
1131+ SnackbarManager .show(context.getString(R .string.steam_not_logged_in))
10651132 }
1066- SteamService .forceSyncUserFiles(
1067- appId = gameId,
1068- prefixToPath = prefixToPath,
1069- overrideLocalChangeNumber = - 1 ,
1070- ).await()
1071- } else {
1072- SnackbarManager .show(context.getString(R .string.steam_not_logged_in))
10731133 }
10741134 }
10751135
@@ -1120,14 +1180,30 @@ class SteamAppScreen : BaseAppScreen() {
11201180 }
11211181 }
11221182
1183+ DialogType .CLOUD_SYNC_CONFLICT -> {
1184+ {
1185+ hideInstallDialog(gameId)
1186+ performForceCloudSync(context, libraryItem.appId, gameId, SaveLocation .Remote )
1187+ }
1188+ }
1189+
11231190 else -> null
11241191 }
1192+ val effectiveDismissClick: (() -> Unit )? = when (installDialogState.type) {
1193+ DialogType .CLOUD_SYNC_CONFLICT -> {
1194+ {
1195+ hideInstallDialog(gameId)
1196+ performForceCloudSync(context, libraryItem.appId, gameId, SaveLocation .Local )
1197+ }
1198+ }
1199+ else -> onDismissClick
1200+ }
11251201
11261202 MessageDialog (
11271203 visible = installDialogState.visible,
11281204 onDismissRequest = onDismissRequest,
11291205 onConfirmClick = onConfirmClick,
1130- onDismissClick = onDismissClick ,
1206+ onDismissClick = effectiveDismissClick ,
11311207 confirmBtnText = installDialogState.confirmBtnText,
11321208 dismissBtnText = installDialogState.dismissBtnText,
11331209 title = installDialogState.title,
0 commit comments