diff --git a/app/src/main/java/app/gamenative/ui/PluviaMain.kt b/app/src/main/java/app/gamenative/ui/PluviaMain.kt index 4dfb47223..fb3e198fe 100644 --- a/app/src/main/java/app/gamenative/ui/PluviaMain.kt +++ b/app/src/main/java/app/gamenative/ui/PluviaMain.kt @@ -1091,13 +1091,13 @@ fun preLaunchApp( } } } - if (!container.isUseLegacyDRM && !container.isLaunchRealSteam && !SteamService.isFileInstallable(context, "experimental-drm-20260101.tzst")) { + if (!container.isUseLegacyDRM && !container.isLaunchRealSteam && !SteamService.isFileInstallable(context, "experimental-drm-20260116.tzst")) { setLoadingMessage("Downloading extras") SteamService.downloadFile( onDownloadProgress = { setLoadingProgress(it / 1.0f) }, this, context = context, - "experimental-drm-20260101.tzst" + "experimental-drm-20260116.tzst" ).await() } if (container.isLaunchRealSteam && !SteamService.isFileInstallable(context, "steam.tzst")) { 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 c2696d748..6925dba65 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 @@ -1914,7 +1914,7 @@ private fun filterExecutablesForSteamless(executables: List): List Unit, navigateBack: () -> Unit) { Timber.i("Exit called") - + // Prevent duplicate PostHog events when multiple exit triggers fire simultaneously if (isExiting.compareAndSet(false, true)) { PostHog.capture( @@ -2257,77 +2257,85 @@ private fun unpackExecutableFile( output = StringBuilder() - // Scan all executables from A: drive and filter them - val allExecutables = ContainerUtils.scanExecutablesInADrive(container.drives) - Timber.i("Found ${allExecutables.size} executables in A: drive") + if (!container.isLaunchRealSteam && container.isUseLegacyDRM) { + // Scan all executables from A: drive and filter them + val allExecutables = ContainerUtils.scanExecutablesInADrive(container.drives) + Timber.i("Found ${allExecutables.size} executables in A: drive") - val filteredExecutables = filterExecutablesForSteamless(allExecutables) - Timber.i("Filtered to ${filteredExecutables.size} executables for Steamless processing") + val filteredExecutables = filterExecutablesForSteamless(allExecutables) + Timber.i("Filtered to ${filteredExecutables.size} executables for Steamless processing") - if (filteredExecutables.isEmpty()) { - Timber.w("No executables to process with Steamless") - } else { - PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM...")) + if (filteredExecutables.isEmpty()) { + Timber.w("No executables to process with Steamless") + } else { + PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM...")) - // Process each executable individually to handle errors per file - filteredExecutables.forEachIndexed { index, exePath -> - var batchFile: File? = null - try { - val normalizedPath = exePath.replace('/', '\\') - val windowsPath = "A:\\$normalizedPath" + // Process each executable individually to handle errors per file + filteredExecutables.forEachIndexed { index, exePath -> + var batchFile: File? = null + try { + val normalizedPath = exePath.replace('/', '\\') + val windowsPath = "A:\\$normalizedPath" - PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM... (${index + 1}/${filteredExecutables.size})")) + PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM... (${index + 1}/${filteredExecutables.size})")) - // Create a batch file that Wine can execute, to handle paths with spaces in them - batchFile = File(imageFs.getRootDir(), "tmp/steamless_wrapper_${index}.bat") - batchFile.parentFile?.mkdirs() - batchFile.writeText("@echo off\r\nz:\\Steamless\\Steamless.CLI.exe \"$windowsPath\"\r\n") + // Create a batch file that Wine can execute, to handle paths with spaces in them + batchFile = File(imageFs.getRootDir(), "tmp/steamless_wrapper_${index}.bat") + batchFile.parentFile?.mkdirs() + batchFile.writeText("@echo off\r\nz:\\Steamless\\Steamless.CLI.exe \"$windowsPath\"\r\n") - val slCmd = "wine z:\\tmp\\steamless_wrapper_${index}.bat" - val slOutput = guestProgramLauncherComponent.execShellCommand(slCmd) - output.append(slOutput) - } catch (e: Exception) { - Timber.e(e, "Error running Steamless on $exePath, continuing with next file") - output.append("Error processing $exePath: ${e.message}\n") - } finally { - // Clean up batch file - batchFile?.delete() + val slCmd = "wine z:\\tmp\\steamless_wrapper_${index}.bat" + val slOutput = guestProgramLauncherComponent.execShellCommand(slCmd) + output.append(slOutput) + } catch (e: Exception) { + Timber.e(e, "Error running Steamless on $exePath, continuing with next file") + output.append("Error processing $exePath: ${e.message}\n") + } finally { + // Clean up batch file + batchFile?.delete() + } } - } - - Timber.i("Finished processing ${filteredExecutables.size} executables. Result: $output") - - // Process file moving for all filtered executables - for (exePath in filteredExecutables) { - try { - // Paths from scanExecutablesInADrive use forward slashes (Unix format from URI) - // Use as-is for File operations (forward slashes work on Unix/Android) - val unixPath = exePath.replace('\\', '/') - val exe = File(imageFs.wineprefix + "/dosdevices/a:/" + unixPath) - val unpackedExe = File( - imageFs.wineprefix + "/dosdevices/a:/" + unixPath + ".unpacked.exe", - ) - val originalExe = File( - imageFs.wineprefix + "/dosdevices/a:/" + unixPath + ".original.exe", - ) - // For logging, show Windows format - val windowsPath = "A:\\${exePath.replace('/', '\\')}" + Timber.i("Finished processing ${filteredExecutables.size} executables. Result: $output") - Timber.i("Moving files for $windowsPath") - if (exe.exists() && unpackedExe.exists()) { - Files.copy(exe.toPath(), originalExe.toPath(), REPLACE_EXISTING) - Files.copy(unpackedExe.toPath(), exe.toPath(), REPLACE_EXISTING) - Timber.i("Successfully moved files for $windowsPath") - } else { - val errorMsg = - "Either original exe or unpacked exe does not exist for $windowsPath. Original: ${exe.exists()}, Unpacked: ${unpackedExe.exists()}" - Timber.w(errorMsg) + // Process file moving for all filtered executables + for (exePath in filteredExecutables) { + try { + // Paths from scanExecutablesInADrive use forward slashes (Unix format from URI) + // Use as-is for File operations (forward slashes work on Unix/Android) + val unixPath = exePath.replace('\\', '/') + val exe = File(imageFs.wineprefix + "/dosdevices/a:/" + unixPath) + val unpackedExe = File( + imageFs.wineprefix + "/dosdevices/a:/" + unixPath + ".unpacked.exe", + ) + val originalExe = File( + imageFs.wineprefix + "/dosdevices/a:/" + unixPath + ".original.exe", + ) + + // For logging, show Windows format + val windowsPath = "A:\\${exePath.replace('/', '\\')}" + + Timber.i("Moving files for $windowsPath") + if (exe.exists() && unpackedExe.exists()) { + if (originalExe.exists()) { + Timber.i("Original backup exists for $windowsPath; skipping overwrite") + } else { + Files.copy(exe.toPath(), originalExe.toPath(), REPLACE_EXISTING) + } + Files.copy(unpackedExe.toPath(), exe.toPath(), REPLACE_EXISTING) + Timber.i("Successfully moved files for $windowsPath") + } else { + val errorMsg = + "Either exe or unpacked exe does not exist for $windowsPath. Exe: ${exe.exists()}, Unpacked: ${unpackedExe.exists()}" + Timber.w(errorMsg) + } + } catch (e: Exception) { + Timber.e(e, "Error moving files for $exePath, continuing with next executable") } - } catch (e: Exception) { - Timber.e(e, "Error moving files for $exePath, continuing with next executable") } } + } else { + Timber.i("Skipping Steamless (launchRealSteam=${container.isLaunchRealSteam}, useLegacyDRM=${container.isUseLegacyDRM})") } output = StringBuilder() diff --git a/app/src/main/java/app/gamenative/utils/SteamUtils.kt b/app/src/main/java/app/gamenative/utils/SteamUtils.kt index 5a0591d1e..698465084 100644 --- a/app/src/main/java/app/gamenative/utils/SteamUtils.kt +++ b/app/src/main/java/app/gamenative/utils/SteamUtils.kt @@ -242,14 +242,14 @@ object SteamUtils { backupSteamclientFiles(context, steamAppId) val imageFs = ImageFs.find(context) - val downloaded = File(imageFs.getFilesDir(), "experimental-drm-20260101.tzst") + val downloaded = File(imageFs.getFilesDir(), "experimental-drm-20260116.tzst") TarCompressorUtils.extract( TarCompressorUtils.Type.ZSTD, downloaded, imageFs.getRootDir(), ) putBackSteamDlls(appDirPath) - restoreUnpackedExecutable(context, steamAppId) + restoreOriginalExecutable(context, steamAppId) // Get ticket and pass to ensureSteamSettings val ticketBase64 = SteamService.instance?.getEncryptedAppTicketBase64(steamAppId) @@ -336,6 +336,7 @@ object SteamUtils { [Injection] IgnoreLoaderArchDifference=1 + DllsToInjectFolder=extra_dlls """.trimIndent(), ) } @@ -756,28 +757,28 @@ object SteamUtils { val imageFs = ImageFs.find(context) val dosDevicesPath = File(imageFs.wineprefix, "dosdevices/a:") - dosDevicesPath.walkTopDown().maxDepth(10).firstOrNull { - it.isFile && it.name.endsWith(".original.exe", ignoreCase = true) - }?.let { file -> - try { - val origPath = file.toPath() - val originalPath = origPath.parent.resolve(origPath.name.removeSuffix(".original.exe")) - Timber.i("Found ${origPath.name} at ${origPath.absolutePathString()}, restoring...") + dosDevicesPath.walkTopDown().maxDepth(10) + .filter { it.isFile && it.name.endsWith(".original.exe", ignoreCase = true) } + .forEach { file -> + try { + val origPath = file.toPath() + val originalPath = origPath.parent.resolve(origPath.name.removeSuffix(".original.exe")) + Timber.i("Found ${origPath.name} at ${origPath.absolutePathString()}, restoring...") - // Delete the current exe if it exists - if (Files.exists(originalPath)) { - Files.delete(originalPath) - } + // Delete the current exe if it exists + if (Files.exists(originalPath)) { + Files.delete(originalPath) + } - // Copy the backup back to the original location - Files.copy(origPath, originalPath) + // Copy the backup back to the original location + Files.copy(origPath, originalPath) - Timber.i("Restored ${originalPath.fileName} from backup") - restoredCount++ - } catch (e: IOException) { - Timber.w(e, "Failed to restore ${file.name} from backup") + Timber.i("Restored ${originalPath.fileName} from backup") + restoredCount++ + } catch (e: IOException) { + Timber.w(e, "Failed to restore ${file.name} from backup") + } } - } Timber.i("Finished restoreOriginalExecutable for appId: $steamAppId. Restored $restoredCount executable(s)") } diff --git a/app/src/test/java/app/gamenative/utils/SteamUtilsFileSearchTest.kt b/app/src/test/java/app/gamenative/utils/SteamUtilsFileSearchTest.kt index 75a36a918..5605f4116 100644 --- a/app/src/test/java/app/gamenative/utils/SteamUtilsFileSearchTest.kt +++ b/app/src/test/java/app/gamenative/utils/SteamUtilsFileSearchTest.kt @@ -225,9 +225,13 @@ class SteamUtilsFileSearchTest { val dosDevicesPath = File(imageFs.wineprefix, "dosdevices/a:") dosDevicesPath.mkdirs() - // Create .original.exe file + // Create multiple .original.exe files in different folders val origExeFile = File(dosDevicesPath, "game.exe.original.exe") origExeFile.writeBytes("original exe content".toByteArray()) + val nestedDir = File(dosDevicesPath, "bin") + nestedDir.mkdirs() + val origExeFile2 = File(nestedDir, "game2.exe.original.exe") + origExeFile2.writeBytes("original exe content 2".toByteArray()) // Call the actual function SteamUtils.restoreOriginalExecutable(context, steamAppId) @@ -237,6 +241,10 @@ class SteamUtilsFileSearchTest { assertTrue("Should restore exe to original location", restoredFile.exists()) assertEquals("Restored content should match backup", "original exe content", restoredFile.readText()) + val restoredFile2 = File(nestedDir, "game2.exe") + assertTrue("Should restore exe to original location in subdirectory", restoredFile2.exists()) + assertEquals("Restored content should match backup for second exe", + "original exe content 2", restoredFile2.readText()) } @Test @@ -645,7 +653,7 @@ class SteamUtilsFileSearchTest { // Verify game.exe is NOT overwritten after first replaceSteamClientDll call assertEquals("game.exe should be overwritten after replaceSteamClientDll", - "unpacked exe content", gameExe.readText()) + "original exe content", gameExe.readText()) // Verify marker was set assertTrue("Should add STEAM_COLDCLIENT_USED marker", @@ -762,8 +770,8 @@ class SteamUtilsFileSearchTest { steamAppId.toString(), steamAppIdFile.readText().trim()) // Verify game.exe is NOT overwritten after second replaceSteamClientDll call - assertEquals("game.exe should be overwritten after second replaceSteamClientDll", - "unpacked exe content", gameExe.readText()) + assertEquals("game.exe should not be overwritten after second replaceSteamClientDll", + "original exe content", gameExe.readText()) // Verify marker was set assertTrue("Should add STEAM_COLDCLIENT_USED marker", @@ -871,7 +879,7 @@ class SteamUtilsFileSearchTest { // Verify game.exe is NOT overwritten after first replaceSteamClientDll call assertEquals("game.exe should not be overwritten after replaceSteamClientDll", - "unpacked exe content", gameExe.readText()) + "original exe content", gameExe.readText()) // Verify marker was set assertTrue("Should add STEAM_COLDCLIENT_USED marker", @@ -917,8 +925,8 @@ class SteamUtilsFileSearchTest { SteamUtils.replaceSteamclientDll(context, testAppId) // Verify restoreUnpackedExecutable overwrites game.exe with game.exe.unpacked.exe content - assertEquals("game.exe should be overwritten with game.exe.unpacked.exe content after second replaceSteamClientDll", - "unpacked exe content", gameExe.readText()) + assertEquals("game.exe should be overwritten with game.exe.original.exe content after second replaceSteamClientDll", + "original exe content", gameExe.readText()) // Verify steam_settings folder still exists next to steamclient.dll in Steam directory assertTrue("steam_settings folder should still exist in Steam directory",