From 636adeef85c05deb4c7034a66493d47ca7430377 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Fri, 23 Jan 2026 18:05:33 +0530 Subject: [PATCH 1/4] exit container cleanly if game is exited --- .../ui/screen/xserver/XServerScreen.kt | 132 ++++++++++++++++++ .../java/com/winlator/core/WineUtils.java | 35 ++++- 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 99773be19..f0ace8bda 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -108,6 +108,8 @@ import com.winlator.widget.TouchpadView import com.winlator.widget.XServerView import com.winlator.winhandler.WinHandler import com.winlator.winhandler.WinHandler.PreferredInputApi +import com.winlator.winhandler.OnGetProcessInfoListener +import com.winlator.winhandler.ProcessInfo import com.winlator.xconnector.UnixSocketConfig import com.winlator.xenvironment.ImageFs import com.winlator.xenvironment.XEnvironment @@ -128,9 +130,14 @@ import com.winlator.xserver.ScreenInfo import com.winlator.xserver.Window import com.winlator.xserver.WindowManager import com.winlator.xserver.XServer +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import org.json.JSONException import org.json.JSONObject import timber.log.Timber @@ -154,6 +161,50 @@ private const val ALWAYS_REEXTRACT = true // Guard to prevent duplicate game_exited events when multiple exit triggers fire simultaneously private val isExiting = AtomicBoolean(false) +private const val EXIT_PROCESS_TIMEOUT_MS = 30_000L +private const val EXIT_PROCESS_POLL_INTERVAL_MS = 1_000L +private const val EXIT_PROCESS_RESPONSE_TIMEOUT_MS = 2_000L +private val CORE_WINE_PROCESSES = setOf( + "wineserver", + "services", + "start", + "winhandler", + "tabtip", + "explorer", + "winedevice", + "svchost", +) + +private fun normalizeProcessName(name: String): String { + val trimmed = name.trim().trim('"') + val base = trimmed.substringAfterLast('/').substringAfterLast('\\') + val lower = base.lowercase(Locale.getDefault()) + return if (lower.endsWith(".exe")) lower.removeSuffix(".exe") else lower +} + +private fun extractExecutableBasename(path: String): String { + if (path.isBlank()) return "" + return normalizeProcessName(path) +} + +private fun windowMatchesExecutable(window: Window, targetExecutable: String): Boolean { + if (targetExecutable.isBlank()) return false + val normalizedTarget = normalizeProcessName(targetExecutable) + val candidates = listOf(window.name, window.className) + return candidates.any { candidate -> + candidate.split('\u0000') + .asSequence() + .map { normalizeProcessName(it) } + .any { it == normalizedTarget } + } +} + +private fun buildEssentialProcessAllowlist(): Set { + val essentialServices = WineUtils.getEssentialServiceNames() + .map { normalizeProcessName(it) } + return (essentialServices + CORE_WINE_PROCESSES).toSet() +} + // TODO logs in composables are 'unstable' which can cause recomposition (performance issues) @Composable @@ -244,11 +295,14 @@ fun XServerScreen( var win32AppWorkarounds: Win32AppWorkarounds? by remember { mutableStateOf(null) } var physicalControllerHandler: PhysicalControllerHandler? by remember { mutableStateOf(null) } + var exitWatchJob: Job? by remember { mutableStateOf(null) } DisposableEffect(Unit) { onDispose { physicalControllerHandler?.cleanup() physicalControllerHandler = null + exitWatchJob?.cancel() + exitWatchJob = null } } var isKeyboardVisible = false @@ -260,6 +314,83 @@ fun XServerScreen( var elementToEdit by remember { mutableStateOf(null) } var showPhysicalControllerDialog by remember { mutableStateOf(false) } + fun startExitWatchForUnmappedGameWindow(window: Window) { + val winHandler = xServerView?.getxServer()?.winHandler ?: return + if (exitWatchJob?.isActive == true) return + val targetExecutable = extractExecutableBasename(container.executablePath) + if (!windowMatchesExecutable(window, targetExecutable)) return + + exitWatchJob = CoroutineScope(Dispatchers.IO).launch { + val allowlist = buildEssentialProcessAllowlist() + val previousListener = winHandler.getOnGetProcessInfoListener() + val lock = Any() + var pendingSnapshot: CompletableDeferred?>? = null + var currentList = mutableListOf() + var expectedCount = 0 + + val listener = OnGetProcessInfoListener { index, count, processInfo -> + previousListener?.onGetProcessInfo(index, count, processInfo) + synchronized(lock) { + val deferred = pendingSnapshot ?: return@synchronized + if (count == 0 && processInfo == null) { + if (!deferred.isCompleted) deferred.complete(null) + return@synchronized + } + if (index == 0) { + currentList = mutableListOf() + expectedCount = count + } + if (processInfo != null) { + currentList.add(processInfo) + } + if (currentList.size >= expectedCount && !deferred.isCompleted) { + deferred.complete(currentList.toList()) + } + } + } + + winHandler.setOnGetProcessInfoListener(listener) + try { + val startTime = System.currentTimeMillis() + while (System.currentTimeMillis() - startTime < EXIT_PROCESS_TIMEOUT_MS) { + val deferred = CompletableDeferred?>() + synchronized(lock) { + pendingSnapshot = deferred + } + winHandler.listProcesses() + val snapshot = withTimeoutOrNull(EXIT_PROCESS_RESPONSE_TIMEOUT_MS) { + deferred.await() + } + if (snapshot != null && snapshot.isNotEmpty()) { + val hasNonEssential = snapshot.any { + !allowlist.contains(normalizeProcessName(it.name)) + } + if (!hasNonEssential) { + withContext(Dispatchers.Main) { + exit( + winHandler, + PluviaApp.xEnvironment, + frameRating, + currentAppInfo, + container, + onExit, + navigateBack, + ) + } + break + } + } + delay(EXIT_PROCESS_POLL_INTERVAL_MS) + } + } finally { + winHandler.setOnGetProcessInfoListener(previousListener) + synchronized(lock) { + pendingSnapshot = null + } + } + } + } + val gameBack: () -> Unit = gameBack@{ val imeVisible = ViewCompat.getRootWindowInsets(view) ?.isVisible(WindowInsetsCompat.Type.ime()) == true @@ -613,6 +744,7 @@ fun XServerScreen( "\n\tchildrenSize: ${window.children.size}", ) changeFrameRatingVisibility(window, null) + startExitWatchForUnmappedGameWindow(window) onWindowUnmapped?.invoke(window) } }, diff --git a/app/src/main/java/com/winlator/core/WineUtils.java b/app/src/main/java/com/winlator/core/WineUtils.java index 3bebea53b..c37c3dad8 100644 --- a/app/src/main/java/com/winlator/core/WineUtils.java +++ b/app/src/main/java/com/winlator/core/WineUtils.java @@ -11,7 +11,10 @@ import org.json.JSONObject; import java.io.File; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.List; import java.util.Locale; import timber.log.Timber; @@ -355,8 +358,38 @@ else if (identifier.equals("wmdecoder")) { } } + private static final String[] SERVICE_DEFAULTS = { + "BITS:3", + "Eventlog:2", + "HTTP:3", + "LanmanServer:3", + "NDIS:2", + "PlugPlay:2", + "RpcSs:3", + "scardsvr:3", + "Schedule:3", + "Spooler:3", + "StiSvc:3", + "TermService:3", + "winebus:3", + "winehid:3", + "Winmgmt:3", + "wuauserv:3", + }; + + public static List getEssentialServiceNames() { + ArrayList names = new ArrayList<>(); + for (String service : SERVICE_DEFAULTS) { + int separator = service.indexOf(":"); + if (separator > 0) { + names.add(service.substring(0, separator)); + } + } + return Collections.unmodifiableList(names); + } + public static void changeServicesStatus(Container container, boolean onlyEssential) { - final String[] services = {"BITS:3", "Eventlog:2", "HTTP:3", "LanmanServer:3", "NDIS:2", "PlugPlay:2", "RpcSs:3", "scardsvr:3", "Schedule:3", "Spooler:3", "StiSvc:3", "TermService:3", "winebus:3", "winehid:3", "Winmgmt:3", "wuauserv:3"}; + final String[] services = SERVICE_DEFAULTS; File systemRegFile = new File(container.getRootDir(), ".wine/system.reg"); try (WineRegistryEditor registryEditor = new WineRegistryEditor(systemRegFile)) { From fc3826b3ad0f77ef3acb387a8dcd7f17226b2f4b Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Fri, 23 Jan 2026 18:26:16 +0530 Subject: [PATCH 2/4] force the correct executable to be fullscreen! --- .../java/app/gamenative/ui/screen/xserver/XServerScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index f0ace8bda..d58256f2d 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -681,7 +681,9 @@ fun XServerScreen( if (!bootToContainer) { renderer.setUnviewableWMClasses("explorer.exe") // TODO: make 'force fullscreen' be an option of the app being launched - appLaunchInfo?.let { renderer.forceFullscreenWMClass = Paths.get(it.executable).name } + if (container.executablePath.isNotBlank()) { + renderer.forceFullscreenWMClass = Paths.get(container.executablePath).name + } } getxServer().windowManager.addOnWindowModificationListener( object : WindowManager.OnWindowModificationListener { From 1061152a60b8994a53cff8fc22673efa8728d4dc Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Fri, 23 Jan 2026 18:50:43 +0530 Subject: [PATCH 3/4] small fix so container closing doesn't stall --- .../main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index d58256f2d..fc09814e9 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -361,7 +361,7 @@ fun XServerScreen( val snapshot = withTimeoutOrNull(EXIT_PROCESS_RESPONSE_TIMEOUT_MS) { deferred.await() } - if (snapshot != null && snapshot.isNotEmpty()) { + if (snapshot != null) { val hasNonEssential = snapshot.any { !allowlist.contains(normalizeProcessName(it.name)) } From 938601e3f0f5611947e469f8e6992592e9e759f0 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Fri, 23 Jan 2026 19:22:25 +0530 Subject: [PATCH 4/4] Fix for infinite sync in progress --- .../app/gamenative/service/SteamService.kt | 231 ++++++++++-------- .../main/java/app/gamenative/ui/PluviaMain.kt | 43 +++- .../app/gamenative/ui/enums/DialogType.kt | 1 + app/src/main/res/values-da/strings.xml | 3 + app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values-fr/strings.xml | 3 + app/src/main/res/values-it/strings.xml | 3 + app/src/main/res/values-pt-rBR/strings.xml | 3 + app/src/main/res/values-ro/strings.xml | 3 + app/src/main/res/values-uk/strings.xml | 3 + app/src/main/res/values-zh-rCN/strings.xml | 3 + app/src/main/res/values-zh-rTW/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 13 files changed, 194 insertions(+), 111 deletions(-) diff --git a/app/src/main/java/app/gamenative/service/SteamService.kt b/app/src/main/java/app/gamenative/service/SteamService.kt index 26090e263..73d386d4d 100644 --- a/app/src/main/java/app/gamenative/service/SteamService.kt +++ b/app/src/main/java/app/gamenative/service/SteamService.kt @@ -103,6 +103,7 @@ import java.util.Collections import java.util.EnumSet import java.util.concurrent.CancellationException import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.io.path.pathString import kotlin.time.Duration.Companion.seconds @@ -317,7 +318,30 @@ class SteamService : Service(), IChallengeUrlChanged { return File(dirPath).exists() && !MarkerUtils.hasMarker(dirPath, Marker.DOWNLOAD_COMPLETE_MARKER) } - private var syncInProgress: Boolean = false + private val syncInProgressApps = ConcurrentHashMap() + + private fun getSyncFlag(appId: Int): AtomicBoolean { + val existing = syncInProgressApps[appId] + if (existing != null) { + return existing + } + val created = AtomicBoolean(false) + val prior = syncInProgressApps.putIfAbsent(appId, created) + return prior ?: created + } + + private fun tryAcquireSync(appId: Int): Boolean { + val flag = getSyncFlag(appId) + return flag.compareAndSet(false, true) + } + + private fun releaseSync(appId: Int) { + val flag = syncInProgressApps[appId] + flag?.set(false) + if (flag != null && !flag.get()) { + syncInProgressApps.remove(appId, flag) + } + } // Track whether a game is currently running to prevent premature service stop @JvmStatic @@ -1916,71 +1940,71 @@ class SteamService : Service(), IChallengeUrlChanged { if (isOffline || !isConnected) { return@async PostSyncInfo(SyncResult.UpToDate) } - if (syncInProgress) { - Timber.w("Cannot launch app when sync already in progress") + if (!tryAcquireSync(appId)) { + Timber.w("Cannot launch app when sync already in progress for appId=$appId") return@async PostSyncInfo(SyncResult.InProgress) } - syncInProgress = true - - var syncResult = PostSyncInfo(SyncResult.UnknownFail) - - PrefManager.clientId?.let { clientId -> - instance?.let { steamInstance -> - getAppInfoOf(appId)?.let { appInfo -> - steamInstance._steamCloud?.let { steamCloud -> - val postSyncInfo = SteamAutoCloud.syncUserFiles( - appInfo = appInfo, - clientId = clientId, - steamInstance = steamInstance, - steamCloud = steamCloud, - preferredSave = preferredSave, - parentScope = parentScope, - prefixToPath = prefixToPath, - onProgress = onProgress, - ).await() - - postSyncInfo?.let { info -> - syncResult = info + try { + var syncResult = PostSyncInfo(SyncResult.UnknownFail) - if (info.syncResult == SyncResult.Success || info.syncResult == SyncResult.UpToDate) { - Timber.i( - "Signaling app launch:\n\tappId: %d\n\tclientId: %s\n\tosType: %s", - appId, - PrefManager.clientId, - EOSType.AndroidUnknown, - ) + PrefManager.clientId?.let { clientId -> + instance?.let { steamInstance -> + getAppInfoOf(appId)?.let { appInfo -> + steamInstance._steamCloud?.let { steamCloud -> + val postSyncInfo = SteamAutoCloud.syncUserFiles( + appInfo = appInfo, + clientId = clientId, + steamInstance = steamInstance, + steamCloud = steamCloud, + preferredSave = preferredSave, + parentScope = parentScope, + prefixToPath = prefixToPath, + onProgress = onProgress, + ).await() - val pendingRemoteOperations = steamCloud.signalAppLaunchIntent( - appId = appId, - clientId = clientId, - machineName = SteamUtils.getMachineName(steamInstance), - ignorePendingOperations = ignorePendingOperations, - osType = EOSType.AndroidUnknown, - ).await() + postSyncInfo?.let { info -> + syncResult = info - if (pendingRemoteOperations.isNotEmpty() && !ignorePendingOperations) { - syncResult = PostSyncInfo( - syncResult = SyncResult.PendingOperations, - pendingRemoteOperations = pendingRemoteOperations, + if (info.syncResult == SyncResult.Success || info.syncResult == SyncResult.UpToDate) { + Timber.i( + "Signaling app launch:\n\tappId: %d\n\tclientId: %s\n\tosType: %s", + appId, + PrefManager.clientId, + EOSType.AndroidUnknown, ) - } else if (ignorePendingOperations && - pendingRemoteOperations.any { - it.operation == ECloudPendingRemoteOperation.k_ECloudPendingRemoteOperationAppSessionActive + + val pendingRemoteOperations = steamCloud.signalAppLaunchIntent( + appId = appId, + clientId = clientId, + machineName = SteamUtils.getMachineName(steamInstance), + ignorePendingOperations = ignorePendingOperations, + osType = EOSType.AndroidUnknown, + ).await() + + if (pendingRemoteOperations.isNotEmpty() && !ignorePendingOperations) { + syncResult = PostSyncInfo( + syncResult = SyncResult.PendingOperations, + pendingRemoteOperations = pendingRemoteOperations, + ) + } else if (ignorePendingOperations && + pendingRemoteOperations.any { + it.operation == ECloudPendingRemoteOperation.k_ECloudPendingRemoteOperationAppSessionActive + } + ) { + steamInstance._steamUser!!.kickPlayingSession() } - ) { - steamInstance._steamUser!!.kickPlayingSession() } } } } } } - } - syncInProgress = false - - return@async syncResult + return@async syncResult + } finally { + releaseSync(appId) + } } suspend fun forceSyncUserFiles( @@ -1990,82 +2014,82 @@ class SteamService : Service(), IChallengeUrlChanged { parentScope: CoroutineScope = CoroutineScope(Dispatchers.IO), overrideLocalChangeNumber: Long? = null, ): Deferred = parentScope.async { - if (syncInProgress) { - Timber.w("Cannot force sync when sync already in progress") + if (!tryAcquireSync(appId)) { + Timber.w("Cannot force sync when sync already in progress for appId=$appId") return@async PostSyncInfo(SyncResult.InProgress) } - syncInProgress = true - - var syncResult = PostSyncInfo(SyncResult.UnknownFail) + try { + var syncResult = PostSyncInfo(SyncResult.UnknownFail) - PrefManager.clientId?.let { clientId -> - instance?.let { steamInstance -> - getAppInfoOf(appId)?.let { appInfo -> - steamInstance._steamCloud?.let { steamCloud -> - val postSyncInfo = SteamAutoCloud.syncUserFiles( - appInfo = appInfo, - clientId = clientId, - steamInstance = steamInstance, - steamCloud = steamCloud, - preferredSave = preferredSave, - parentScope = parentScope, - prefixToPath = prefixToPath, - overrideLocalChangeNumber = overrideLocalChangeNumber, - ).await() + PrefManager.clientId?.let { clientId -> + instance?.let { steamInstance -> + getAppInfoOf(appId)?.let { appInfo -> + steamInstance._steamCloud?.let { steamCloud -> + val postSyncInfo = SteamAutoCloud.syncUserFiles( + appInfo = appInfo, + clientId = clientId, + steamInstance = steamInstance, + steamCloud = steamCloud, + preferredSave = preferredSave, + parentScope = parentScope, + prefixToPath = prefixToPath, + overrideLocalChangeNumber = overrideLocalChangeNumber, + ).await() - postSyncInfo?.let { info -> - syncResult = info - Timber.i("Force cloud sync completed for app $appId with result: ${info.syncResult}") + postSyncInfo?.let { info -> + syncResult = info + Timber.i("Force cloud sync completed for app $appId with result: ${info.syncResult}") + } } } } } - } - - syncInProgress = false - return@async syncResult + return@async syncResult + } finally { + releaseSync(appId) + } } suspend fun closeApp(appId: Int, isOffline: Boolean, prefixToPath: (String) -> String) = withContext(Dispatchers.IO) { async { - if (syncInProgress) { - Timber.w("Cannot close app when sync already in progress") + if (isOffline || !isConnected) { return@async } - if (isOffline || !isConnected) { + if (!tryAcquireSync(appId)) { + Timber.w("Cannot close app when sync already in progress for appId=$appId") return@async } - syncInProgress = true - - PrefManager.clientId?.let { clientId -> - instance?.let { steamInstance -> - getAppInfoOf(appId)?.let { appInfo -> - steamInstance._steamCloud?.let { steamCloud -> - val postSyncInfo = SteamAutoCloud.syncUserFiles( - appInfo = appInfo, - clientId = clientId, - steamInstance = steamInstance, - steamCloud = steamCloud, - parentScope = this, - prefixToPath = prefixToPath, - ).await() + try { + PrefManager.clientId?.let { clientId -> + instance?.let { steamInstance -> + getAppInfoOf(appId)?.let { appInfo -> + steamInstance._steamCloud?.let { steamCloud -> + val postSyncInfo = SteamAutoCloud.syncUserFiles( + appInfo = appInfo, + clientId = clientId, + steamInstance = steamInstance, + steamCloud = steamCloud, + parentScope = this, + prefixToPath = prefixToPath, + ).await() - steamCloud.signalAppExitSyncDone( - appId = appId, - clientId = clientId, - uploadsCompleted = postSyncInfo?.uploadsCompleted == true, - uploadsRequired = postSyncInfo?.uploadsRequired == false, - ) + steamCloud.signalAppExitSyncDone( + appId = appId, + clientId = clientId, + uploadsCompleted = postSyncInfo?.uploadsCompleted == true, + uploadsRequired = postSyncInfo?.uploadsRequired == false, + ) + } } } } + } finally { + releaseSync(appId) } - - syncInProgress = false } } @@ -2385,7 +2409,8 @@ class SteamService : Service(), IChallengeUrlChanged { // Add helper to detect if any downloads or cloud sync are in progress fun hasActiveOperations(): Boolean { - return syncInProgress || downloadJobs.values.any { it.getProgress() < 1f } + val anySyncInProgress = syncInProgressApps.values.any { it.get() } + return anySyncInProgress || downloadJobs.values.any { it.getProgress() < 1f } } // Should service auto-stop when idle (backgrounded)? diff --git a/app/src/main/java/app/gamenative/ui/PluviaMain.kt b/app/src/main/java/app/gamenative/ui/PluviaMain.kt index 0c4aefe02..fdf63ba45 100644 --- a/app/src/main/java/app/gamenative/ui/PluviaMain.kt +++ b/app/src/main/java/app/gamenative/ui/PluviaMain.kt @@ -576,6 +576,29 @@ fun PluviaMain( } } + DialogType.SYNC_IN_PROGRESS -> { + onConfirmClick = { + setMessageDialogState(MessageDialogState(false)) + preLaunchApp( + context = context, + appId = state.launchedAppId, + skipCloudSync = true, + setLoadingDialogVisible = viewModel::setLoadingDialogVisible, + setLoadingProgress = viewModel::setLoadingDialogProgress, + setLoadingMessage = viewModel::setLoadingDialogMessage, + setMessageDialogState = setMessageDialogState, + onSuccess = viewModel::launchApp, + isOffline = viewModel.isOffline.value, + ) + } + onDismissClick = { + setMessageDialogState(MessageDialogState(false)) + } + onDismissRequest = { + setMessageDialogState(MessageDialogState(false)) + } + } + DialogType.PENDING_UPLOAD_IN_PROGRESS -> { onDismissClick = { setMessageDialogState(MessageDialogState(false)) @@ -1008,6 +1031,7 @@ fun preLaunchApp( ignorePendingOperations: Boolean = false, preferredSave: SaveLocation = SaveLocation.None, useTemporaryOverride: Boolean = false, + skipCloudSync: Boolean = false, setLoadingDialogVisible: (Boolean) -> Unit, setLoadingProgress: (Float) -> Unit, setLoadingMessage: (String) -> Unit, @@ -1187,6 +1211,13 @@ fun preLaunchApp( return@launch } + if (skipCloudSync) { + Timber.tag("preLaunchApp").w("Skipping Steam Cloud sync for $appId by user request") + setLoadingDialogVisible(false) + onSuccess(context, appId) + return@launch + } + // For Steam games, sync save files and check no pending remote operations are running val prefixToPath: (String) -> String = { prefix -> PathType.from(prefix).toAbsPath(context, gameId, SteamService.userSteamId!!.accountID) @@ -1245,18 +1276,14 @@ fun preLaunchApp( retryCount = retryCount + 1, ) } else { - val message = if (useTemporaryOverride) { - context.getString(R.string.main_sync_in_progress_retry) - } else { - context.getString(R.string.main_sync_in_progress) - } setMessageDialogState( MessageDialogState( visible = true, - type = DialogType.SYNC_FAIL, + type = DialogType.SYNC_IN_PROGRESS, title = context.getString(R.string.sync_error_title), - message = message, - dismissBtnText = context.getString(R.string.ok), + message = context.getString(R.string.main_sync_in_progress_launch_anyway_message), + confirmBtnText = context.getString(R.string.main_launch_anyway), + dismissBtnText = context.getString(R.string.main_wait), ), ) } diff --git a/app/src/main/java/app/gamenative/ui/enums/DialogType.kt b/app/src/main/java/app/gamenative/ui/enums/DialogType.kt index 6d74cffd8..1b783b3da 100644 --- a/app/src/main/java/app/gamenative/ui/enums/DialogType.kt +++ b/app/src/main/java/app/gamenative/ui/enums/DialogType.kt @@ -12,6 +12,7 @@ enum class DialogType(val icon: ImageVector? = null) { DISCORD, SYNC_CONFLICT, SYNC_FAIL, + SYNC_IN_PROGRESS, MULTIPLE_PENDING_OPERATIONS, PENDING_OPERATION_NONE, PENDING_UPLOAD, diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index a468d7161..06f475077 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -575,6 +575,9 @@ Synkroniseringsoperationen tager for lang tid. Prøv venligst at starte spillet igen om et øjeblik. Synkronisering er i gang. Prøv venligst igen om et øjeblik. + Cloud-synkronisering for dette spil er allerede i gang. Du kan vente og prøve igen eller starte alligevel uden synkronisering (kan give konflikter i gemte filer). + Start alligevel + Vent Kunne ikke synkronisere gemfiler: %s. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8ca0355ed..6aa267ef2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -690,6 +690,9 @@ Die Synchronisierung dauert zu lange. Bitte versuche, das Spiel später erneut zu starten. Synchronisierung läuft. Bitte versuche es später noch einmal. + Die Cloud-Synchronisierung für dieses Spiel läuft bereits. Du kannst warten und es später erneut versuchen oder trotzdem starten ohne zu synchronisieren (kann Speicherstand-Konflikte verursachen). + Trotzdem starten + Warten Speicherstände konnten nicht synchronisiert werden: %s. Upload läuft diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 08180c422..4c864f25a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -742,6 +742,9 @@ L\'opération de synchronisation prend trop de temps. Veuillez réessayer de lancer le jeu dans un moment. La synchronisation est en cours. Veuillez réessayer dans un moment. + La synchronisation cloud des sauvegardes pour ce jeu est déjà en cours. Vous pouvez attendre et réessayer, ou lancer quand même sans synchroniser (peut causer des conflits de sauvegarde). + Lancer quand même + Attendre Échec de la synchronisation des fichiers de sauvegarde : %s. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1a17b2ddb..e23a79ee3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -746,6 +746,9 @@ L\'operazione di sincronizzazione sta impiegando troppo tempo. Prova ad avviare nuovamente il gioco tra un momento. La sincronizzazione è attualmente in corso. Riprova tra un momento. + La sincronizzazione dei salvataggi cloud per questo gioco è già in corso. Puoi attendere e riprovare, oppure avviare comunque senza sincronizzare (può causare conflitti di salvataggio). + Avvia comunque + Attendi Impossibile sincronizzare i file di salvataggio: %s. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index afa398a91..38278b708 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -575,6 +575,9 @@ A operação de sincronização está demorando muito. Por favor, tente iniciar o jogo novamente em um momento. A sincronização está em andamento. Por favor, tente novamente em um momento. + A sincronização na nuvem deste jogo já está em andamento. Você pode esperar e tentar novamente ou iniciar mesmo assim sem sincronizar (pode causar conflitos de salvamento). + Iniciar mesmo assim + Esperar Falha ao sincronizar arquivos de save: %s. diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index afcb7531b..f1711bd4a 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -746,6 +746,9 @@ Operația de sincronizare durează prea mult. Încearcă să lansezi jocul din nou peste puțin timp. Sincronizarea este în desfășurare. Încearcă din nou peste puțin timp. + Sincronizarea în cloud pentru acest joc este deja în curs. Poți aștepta și încerca din nou sau poți porni oricum fără sincronizare (poate cauza conflicte de salvare). + Pornește oricum + Așteaptă Sincronizarea fișierelor de salvare a eșuat: %s. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b2b45ebb4..4f6187fba 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -746,6 +746,9 @@ Операція синхронізації триває занадто довго. Спробуйте запустити гру ще раз через деякий час. Наразі триває синхронізація. Спробуйте ще раз за мить. + Синхронізація хмарних збережень для цієї гри вже триває. Ви можете зачекати й спробувати знову або запустити без синхронізації (може спричинити конфлікти збережень). + Запустити попри це + Зачекати Помилка синхронізації файлів збереження: %s. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ac00393e7..a88afa196 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -736,6 +736,9 @@ 同步操作耗时过久,请稍后重新启动游戏 同步目前正在进行中,请稍后再试 + 该游戏的云存档同步正在进行。你可以等待后重试,或继续启动但不进行同步(可能导致存档冲突)。 + 仍然启动 + 等待 同步存档失败:%s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fe7d8d344..d304528f0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -740,6 +740,9 @@ 同步操作耗時過久, 請稍後重新啟動遊戲 同步目前正在進行中, 請稍後再試 + 此遊戲的雲端存檔同步正在進行。你可以稍後再試,或繼續啟動但不進行同步(可能造成存檔衝突)。 + 仍然啟動 + 等待 同步存檔案失敗:%s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b6c03a96..6439b9072 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -746,6 +746,9 @@ Sync operation is taking too long. Please try launching the game again in a moment. Sync is currently in progress. Please try again in a moment. + Cloud save sync for this game is already running. You can wait and try again, or launch anyway without syncing (may cause save conflicts). + Launch anyway + Wait Failed to sync save files: %s.