fix: find Steam libraries whose installs have no appmanifest

This commit is contained in:
Kurt Himebauch committed 2026-11-03 14:39:45 -05:00
1 parent 9f6c73442b
commit be5f1560b2
3 files changed
+50 -2

No files matched your search

@@ -186,6 +186,9 @@ object AddedGames {
private fun entryFolders(context: Context): Set<String> = AddedExes.list(context).map { it.folder }.toSet()
private fun libraryDepotInstalls(context: Context): List<File> =
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
}
@@ -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<File> {
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
@@ -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 })