diff --git a/.gitignore b/.gitignore index 2973c592..d22c5e75 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 9127fe5a..7a789e0e 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -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) } diff --git a/src/android/app/src/main/assets/titleid_to_gameid_mapping.json b/src/android/app/src/main/assets/titleid_to_gameid_mapping.json new file mode 100644 index 00000000..bef6c6c1 --- /dev/null +++ b/src/android/app/src/main/assets/titleid_to_gameid_mapping.json @@ -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" +} diff --git a/src/android/app/src/main/cpp/NativeEmulation.cpp b/src/android/app/src/main/cpp/NativeEmulation.cpp index a06f353a..0e28a922 100644 --- a/src/android/app/src/main/cpp/NativeEmulation.cpp +++ b/src/android/app/src/main/cpp/NativeEmulation.cpp @@ -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); diff --git a/src/android/app/src/main/java/com/izzy2lost/weeu/common/settings/SettingsManager.kt b/src/android/app/src/main/java/com/izzy2lost/weeu/common/settings/SettingsManager.kt index e467722a..515e8320 100644 --- a/src/android/app/src/main/java/com/izzy2lost/weeu/common/settings/SettingsManager.kt +++ b/src/android/app/src/main/java/com/izzy2lost/weeu/common/settings/SettingsManager.kt @@ -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) } diff --git a/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/GamesListScreen.kt b/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/GamesListScreen.kt index c36258b2..59bf8595 100644 --- a/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/GamesListScreen.kt +++ b/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/GamesListScreen.kt @@ -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, + 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(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, + ) + } +} diff --git a/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/cover/CoverDownloadService.kt b/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/cover/CoverDownloadService.kt new file mode 100644 index 00000000..828927c0 --- /dev/null +++ b/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/cover/CoverDownloadService.kt @@ -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() + } +} diff --git a/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/cover/GameTitleMapper.kt b/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/cover/GameTitleMapper.kt new file mode 100644 index 00000000..ed002ac6 --- /dev/null +++ b/src/android/app/src/main/java/com/izzy2lost/weeu/gamelist/cover/GameTitleMapper.kt @@ -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? = 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 + } + } +} diff --git a/src/android/gradle/libs.versions.toml b/src/android/gradle/libs.versions.toml index e083d429..83d1f693 100644 --- a/src/android/gradle/libs.versions.toml +++ b/src/android/gradle/libs.versions.toml @@ -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" } diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 70957f60..6ba77744 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -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"); diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 60290176..87f867fa 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -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{};