added cover downloading

This commit is contained in:
izzy2lost
2025-10-25 05:06:16 -04:00
parent a46d74c20a
commit ac1949de1d
11 changed files with 623 additions and 24 deletions
+1
View File
@@ -57,3 +57,4 @@ bin/graphicPacks/*
.DS_Store
.kilocode/mcp.json
vcpkg-android-arm64.tar.gz
src/android/app/src/main/assets/COVER_DOWNLOAD_README.md
+2
View File
@@ -247,4 +247,6 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
}
@@ -0,0 +1,224 @@
{
"000500001010f300": "AFPE01",
"0005000010144000": "ADVE01",
"0005000010145e00": "AGRE52",
"0005000010147b00": "ABBEVZ",
"0005000010137c00": "AZEE01",
"0005000010110f00": "AD2P78",
"000500001012b200": "ADXPGD",
"0005000010177700": "ADNP01",
"000500001010db00": "AEME4Q",
"0005000010188c00": "ADRP4Q",
"000500001011c500": "APAE4Q",
"0005000010137f00": "ARKE01",
"0005000010129000": "WDKE08",
"000500001010b400": "ASPE41",
"000500001010d900": "AF3E69",
"000500001011f200": "ASAE01",
"000500001010e400": "AGPE01",
"0005000010147e00": "AHKE01",
"0005000010143300": "AHWE01",
"000500001017d800": "BWPE01",
"0005000010139900": "AJ5E41",
"0005000010176f00": "BJDP41",
"0005000010179100": "BJDE41",
"00050000101b9800": "AJ6P41",
"000500001010ae00": "AJDE41",
"0005000010145200": "AJKE41",
"00050000101abc00": "AXYE01",
"0005000010193300": "BLKP6V",
"0005000010135e00": "ALBE01",
"000500001016ad00": "BTME01",
"00050000101a5c00": "ALJP01",
"0005000010132400": "ALMP01",
"0005000010132b00": "ALME01",
"0005000010168d00": "ALHP01",
"000500001010d800": "AMDE69",
"000500001010c800": "AURP01",
"000500001010c700": "AURE01",
"000500001010ed00": "AMKP01",
"000500001010ec00": "AMKE01",
"0005000010162e00": "ABAP01",
"0005000010162d00": "ABAE01",
"0005000010110800": "AMVP41",
"000500001010af00": "AMVE41",
"0005000010117200": "AHDP08",
"0005000010118300": "AHDE08",
"0005000010128400": "ANSP69",
"0005000010128800": "ANSE69",
"0005000010101d00": "ARPE01",
"0005000010110a00": "ANGE01",
"0005000010102100": "ALCP01",
"0005000010102000": "ALCE01",
"0005000010146300": "APCE01",
"0005000010139400": "APFE5G",
"000500001012be00": "AC3P01",
"000500001010fc00": "ARBP41",
"0005000010111400": "ARMP41",
"000500001010b100": "ARME41",
"000500001012b400": "ABHP08",
"0005000010132c00": "ADCE01",
"000500001010b200": "ASCE01",
"0005000010178f00": "WKNPAY",
"0005000010140400": "ASFP52",
"000500001010b300": "AS2E8P",
"0005000010175b00": "BSSE8P",
"0005000010128f00": "ASNE8P",
"0005000010146900": "AS5E52",
"0005000010145d00": "ARDP01",
"0005000010145c00": "ARDE01",
"000500001018dd00": "AMAP01",
"0005000010145000": "AXFP01",
"0005000010144f00": "AXFE01",
"000500001012c500": "ACRP01",
"0005000010112000": "ACRE01",
"0005000010143600": "BCZP01",
"0005000010143500": "BCZE01",
"000500001014e000": "ALAP01",
"0005000010113300": "ASUE41",
"0005000010135300": "ACMP01",
"000500001012dc00": "ACME01",
"000500001012ac00": "AS9P41",
"0005000010173400": "AYEP52",
"0005000010136700": "ATBP01",
"0005000010133900": "ATBE01",
"0005000010112b00": "AHBPC8",
"0005000010140500": "AWCP41",
"0005000010102300": "ASTE01",
"0005000010137d00": "ANXE01",
"0005000010144d00": "AWSE01",
"000500001010e800": "AW3E52",
"000500001011a700": "AZUP41",
"000500001010dd00": "AZUE41",
"00050000101c4c00": "AX5P01",
"000500001010b600": "AYSE41",
"0005000010149100": "AGRP52",
"0005000010140000": "ANRP52",
"00050000101c6500": "AALP01",
"000500001017b500": "BXAP01",
"000500001010f600": "ASSP41",
"0005000010138800": "ASBP41",
"0005000010157f00": "AAFP01",
"0005000010157e00": "AAFE01",
"0005000010172600": "AQUE01",
"0005000010111000": "ABEP01",
"000500001010ac00": "ABEE01",
"0005000010146600": "ABVP01",
"0005000010147500": "ABVE01",
"0005000010180700": "AKBP01",
"0005000010180600": "AKBE01",
"00050000101b3d00": "BD3P4Q",
"0005000010138300": "ARKP01",
"000500001010ee00": "AF3P69",
"000500001010ff00": "AGPP01",
"0005000010162a00": "ATDP01",
"000500001017d900": "BWPP01",
"0005000010111a00": "AJSP01",
"0005000010145b00": "AJKP41",
"00050000101b5100": "AXYP01",
"0005000010147a00": "AC2PVZ",
"00050000101ca900": "BMSPVZ",
"0005000010101e00": "ARPP01",
"0005000010175d00": "AUNP01",
"0005000010147900": "APCP01",
"000500001017e500": "BPMP01",
"0005000010193f00": "APGPVZ",
"000500001012bd00": "AC3E01",
"000500001010f100": "ARGP01",
"000500001012b100": "ASNP8P",
"0005000010176a00": "AGMP01",
"0005000010111d00": "ASPP41",
"000500001016d600": "AM9P52",
"000500001019e600": "AZAP01",
"00050000101d5000": "BPEP52",
"000500001010f400": "ATRP52",
"0005000010184e00": "AYCP01",
"000500001010f700": "AYSP41",
"0005000010172700": "AQUP01",
"00050000101df500": "APKP01",
"00050000101b0500": "AFXP01",
"00050000101d1d00": "BRQPVZ",
"000500001010f800": "AKNP01",
"00050000101c4000": "BRDPNS",
"00050000101aa500": "BKFPVZ",
"000500001018cc00": "AFRPVZ",
"00050000101be900": "ALRP01",
"000500001016a700": "ALHE01",
"00050000101a3600": "AVXP01",
"00050000101bfc00": "BS5P52",
"0005000010181f00": "BK7P52",
"0005000010111f00": "AS2P8P",
"0005000e10176900": "AGME01",
"00050000101d9d00": "AUME01",
"0005000010176900": "AGME01",
"0005000010110700": "ASVP52",
"000500001011a600": "ACAP52",
"0005000010156100": "ACPP52",
"0005000010136f00": "ADSP4Q",
"000500001012da00": "AF6P52",
"0005000010118400": "AFBP01",
"0005000010140300": null,
"000500001016c800": null,
"000500001010f500": "AMEP69",
"00050000101d0300": null,
"0005000010133b00": "AS8P01",
"000500001010fe00": "ATKP01",
"00050000101a1600": null,
"0005000010129400": "AWDP52",
"000500001014e300": "AZBP01",
"000500001010ab00": "ABTE01",
"0005000010146800": "ACPE52",
"000500001012ba00": "ADXE01",
"0005000010111600": "AF6E52",
"0005000010101a00": "APLE01",
"000500001014b700": "ATWE01",
"000500001010d700": "ASLE52",
"0005000010134e00": "AS8E01",
"00050000101b0400": "AFXE01",
"000500001018dc00": "AMAE01",
"000500001010fd00": "ATKE01",
"000500001010e300": "AS9E41",
"0005000010184d00": "AYCE01",
"00050000101c4d00": "AX5E01",
"00050000101df400": "APKE01",
"0005000010113400": "AECP52",
"0005000010111e00": "ASCP01",
"000500001f600a00": "CNFE01",
"000500001010f000": "AJDP41",
"000500001f600b00": "CNFP01",
"000500001019e500": "AZAE01",
"0005000e10180700": "AKBP01",
"0005000010144e00": "AWSP01",
"0005000e10136f00": "ADSP4Q",
"0005000010172b00": "AW9E01",
"0005000010138e00": "ARME41",
"0005000010131c00": "AW9P01",
"0005000e10128400": "ANSP69",
"0005000e10102100": "ALCP01",
"0005000c1012b100": "ASNP8P",
"000500021010ef01": "AZUE41",
"0005000010159a00": "ASTE01",
"0005000e1010ed00": "AMKP01",
"0005000e10188c00": "ADRP4Q",
"0005000e10181f00": "BK7P52",
"0005000e1011b700": "AS9P41",
"0005000e10144e00": "AWSP01",
"0005000e10146100": "AFDE01",
"0005000e10113400": "AECP52",
"000500001016e200": "AXZE01",
"0005000e10195d00": "APZP01",
"0005000c10145000": "AXFP01",
"000500001019cd00": "AGGE01",
"0005000010119300": "APCE01",
"0005000010132f00": "AHDP08",
"000500001019d000": "ARKE01",
"0005000010146100": "AFDE01",
"0005000010109900": "AZAE01",
"0005000010145600": "ADBE01",
"0005000e10145000": "AXFP01",
"0005000e10138e00": "ARME41",
"00050000101b9900": "APHE01",
"0005000010111300": "ABTP01",
"0005000010131d00": "ASEJ01",
"000500001014b800": "ARPP01"
}
@@ -89,6 +89,14 @@ namespace NativeEmulation
if (!fs::exists(controllerProfileFolder))
fs::create_directories(controllerProfileFolder);
// Initialize controller 0 with Wii U Gamepad if no profile exists
const auto controller0Profile = ActiveSettings::GetConfigPath("controllerProfiles/controller0.xml");
if (!fs::exists(controller0Profile))
{
InputManager::instance().set_controller(0, EmulatedController::Type::VPAD);
InputManager::instance().save(0);
}
const auto memorySearcherFolder = ActiveSettings::GetUserDataPath(L"memorySearcher").generic_wstring();
if (!fs::exists(memorySearcherFolder))
fs::create_directories(memorySearcherFolder);
@@ -19,13 +19,19 @@ class EmulationSettings(sharedPreferences: SharedPreferences) {
var gamePadPosition by sharedPreferences.enumPref(GamePadPosition.RIGHT)
}
enum class GameListViewMode {
LIST,
COVER
}
class GuiSettings(sharedPreferences: SharedPreferences) {
var language by sharedPreferences.stringPref(DEFAULT_LANGUAGE)
var gameListViewMode by sharedPreferences.enumPref(GameListViewMode.LIST)
}
class InputOverlaySettings(sharedPreferences: SharedPreferences) {
var isVibrateOnTouchEnabled by sharedPreferences.booleanPref(false)
var isOverlayEnabled by sharedPreferences.booleanPref(false)
var isOverlayEnabled by sharedPreferences.booleanPref(true)
var controllerIndex by sharedPreferences.intPref(0)
var alpha by sharedPreferences.intPref(64)
}
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
@@ -33,6 +34,17 @@ import androidx.compose.material.icons.filled.GraphicEq
import androidx.compose.material.icons.filled.Headphones
import androidx.compose.material.icons.filled.Layers
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.automirrored.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
@@ -69,6 +81,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -78,7 +91,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.izzy2lost.weeu.R
import com.izzy2lost.weeu.common.ui.components.FilledSearchToolbar
import com.izzy2lost.weeu.common.settings.GameListViewMode
import com.izzy2lost.weeu.common.settings.SettingsManager
import com.izzy2lost.weeu.gamelist.cover.CoverDownloadService
import com.izzy2lost.weeu.common.ui.localization.tr
import java.io.File
import com.izzy2lost.weeu.nativeinterface.NativeGameTitles
import com.izzy2lost.weeu.nativeinterface.NativeGameTitles.Game
import kotlinx.coroutines.delay
@@ -107,6 +124,9 @@ fun GamesListScreen(
val gameSearchQuery by gameListViewModel.filterText.collectAsStateWithLifecycle()
val games by gameListViewModel.games.collectAsStateWithLifecycle()
var fabExpanded by rememberSaveable { mutableStateOf(false) }
var viewMode by remember { mutableStateOf(SettingsManager.guiSettings.gameListViewMode) }
val context = LocalContext.current
val coverService = remember { CoverDownloadService(context) }
val state = rememberPullToRefreshState()
@@ -127,7 +147,18 @@ fun GamesListScreen(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
topBar = {
FilledSearchToolbar(
actions = toolbarActions,
actions = {
IconButton(onClick = {
viewMode = if (viewMode == GameListViewMode.LIST) GameListViewMode.COVER else GameListViewMode.LIST
SettingsManager.guiSettings.gameListViewMode = viewMode
}) {
Icon(
imageVector = if (viewMode == GameListViewMode.LIST) Icons.Filled.ViewModule else Icons.AutoMirrored.Filled.ViewList,
contentDescription = tr("Toggle view")
)
}
toolbarActions()
},
hint = tr("Search games"),
query = gameSearchQuery,
onValueChange = gameListViewModel::setFilterText
@@ -165,6 +196,8 @@ fun GamesListScreen(
) {
GameList(
games = games,
viewMode = viewMode,
coverService = coverService,
setFavorite = gameListViewModel::setGameTitleFavorite,
deleteShaderCaches = {
coroutineScope.launch { snackbarHostState.showSnackbar(tr("Shader caches removed")) }
@@ -189,6 +222,8 @@ fun GamesListScreen(
@Composable
private fun GameList(
games: List<Game>,
viewMode: GameListViewMode,
coverService: CoverDownloadService,
startGame: (Game) -> Unit,
goToGameDetails: (Game) -> Unit,
goToGameEditProfile: (Game) -> Unit,
@@ -200,28 +235,51 @@ private fun GameList(
modifier = Modifier
.padding(8.dp)
.fillMaxSize(),
columns = GridCells.Adaptive(620.dp)
columns = if (viewMode == GameListViewMode.COVER) GridCells.Adaptive(150.dp) else GridCells.Adaptive(620.dp)
) {
items(items = games, key = { it.titleId }) { game ->
var showDeleteShaderConfirmationDialog by remember { mutableStateOf(false) }
GameListItem(
modifier = Modifier.animateItem(),
game = game,
onStartGame = startGame,
onIsFavoriteChanged = { isFavorite ->
setFavorite(game, isFavorite)
},
onEditGameProfile = {
goToGameEditProfile(game)
},
onRemoveShaderCaches = { showDeleteShaderConfirmationDialog = true },
onAboutTitle = {
goToGameDetails(game)
},
onCreateShortcut = {
createShortcut(game)
},
)
if (viewMode == GameListViewMode.COVER) {
GameCoverItem(
modifier = Modifier.animateItem(),
game = game,
coverService = coverService,
onStartGame = startGame,
onIsFavoriteChanged = { isFavorite ->
setFavorite(game, isFavorite)
},
onEditGameProfile = {
goToGameEditProfile(game)
},
onRemoveShaderCaches = { showDeleteShaderConfirmationDialog = true },
onAboutTitle = {
goToGameDetails(game)
},
onCreateShortcut = {
createShortcut(game)
},
)
} else {
GameListItem(
modifier = Modifier.animateItem(),
game = game,
onStartGame = startGame,
onIsFavoriteChanged = { isFavorite ->
setFavorite(game, isFavorite)
},
onEditGameProfile = {
goToGameEditProfile(game)
},
onRemoveShaderCaches = { showDeleteShaderConfirmationDialog = true },
onAboutTitle = {
goToGameDetails(game)
},
onCreateShortcut = {
createShortcut(game)
},
)
}
if (showDeleteShaderConfirmationDialog)
ShaderCachesConfirmationDialog(
@@ -475,3 +533,102 @@ private fun GameContextMenu(
)
}
}
@Composable
private fun GameCoverItem(
game: Game,
coverService: CoverDownloadService,
onStartGame: (Game) -> Unit,
onIsFavoriteChanged: (Boolean) -> Unit,
onEditGameProfile: () -> Unit,
onRemoveShaderCaches: () -> Unit,
onAboutTitle: () -> Unit,
onCreateShortcut: () -> Unit,
modifier: Modifier = Modifier,
) {
var contextMenuExpanded by rememberSaveable { mutableStateOf(false) }
var coverFile by remember { mutableStateOf<File?>(null) }
LaunchedEffect(game.titleId) {
val titleIdHex = game.titleId.toString(16).uppercase().padStart(16, '0')
println("Downloading cover for ${game.name} with titleId: $titleIdHex")
val result = coverService.downloadCover(titleIdHex)
println("Cover download result for ${game.name}: ${result?.absolutePath}")
coverFile = result
}
Card(
modifier = modifier
.padding(4.dp)
.combinedClickable(
onClick = { onStartGame(game) },
onLongClick = { contextMenuExpanded = true }
)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.71f)
) {
if (coverFile != null && coverFile!!.exists()) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(coverFile)
.crossfade(true)
.build(),
contentDescription = game.name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit
)
} else {
GameIcon(
game = game,
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
)
}
if (game.isFavorite) {
Icon(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
.size(24.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant),
painter = painterResource(R.drawable.ic_favorite),
tint = MaterialTheme.colorScheme.primary,
contentDescription = null
)
}
}
Text(
text = game.name ?: "",
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)
}
GameContextMenu(
expanded = contextMenuExpanded,
onDismissRequest = { contextMenuExpanded = false },
game = game,
onIsFavoriteChanged = onIsFavoriteChanged,
onEditGameProfile = onEditGameProfile,
onRemoveShaderCaches = onRemoveShaderCaches,
onAboutTitle = onAboutTitle,
onCreateShortcut = onCreateShortcut,
)
}
}
@@ -0,0 +1,113 @@
package com.izzy2lost.weeu.gamelist.cover
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
class CoverDownloadService(private val context: Context) {
private val client = OkHttpClient()
private val coverCacheDir = File(context.cacheDir, "covers").apply { mkdirs() }
init {
GameTitleMapper.initialize(context)
}
suspend fun downloadCover(titleIdHex: String, region: String = "US"): File? = withContext(Dispatchers.IO) {
try {
val coverFile = getCoverFile(titleIdHex)
if (coverFile.exists()) {
println("Cover already exists: ${coverFile.absolutePath}")
return@withContext coverFile
}
// Get game ID from mapper (this is the full 6-character GameTDB ID like ALZP01)
val gameId = GameTitleMapper.getGameCodeFromTitleId(titleIdHex)
println("Title ID: $titleIdHex -> Game ID: $gameId")
if (gameId != null) {
// If the game ID is already 6 characters, use it directly
// Otherwise, append region code
val fullGameId = if (gameId.length >= 6) {
gameId
} else {
// GameTDB format: 4-char code + 2-char region (e.g., ALZP01 for US, ALZP02 for EU)
val regionCode = when(region) {
"US", "USA" -> "01"
"EU", "EUR" -> "02"
"JP", "JPN" -> "00"
else -> "01"
}
gameId + regionCode
}
println("Full game ID: $fullGameId")
// Try multiple URL formats, prioritizing custom upscaled 3D covers from GitHub
val urls = listOf(
// Custom upscaled 3D covers from GitHub (preferred - 4x quality)
"https://raw.githubusercontent.com/izzy2lost/WeeU-Covers/main/3D/$fullGameId.png",
// GameTDB 3D covers (fallback)
"https://art.gametdb.com/wiiu/cover3D/US/$fullGameId.png",
"https://art.gametdb.com/wiiu/cover3D/EN/$fullGameId.png",
"https://art.gametdb.com/wiiu/cover3D/EU/$fullGameId.png",
"https://art.gametdb.com/wiiu/cover3D/JA/$fullGameId.png",
// Full HQ covers (fallback)
"https://art.gametdb.com/wiiu/coverfullHQ/US/$fullGameId.png",
"https://art.gametdb.com/wiiu/coverfullHQ/EN/$fullGameId.png",
"https://art.gametdb.com/wiiu/coverfullHQ/EU/$fullGameId.png",
// Regular covers (last resort)
"https://art.gametdb.com/wiiu/cover/US/$fullGameId.png",
"https://art.gametdb.com/wiiu/cover/EN/$fullGameId.png",
"https://art.gametdb.com/wiiu/cover/EU/$fullGameId.png"
)
for (url in urls) {
try {
println("Trying URL: $url")
val request = Request.Builder()
.url(url)
.addHeader("User-Agent", "WeeU-Android/1.0")
.build()
client.newCall(request).execute().use { response ->
println("Response code: ${response.code}")
if (response.isSuccessful) {
response.body?.byteStream()?.use { input ->
coverFile.outputStream().use { output ->
input.copyTo(output)
}
println("Successfully downloaded cover from: $url")
println("Saved to: ${coverFile.absolutePath}")
return@withContext coverFile
}
}
}
} catch (e: Exception) {
println("Error downloading from $url: ${e.message}")
// Try next URL
continue
}
}
println("Failed to download cover from any URL for game ID: $fullGameId")
} else {
println("Could not determine game ID for title: $titleIdHex")
}
null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun getCoverFile(titleId: String): File {
return File(coverCacheDir, "$titleId.png")
}
fun hasCover(titleId: String): Boolean {
return getCoverFile(titleId).exists()
}
}
@@ -0,0 +1,85 @@
package com.izzy2lost.weeu.gamelist.cover
import android.content.Context
import kotlinx.serialization.json.Json
import java.io.BufferedReader
import java.io.InputStreamReader
object GameTitleMapper {
private var titleIdToGameIdMap: Map<String, String?>? = null
private val json = Json { ignoreUnknownKeys = true }
fun initialize(context: Context) {
if (titleIdToGameIdMap != null) {
println("GameTitleMapper already initialized")
return
}
try {
// Load title ID to game ID mapping
println("Loading titleid_to_gameid_mapping.json from assets...")
val mapInputStream = context.assets.open("titleid_to_gameid_mapping.json")
val mapReader = BufferedReader(InputStreamReader(mapInputStream))
val mapJsonString = mapReader.use { it.readText() }
titleIdToGameIdMap = json.decodeFromString(mapJsonString)
println("Loaded ${titleIdToGameIdMap?.size} title ID mappings")
} catch (e: Exception) {
println("Error loading game data: ${e.message}")
e.printStackTrace()
titleIdToGameIdMap = emptyMap()
}
}
fun getGameCodeFromTitleId(titleIdHex: String): String? {
val normalizedId = titleIdHex.lowercase().padStart(16, '0')
println("Looking up title ID: $normalizedId")
// First, try to get the game ID from the mapping file
val gameId = titleIdToGameIdMap?.get(normalizedId)
if (gameId != null) {
println("Found game ID in mapping: $gameId")
return gameId
}
// If not found in mapping, try to extract from title ID
println("Game ID not found in mapping, attempting extraction")
val extractedCode = extractGameCode(normalizedId)
println("Extracted game code: $extractedCode")
return extractedCode
}
private fun extractGameCode(titleIdHex: String): String? {
if (titleIdHex.length != 16) return null
// Wii U title ID format: 0005000010XXXXXX
// GameTDB format: 6-char game ID (e.g., ALZP01)
// The game code is encoded in bytes 10-13 (hex positions 8-15)
// Extract last 8 hex digits: positions 8-15
val gameIdHex = titleIdHex.substring(8, 16)
// Convert hex to bytes and try to extract ASCII game code
return try {
// Take first 8 hex chars (4 bytes) for the game code
val codeBytes = gameIdHex.substring(0, 8).chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
// Convert to ASCII string
val gameCode = String(codeBytes, Charsets.US_ASCII)
.filter { it.isLetterOrDigit() }
// GameTDB format is typically 6 characters (4 game + 2 region)
// For now, just return the 4-character game code
// The region will be added by the download service
if (gameCode.length >= 4) {
gameCode.take(4).uppercase()
} else {
null
}
} catch (e: Exception) {
null
}
}
}
+3
View File
@@ -17,6 +17,7 @@ espresso-core = "3.7.0"
okhttp = "5.1.0"
kotlinx-serialization-json = "1.9.0"
kotlinx-gettext = "0.7.0"
coil = "3.0.4"
[libraries]
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
@@ -42,6 +43,8 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version.ref = "okhttp" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
kotlinx-gettext = { module = "name.kropp.kotlinx-gettext:kotlinx-gettext", version.ref = "kotlinx-gettext" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
[plugins]
aboutlibraries-android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlibraries" }
+2 -2
View File
@@ -152,7 +152,7 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser)
notification.controller_profiles = overlay_node.get("ControllerProfiles", true);
notification.controller_battery = overlay_node.get("ControllerBattery", true);
notification.shader_compiling = overlay_node.get("ShaderCompiling", true);
notification.shader_compiling = overlay_node.get("ShaderCompiling", false);
}
else
{
@@ -167,7 +167,7 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser)
notification.controller_profiles = graphic.get("OverlayControllerProfiles", true);
notification.controller_battery = graphic.get("OverlayControllerBattery", true);
notification.shader_compiling = graphic.get("ShaderCompiling", true);
notification.shader_compiling = graphic.get("ShaderCompiling", false);
}
auto notification_node = graphic.get("Notification");
+1 -1
View File
@@ -416,7 +416,7 @@ struct CemuConfig
sint32 text_scale = 100;
bool controller_profiles = true;
bool controller_battery = false;
bool shader_compiling = true;
bool shader_compiling = false;
bool friends = true;
} notification{};