From 969b7e625f776d40bd06c27c10abcebb6064e57c Mon Sep 17 00:00:00 2001 From: visail Date: Tue, 27 Oct 2026 13:55:32 +0800 Subject: [PATCH] fix: list Battle.net games installed under Blizzard's China uids Blizzard's China client records some products under a CN uid, so the installs it makes never reach the Library and their Stores card keeps offering Install: Diablo II: Resurrected osic (catalogue had osi) StarCraft II s2_cn (catalogue had s2) Diablo Immortal anbs_cn (catalogue had anbs) title() now resolves those uids to the international title, so the Library, the Stores "installed" state, the cover folder and the launch code all agree, while Game.installUid keeps the uid the Agent recorded for updates and uninstalls. TITLES and STORE are untouched. --- .../droiddeck/launcher/runtime/BattleNet.kt | 7 ++++++- .../launcher/runtime/BattleNetTest.kt | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) 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 fa58c26ea..a98e3d0cf 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/BattleNet.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/BattleNet.kt @@ -61,6 +61,9 @@ object BattleNet { val STORE: List get() = TITLES.distinctBy { it.code }.filter { it.uid != "d3cn" && it.code != "WoWC" && it.code != "WoWF" } + // Blizzard's China client installs some products under a CN uid; they are the same games and keep their store identity. + private val CN_UIDS = mapOf("s2_cn" to "s2", "osic" to "osi", "anbs_cn" to "anbs") + fun notInstalled(title: Title, installedIds: Collection<String?>) = installedIds.mapNotNull { id -> id?.let(::title)?.code }.none { it == title.code } @@ -164,8 +167,10 @@ object BattleNet { internal fun title(uid: String): Title? { val id = uid.lowercase() TITLES.firstOrNull { it.uid == id }?.let { return it } + CN_UIDS[id]?.let { alias -> TITLES.firstOrNull { it.uid == alias }?.let { return it } } val base = id.substringBeforeLast('_', "") - return if (base.isNotEmpty() && id.substringAfterLast('_') in LOCALES) TITLES.firstOrNull { it.uid == base } else null + if (base.isEmpty() || id.substringAfterLast('_') !in LOCALES) return null + return TITLES.firstOrNull { it.uid == base } ?: CN_UIDS[base]?.let { alias -> TITLES.firstOrNull { it.uid == alias } } } internal fun titleForProduct(product: String): Title? { 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 77800dc1e..c3d821d07 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/BattleNetTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/BattleNetTest.kt @@ -259,6 +259,7 @@ class BattleNetTest { BattleNet.hostFolder(context, "Z:/root/Games/Stores/Battle.net/Hearthstone/")) } + @Test fun wowForeverIsListedBesideRetailInTheSharedWorldOfWarcraftFolder() { val pfx = File(BattleNet.prefix(context), "pfx").apply { mkdirs() } val wow = File(pfx, "drive_c/Program Files (x86)/World of Warcraft").apply { mkdirs() } @@ -329,6 +330,24 @@ class BattleNetTest { assertEquals(listOf(File(wow, "_forever_")), BattleNet.games(context).map { it.folder }) db.writeBytes(message(forever, retail)) assertEquals(listOf(File(wow, "_forever_"), wow), BattleNet.games(context).map { it.folder }) + + @Test fun aGameInstalledUnderBlizzardsChineseUidIsStillListed() { + val pfx = File(BattleNet.prefix(context), "pfx").apply { mkdirs() } + File(pfx, "drive_c/ProgramData/Battle.net/Agent").mkdirs() + File(pfx, "drive_c/ProgramData/Battle.net/Agent/product.db").writeBytes(message( + field(1, message(field(1, "osic"), field(3, message(field(1, "Z:/root/Games/Stores/Battle.net/Diablo II Resurrected"))))), + field(1, message(field(1, "s2_cn"), field(3, message(field(1, "Z:/root/Games/Stores/Battle.net/StarCraft II"))))), + )) + for (folder in listOf("Diablo II Resurrected", "StarCraft II")) { + File(LinuxRuntime.rootDir(context), "root/Games/Stores/Battle.net/$folder").mkdirs() + } + val games = BattleNet.games(context) + assertEquals(listOf("osic", "s2_cn"), games.map { it.installUid }) + assertEquals(listOf("osi", "s2"), games.map { it.uid }) + assertEquals(listOf("Diablo II: Resurrected" to "OSI", "StarCraft II" to "S2"), games.map { it.name to it.code }) + assertFalse(BattleNet.notInstalled(BattleNet.title("osi")!!, listOf("osic"))) + assertFalse(BattleNet.notInstalled(BattleNet.title("s2")!!, games.map { it.installUid })) + } @Test fun aGamesShortcutHasBattleNetLaunchItInItsPrefix() {