diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/BattleNet.kt b/app/src/main/java/com/droiddeck/launcher/runtime/BattleNet.kt index f753585b1..d1d54c0db 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/BattleNet.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/BattleNet.kt @@ -161,8 +161,10 @@ object BattleNet { fun games(context: Context): List { val pfx = File(prefix(context), "pfx") - val entries = runCatching { File(pfx, "system.reg").useLines { uninstallEntries(it) } }.getOrDefault(emptyList()) + - runCatching { productDbEntries(File(pfx, "drive_c/ProgramData/Battle.net/Agent/product.db").readBytes()) }.getOrDefault(emptyList()) + val entries = installs( + runCatching { File(pfx, "system.reg").useLines { uninstallEntries(it) } }.getOrDefault(emptyList()), + runCatching { productDbEntries(File(pfx, "drive_c/ProgramData/Battle.net/Agent/product.db").readBytes()) }.getOrNull(), + ) val found = LinkedHashMap() for ((uid, path) in entries) { val title = title(uid) @@ -177,6 +179,12 @@ object BattleNet { return found.values.toList() } + internal fun installs(registry: List>, agent: List>?): List> { + if (agent.isNullOrEmpty()) return registry + val tracked = agent.map { it.first.lowercase() }.toSet() + return registry.filter { it.first.lowercase() in tracked } + agent + } + internal fun hostFolder(context: Context, path: String): File? { val drive = Regex("""^([CcZz]):[\\/](.*)$""").matchEntire(path.trim()) ?: return null val relative = drive.groupValues[2].replace('\\', '/').trim('/') diff --git a/app/src/main/java/com/droiddeck/launcher/stores/battlenet/BattleNetInstall.kt b/app/src/main/java/com/droiddeck/launcher/stores/battlenet/BattleNetInstall.kt index d73e77d13..735471cc8 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/battlenet/BattleNetInstall.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/battlenet/BattleNetInstall.kt @@ -34,7 +34,6 @@ object BattleNetInstall { internal const val STATE = "BNET_STATE " internal const val DONE = "BNET_DONE " internal const val ERROR = "BNET_ERROR " - private const val LIBRARY_GUEST = "/mnt/droiddeck-sd" private const val VERSIONS = "https://%s.version.battle.net/v2/products/%s/versions" private const val BUILD_INFO = ".build.info" private const val CHECK_AGAIN_MS = 30 * 60_000L @@ -70,22 +69,28 @@ object BattleNetInstall { return if (two.matches(Regex("[A-Z]{2}")) && three.matches(Regex("[A-Z]{3}"))) three to two else "USA" to "US" } - internal fun binds(context: Context, guest: String): List { - val out = ArrayList() - if (guest.startsWith(StoreInstallRoot.GUEST_EXTERNAL + "/")) out += StoreInstallRoot.externalBindSpecs(context) - if (guest == LIBRARY_GUEST || guest.startsWith("$LIBRARY_GUEST/")) { - GameStorage.effective(context)?.let { library -> - File(LinuxRuntime.rootDir(context), LIBRARY_GUEST.removePrefix("/")).mkdirs() - out += SecondaryLibrary.contentPath(File(library.path)).path + ":" + LIBRARY_GUEST + internal fun binds(context: Context): List { + val out = ArrayList(StoreInstallRoot.externalBindSpecs(context)) + val root = LinuxRuntime.rootDir(context) + GameStorage.effective(context)?.let { library -> + val content = SecondaryLibrary.contentPath(File(library.path)).path + for (guest in SecondaryLibrary.CARD_GUESTS) { + File(root, guest.removePrefix("/")).mkdirs() + out += "$content:$guest" } } + for (folder in AddedGames.roots(context)) { + if (!folder.host.isDirectory || !folder.host.canRead()) continue + File(root, folder.guest.removePrefix("/")).mkdirs() + out += folder.host.path + ":" + folder.guest + } return out } - internal fun run(context: Context, args: List, guest: String?, cancelled: AtomicBoolean?, onLine: (String) -> Unit): Int = + internal fun run(context: Context, args: List, cancelled: AtomicBoolean?, onLine: (String) -> Unit): Int = GuestCommand.run( context, listOf(BattleNet.PROGRAM) + args, logName = "battlenet-agent", - extraBinds = guest?.let { binds(context, it) }, fastPath = true, cancelled = cancelled, onLine = onLine, + extraBinds = binds(context), fastPath = true, cancelled = cancelled, onLine = onLine, ) fun enqueue(context: Context, item: CatalogItem, folder: File, update: Boolean = false) { @@ -116,12 +121,12 @@ object BattleNetInstall { fun uninstall(context: Context, game: InstalledStoreGame): Boolean { if (product(game.sidecar.id) == null || !BattleNet.installed(context)) return false - val done = agentUninstall(context, installUid(context, game), game.folder) + val done = agentUninstall(context, installUid(context, game)) if (done) StoresState.post { StoresState.notifyLibraryChanged(context.applicationContext) } return done } - internal fun agentUninstall(context: Context, uid: String, folder: File): Boolean { + internal fun agentUninstall(context: Context, uid: String): Boolean { val stop = AtomicBoolean(false) val started = AtomicBoolean(false) val done = AtomicBoolean(false) @@ -133,7 +138,7 @@ object BattleNetInstall { } }, "battlenet-uninstall-wait").apply { isDaemon = true; start() } val code = runCatching { - run(context, listOf("agent-uninstall", uid), AddedGames.guestPath(context, folder), stop) { line -> + run(context, listOf("agent-uninstall", uid), stop) { line -> if (line == STATE + "starting") started.set(true) if (line.startsWith(DONE)) done.set(true) } @@ -263,7 +268,7 @@ internal class BattleNetInstaller( else listOf("agent-install", uid, product, guest, BattleNetInstall.localeTag(app), country, region) var done: JSONObject? = null var reason: String? = null - val code = BattleNetInstall.run(app, args, guest, cancelled) { line -> + val code = BattleNetInstall.run(app, args, cancelled) { line -> when { line.startsWith(BattleNetInstall.PROGRESS) -> runCatching { JSONObject(line.removePrefix(BattleNetInstall.PROGRESS)) }.getOrNull()?.let { progress(handle, it) } @@ -304,7 +309,7 @@ internal class BattleNetInstaller( val uid = item.extra[BattleNetInstall.INSTALL_UID] ?: BattleNet.title(item.id)?.uid ?: return Thread({ if (started.get() && !ended.await(END_WAIT_S, TimeUnit.SECONDS)) Log.w("BattleNetInstall", "the cancelled install of $uid is still stopping") - val gone = BattleNetInstall.agentUninstall(app, uid, folder) + val gone = BattleNetInstall.agentUninstall(app, uid) if (folder.isDirectory && (gone || folder.list().isNullOrEmpty())) com.droiddeck.launcher.core.FileUtils.delete(folder) Log.i("BattleNetInstall", "cancelled install of $uid: ${if (gone) "removed" else "left for Battle.net to remove"}") }, "battlenet-cancel").start() diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/BattleNetTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/BattleNetTest.kt index d23a2eac2..6a4e84cb7 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/BattleNetTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/BattleNetTest.kt @@ -216,6 +216,13 @@ class BattleNetTest { assertEquals(emptyList>(), BattleNet.productDbEntries(byteArrayOf(0x0a, 0x7f, 0x01))) } + @Test fun aGameTheAgentNoLongerTracksIsNotInstalled() { + val registry = listOf("wow" to "Z:\\mnt\\droiddeck-sd\\Games\\Battle.net\\World of Warcraft", "hs_beta" to "C:\\Hearthstone") + assertEquals(listOf("hs_beta" to "C:\\Hearthstone", "HS_BETA" to "C:/Hearthstone"), BattleNet.installs(registry, listOf("HS_BETA" to "C:/Hearthstone"))) + assertEquals(registry, BattleNet.installs(registry, null)) + assertEquals(registry, BattleNet.installs(registry, emptyList())) + } + @Test fun theGamesAreTheTitlesInstalledInTheirFolders() { val pfx = File(BattleNet.prefix(context), "pfx").apply { mkdirs() } File(pfx, "system.reg").writeText(REGISTRY) @@ -229,9 +236,11 @@ class BattleNetTest { File(pfx, "drive_c/$folder").mkdirs() } val games = BattleNet.games(context) - assertEquals(listOf("Fen", "OSI", "WTCG", "WoWC"), games.map { it.code }) - assertEquals(listOf("Diablo IV", "Diablo II: Resurrected", "Hearthstone", "World of Warcraft Classic"), games.map { it.name }) - assertEquals(File(pfx, "drive_c/Games/Diablo II Resurrected"), games[1].folder) + assertEquals(listOf("Fen", "WTCG", "WoWC"), games.map { it.code }) + assertEquals(listOf("Diablo IV", "Hearthstone", "World of Warcraft Classic"), games.map { it.name }) + assertEquals(File(pfx, "drive_c/Program Files (x86)/Hearthstone"), games[1].folder) + File(pfx, "drive_c/ProgramData/Battle.net/Agent/product.db").delete() + assertEquals(listOf("Fen", "OSI"), BattleNet.games(context).map { it.code }) assertNull(BattleNet.hostFolder(context, "D:\\Games\\X")) assertNull(BattleNet.hostFolder(context, "C:\\..\\..\\etc")) assertNull(BattleNet.hostFolder(context, "Z:\\root\\..\\..\\etc"))