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.
This commit is contained in:
visail authored and Kurt Himebauch committed 2026-11-05 11:56:37 -05:00
1 parent 1c24c600cd
commit 969b7e625f
2 files changed
+25 -1

No files matched your search

@@ -61,6 +61,9 @@ object BattleNet {
val STORE: List<Title> 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? {
@@ -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() {