diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt index 0c3481e58..3ffbee55f 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt @@ -186,6 +186,9 @@ object AddedGames { private fun entryFolders(context: Context): Set = AddedExes.list(context).map { it.folder }.toSet() + private fun libraryDepotInstalls(context: Context): List = + SteamLibraries.list(context).flatMap { SteamLibraries.depotInstalls(File(it.path, "steamapps/common")) } + internal fun isSteamLibraryFolder(name: String): Boolean = name.equals("steamapps", ignoreCase = true) || name == com.droiddeck.launcher.session.GameStorage.X64_CARD_DIR @@ -213,6 +216,7 @@ object AddedGames { if (folder.path in removed || !folder.isDirectory) continue scanGame(context, folder, out, single = entry) } + for (folder in libraryDepotInstalls(context)) if (folder.path !in removed) scanGame(context, folder, out) for (folder in StoreInstallRoot.gameFolders(context)) if (StoreInstallRoot.isFinished(folder)) scanGame(context, folder, out) for (game in com.droiddeck.launcher.runtime.BattleNet.games(context)) { if (game.folder.path !in removed) com.droiddeck.launcher.runtime.BattleNet.shortcut(context, game)?.let(out::add) @@ -259,6 +263,7 @@ object AddedGames { entries.map { File(it.folder) }.filter(::owns).maxByOrNull { canonical(it).length }?.let { return it } SessionPrefs.removedAddedGames(context).map { File(it) }.filter { owns(it) && groupGames(it) == null }.maxByOrNull { canonical(it).length }?.let { return it } StoreInstallRoot.gameFolders(context).firstOrNull(::holds)?.let { return it } + libraryDepotInstalls(context).firstOrNull(::holds)?.let { return it } val removed = SessionPrefs.removedAddedGames(context) val dirs = listOfNotNull( GameStorage.effective(context)?.let { File(it.path, "steamapps/common") }, @@ -376,6 +381,7 @@ object AddedGames { } AddedExes.list(context).filter { it.folder !in removed }.forEach { out.add(canonical(File(it.folder))) } StoreInstallRoot.gameFolders(context).forEach { out.add(canonical(it)) } + libraryDepotInstalls(context).filter { it.path !in removed }.forEach { out.add(canonical(it)) } return out } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamLibraries.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamLibraries.kt index 06f63c32b..789e4a2fc 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamLibraries.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamLibraries.kt @@ -126,8 +126,20 @@ object SteamLibraries { return out } - internal fun gameCount(library: File): Int = - File(library, "steamapps").list()?.count { it.startsWith("appmanifest_") && it.endsWith(".acf") } ?: 0 + internal fun gameCount(library: File): Int { + val steamapps = File(library, "steamapps") + val manifests = steamapps.list()?.count { it.startsWith("appmanifest_") && it.endsWith(".acf") } ?: return 0 + return manifests + depotInstalls(File(steamapps, "common")).size + } + + /** Finished downloads another client left without a Steam manifest; the importer registers them from depot.config. */ + fun depotInstalls(common: File): List { + val managed = AddedGames.steamInstallDirs(common) + return common.listFiles { f -> + f.isDirectory && f.name.lowercase() !in managed && + File(f, ".download_complete").isFile && File(f, ".DepotDownloader/depot.config").isFile + }.orEmpty().sortedBy { it.name.lowercase() } + } fun rename(context: Context, id: String, label: String) { val name = clean(label).takeIf { it.isNotEmpty() } ?: return diff --git a/app/src/test/java/com/droiddeck/launcher/session/SteamLibrariesTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SteamLibrariesTest.kt index 1fa5a5aaf..ef8cb3e91 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SteamLibrariesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SteamLibrariesTest.kt @@ -89,6 +89,36 @@ class SteamLibrariesTest { assertEquals(setOf(shallow, deep), withGames.toSet()) } + private fun depotInstall(common: File, name: String, complete: Boolean = true): File = File(common, name).apply { + File(this, ".DepotDownloader").mkdirs() + File(this, ".DepotDownloader/depot.config").writeText("""{"installedManifestIDs":{"43":99}}""") + File(this, "$name.exe").writeBytes(ByteArray(4)) + if (complete) File(this, ".download_complete").writeText("") + } + + @Test fun finishedDownloadsWithoutManifestsCountAsGames() { + val steam = File(tmp.newFolder("card"), "Other/Steam") + val common = File(steam, "steamapps/common").apply { mkdirs() } + assertEquals(0, SteamLibraries.gameCount(steam)) + val done = depotInstall(common, "Done") + depotInstall(common, "Partial", complete = false) + depotInstall(common, "Managed") + File(steam, "steamapps/appmanifest_42.acf").writeText("\"AppState\"\n{\n\t\"installdir\"\t\t\"Managed\"\n}\n") + assertEquals(listOf(done), SteamLibraries.depotInstalls(common)) + assertEquals(2, SteamLibraries.gameCount(steam)) + assertEquals(steam, SteamLibraries.innerLibrary(steam.parentFile)) + } + + @Test fun addedLibrariesImportFinishedDownloadsWithoutManifests() { + SessionPrefs.setGameStorage(app, SessionPrefs.GAME_STORAGE_OFF, "") + val steam = tmp.newFolder("Steam") + val game = depotInstall(File(steam, "steamapps/common"), "Game") + val lib = added(steam.path) + val scanned = AddedGames.scan(app).single() + assertEquals(game, scanned.folder) + assertEquals("${lib.guest}/steamapps/common/Game", AddedGames.guestPath(app, scanned.folder)) + } + @Test fun unreadableOrInvalidEntriesAreDropped() { assertTrue(SteamLibraries.parse("{oops").isEmpty()) assertEquals(listOf("2"), SteamLibraries.parse("""[{"id":"../x","path":"/a"},{"id":"2","path":"/b","label":""}]""").map { it.id })