fix: keep every game library mounted for Battle.net's Agent and drop games it no longer tracks

This commit is contained in:
maxjivi05 authored and MaxsTechReview committed 2026-10-26 10:30:16 -04:00
1 parent fd9de4a21e
commit be5bec2d44
3 files changed
+42 -20

No files matched your search

@@ -161,8 +161,10 @@ object BattleNet {
fun games(context: Context): List<Game> {
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<String, Game>()
for ((uid, path) in entries) {
val title = title(uid)
@@ -177,6 +179,12 @@ object BattleNet {
return found.values.toList()
}
internal fun installs(registry: List<Pair<String, String>>, agent: List<Pair<String, String>>?): List<Pair<String, String>> {
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('/')
@@ -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<String> {
val out = ArrayList<String>()
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<String> {
val out = ArrayList<String>(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<String>, guest: String?, cancelled: AtomicBoolean?, onLine: (String) -> Unit): Int =
internal fun run(context: Context, args: List<String>, 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()
@@ -216,6 +216,13 @@ class BattleNetTest {
assertEquals(emptyList<Pair<String, String>>(), 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"))