diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index a7e3581ee..78198632a 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -78,6 +78,16 @@ + + + + + + + + proceedWithDriverCheckAndLaunch(titleId, iniText) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> finishWithResult(success = false) } + .setOnCancelListener { finishWithResult(success = false) } + .show() + } else { + proceedWithDriverCheckAndLaunch(titleId, iniText) + } + } + + private fun proceedWithDriverCheckAndLaunch(titleId: Long, iniText: String) { + val idHex = String.format("%016X", titleId) + val requestedBackend = extractGraphicsApi(iniText) + + if (requestedBackend == GRAPHICS_BACKEND_VULKAN) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.custom_launch_backend_warning_title) + .setMessage(R.string.custom_launch_backend_warning_message) + .setPositiveButton(R.string.custom_launch_backend_option_use_vulkan) { _, _ -> + writeConfigAndLaunch(titleId, iniText, idHex) + } + .setNegativeButton(R.string.custom_launch_backend_option_use_opengl) { _, _ -> + val adjusted = replaceGraphicsApi(iniText, GRAPHICS_BACKEND_OPENGL) + Log.info("[ExternalLaunch] Falling back to OpenGL for external launch") + writeConfigAndLaunch(titleId, adjusted, idHex) + } + .setOnCancelListener { finishWithResult(success = false) } + .show() + return + } + + writeConfigAndLaunch(titleId, iniText, idHex) + } + + private fun writeConfigAndLaunch(titleId: Long, iniText: String, idHex: String) { + SettingsFile.saveCustomFileRaw(idHex, iniText) + + val game = findGameByTitleId(titleId) + if (game == null) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.custom_launch_missing_game_title) + .setMessage(getString(R.string.custom_launch_missing_game_message, idHex)) + .setPositiveButton(android.R.string.ok) { _, _ -> finishWithResult(success = false) } + .setOnCancelListener { finishWithResult(success = false) } + .show() + return + } + + val launch = Intent(this, EmulationActivity::class.java) + launch.putExtra("game", game) + startActivity(launch) + finishWithResult(success = true) + } + + private fun findGameByTitleId(titleId: Long): Game? { + // Try cached games first + val prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + val serialized = prefs.getStringSet(GameHelper.KEY_GAMES, emptySet()) ?: emptySet() + if (serialized.isNotEmpty()) { + val games = serialized.mapNotNull { + try { kotlinx.serialization.json.Json.decodeFromString(org.citra.citra_emu.model.Game.serializer(), it) } catch (_: Exception) { null } + } + games.firstOrNull { it.titleId == titleId }?.let { return it } + } + // Fallback: rescan library + return GameHelper.getGames().firstOrNull { it.titleId == titleId } + } + + companion object { + const val EXTRA_TITLE_ID = "title_id" + const val EXTRA_CONFIG_INI = "config_ini" + private const val GRAPHICS_BACKEND_OPENGL = 1 + private const val GRAPHICS_BACKEND_VULKAN = 2 + } + + private fun parseTitleId(raw: String?): Long? { + if (raw.isNullOrBlank()) return null + val trimmed = raw.trim() + val withoutPrefix = if (trimmed.startsWith("0x", true)) trimmed.substring(2) else trimmed + + // Prefer hexadecimal interpretation – Title IDs are traditionally provided in hex. + val hexValue = withoutPrefix.toLongOrNull(16) + if (hexValue != null) return hexValue + + return withoutPrefix.toLongOrNull() + } + + private fun extractGraphicsApi(config: String): Int? { + val regex = Regex( + "^\\s*graphics_api\\s*=\\s*(\\d+)\\s*$", + setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE) + ) + val match = regex.find(config) ?: return null + return match.groupValues.getOrNull(1)?.trim()?.toIntOrNull() + } + + private fun replaceGraphicsApi(config: String, backend: Int): String { + val regex = Regex( + "^\\s*graphics_api\\s*=\\s*(\\d+)\\s*$", + setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE) + ) + return regex.replace(config) { "graphics_api = $backend" } + } + + private fun finishWithResult(success: Boolean) { + setResult(if (success) RESULT_OK else RESULT_CANCELED) + finish() + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt index 98845319d..2a858a899 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt @@ -170,6 +170,19 @@ object SettingsFile { } } + fun saveCustomFileRaw(gameId: String, contents: String) { + val ini = getOrCreateCustomGameSettingsFile(gameId) + val context: Context = CitraApplication.appContext + context.contentResolver.openOutputStream(ini.uri, "wt").use { out -> + out?.write(contents.toByteArray()) + out?.flush() + } + } + + fun customExists(gameId: String): Boolean { + return findCustomGameSettingsFile(gameId) != null + } + fun saveFile( fileName: String, setting: AbstractSetting diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 4faf71283..a4a7dbfa7 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -496,6 +496,14 @@ Remove Custom Settings Use default + Custom settings for this game already exist. Overwrite them and proceed? + Game not found + This title isn\'t in your Azahar library (ID: %1$s). Add it to the library and try again. + Vulkan backend requested + This configuration asks Azahar to use the Vulkan renderer. Some devices crash when launching games externally with Vulkan. Keep Vulkan, or switch this launch to OpenGL for safety? + Use Vulkan + Use OpenGL + Missing required driver: %1$s. Please install it and try again. Select Amiibo File Error Loading Amiibo While loading the specified Amiibo file, an error occurred. Please check that the file is correct.