diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index 482bc0b08..7c9e66441 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -18,6 +18,7 @@ import org.citra.citra_emu.features.hotkeys.Hotkey import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.features.settings.utils.InputConfigSync class InputBindingSetting( val abstractSetting: AbstractSetting, @@ -171,13 +172,16 @@ class InputBindingSetting( .remove(oldKey + "_GuestButton") // Used for axis button .remove(oldKey + "_Inverted") // used for axis inversion .apply() + + // Also clear from config.ini + InputConfigSync.clearMappingFromConfig(abstractSetting.key!!) } } /** * Helper function to write a gamepad button mapping for the setting. */ - private fun writeButtonMapping(key: String) { + private fun writeButtonMapping(key: String, hostKeyCode: Int) { val editor = preferences.edit() // Remove mapping for another setting using this input @@ -198,6 +202,9 @@ class InputBindingSetting( // Apply changes editor.apply() + + // Sync to config.ini (ButtonType for core, keyCode for UI restoration) + InputConfigSync.writeButtonMappingToConfig(abstractSetting.key!!, buttonCode, hostKeyCode) } /** @@ -215,6 +222,13 @@ class InputBindingSetting( // Write next reverse mapping for future cleanup .putString(reverseKey, getInputAxisKey(axis)) .apply() + + // Sync to config.ini + if (isTrigger()) { + InputConfigSync.writeAxisButtonMappingToConfig(abstractSetting.key!!, axis, "+") + } else { + InputConfigSync.writeAxisMappingToConfig(abstractSetting.key!!, axis, isHorizontalOrientation()) + } } /** @@ -229,7 +243,7 @@ class InputBindingSetting( } val code = translateEventToKeyId(keyEvent) - writeButtonMapping(getInputButtonKey(code)) + writeButtonMapping(getInputButtonKey(code), code) val uiString = "${keyEvent.device.name}: Button $code" value = uiString } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt index 33aea46f9..4a897a89a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt @@ -18,6 +18,7 @@ import org.citra.citra_emu.utils.FileUtil import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.PermissionsHandler import org.citra.citra_emu.utils.TurboHelper +import org.citra.citra_emu.features.settings.utils.InputConfigSync class SettingsActivityPresenter(private val activityView: SettingsActivityView) { val settings: Settings get() = activityView.settings @@ -53,6 +54,8 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView) } else { settings.loadSettings(activityView) } + // Sync input controls from config.ini to SharedPreferences + InputConfigSync.syncFromConfigToPreferences() } activityView.showSettingsFragment(menuTag, false, gameId) activityView.onSettingsFileLoaded() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/InputConfigSync.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/InputConfigSync.kt new file mode 100644 index 000000000..f1c18e965 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/InputConfigSync.kt @@ -0,0 +1,252 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.utils + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.utils.Log +import java.io.BufferedReader +import java.io.InputStreamReader + +object InputConfigSync { + private const val INPUT_MAPPING_PREFIX = "InputMapping" + + private val context: Context get() = CitraApplication.appContext + private val preferences: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(context) + + // Maps Settings.KEY_* to NativeLibrary.ButtonType codes + private val buttonKeyToCode = mapOf( + Settings.KEY_BUTTON_A to NativeLibrary.ButtonType.BUTTON_A, + Settings.KEY_BUTTON_B to NativeLibrary.ButtonType.BUTTON_B, + Settings.KEY_BUTTON_X to NativeLibrary.ButtonType.BUTTON_X, + Settings.KEY_BUTTON_Y to NativeLibrary.ButtonType.BUTTON_Y, + Settings.KEY_BUTTON_L to NativeLibrary.ButtonType.TRIGGER_L, + Settings.KEY_BUTTON_R to NativeLibrary.ButtonType.TRIGGER_R, + Settings.KEY_BUTTON_ZL to NativeLibrary.ButtonType.BUTTON_ZL, + Settings.KEY_BUTTON_ZR to NativeLibrary.ButtonType.BUTTON_ZR, + Settings.KEY_BUTTON_SELECT to NativeLibrary.ButtonType.BUTTON_SELECT, + Settings.KEY_BUTTON_START to NativeLibrary.ButtonType.BUTTON_START, + Settings.KEY_BUTTON_HOME to NativeLibrary.ButtonType.BUTTON_HOME, + Settings.KEY_BUTTON_UP to NativeLibrary.ButtonType.DPAD_UP, + Settings.KEY_BUTTON_DOWN to NativeLibrary.ButtonType.DPAD_DOWN, + Settings.KEY_BUTTON_LEFT to NativeLibrary.ButtonType.DPAD_LEFT, + Settings.KEY_BUTTON_RIGHT to NativeLibrary.ButtonType.DPAD_RIGHT + ) + + // Maps Settings.KEY_* to analog codes + private val analogKeyToCode = mapOf( + Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL to NativeLibrary.ButtonType.STICK_LEFT, + Settings.KEY_CIRCLEPAD_AXIS_VERTICAL to NativeLibrary.ButtonType.STICK_LEFT, + Settings.KEY_CSTICK_AXIS_HORIZONTAL to NativeLibrary.ButtonType.STICK_C, + Settings.KEY_CSTICK_AXIS_VERTICAL to NativeLibrary.ButtonType.STICK_C, + Settings.KEY_DPAD_AXIS_HORIZONTAL to NativeLibrary.ButtonType.DPAD, + Settings.KEY_DPAD_AXIS_VERTICAL to NativeLibrary.ButtonType.DPAD + ) + + // All button keys + private val allButtonKeys = listOf( + Settings.KEY_BUTTON_A, Settings.KEY_BUTTON_B, Settings.KEY_BUTTON_X, Settings.KEY_BUTTON_Y, + Settings.KEY_BUTTON_L, Settings.KEY_BUTTON_R, Settings.KEY_BUTTON_ZL, Settings.KEY_BUTTON_ZR, + Settings.KEY_BUTTON_SELECT, Settings.KEY_BUTTON_START, Settings.KEY_BUTTON_HOME, + Settings.KEY_BUTTON_UP, Settings.KEY_BUTTON_DOWN, Settings.KEY_BUTTON_LEFT, Settings.KEY_BUTTON_RIGHT + ) + + // All axis keys + private val allAxisKeys = listOf( + Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, + Settings.KEY_CSTICK_AXIS_HORIZONTAL, Settings.KEY_CSTICK_AXIS_VERTICAL, + Settings.KEY_DPAD_AXIS_HORIZONTAL, Settings.KEY_DPAD_AXIS_VERTICAL + ) + + fun writeButtonMappingToConfig(settingsKey: String, buttonCode: Int, keyCode: Int) { + val paramPackage = "engine:gamepad,code:$buttonCode,keycode:$keyCode" + writeToConfigIni(settingsKey, paramPackage) + Log.debug("[InputConfigSync] Wrote button mapping: $settingsKey = $paramPackage") + } + + fun writeAxisMappingToConfig(settingsKey: String, axis: Int, isHorizontal: Boolean) { + val paramPackage = "engine:gamepad,code:$axis" + writeToConfigIni(settingsKey, paramPackage) + Log.debug("[InputConfigSync] Wrote axis mapping: $settingsKey = $paramPackage") + } + + fun writeAxisButtonMappingToConfig(settingsKey: String, axis: Int, direction: String) { + val threshold = if (direction == "+") "0.5" else "-0.5" + val paramPackage = "engine:gamepad,axis:$axis,direction:$direction,threshold:$threshold" + writeToConfigIni(settingsKey, paramPackage) + Log.debug("[InputConfigSync] Wrote axis-button mapping: $settingsKey = $paramPackage") + } + + fun clearMappingFromConfig(settingsKey: String) { + writeToConfigIni(settingsKey, "") + Log.debug("[InputConfigSync] Cleared mapping: $settingsKey") + } + + fun syncFromConfigToPreferences() { + try { + val controlSettings = readControlsFromConfigIni() + if (controlSettings.isEmpty()) { + Log.info("[InputConfigSync] No control settings found in config.ini") + return + } + + val editor = preferences.edit() + var syncCount = 0 + + // Sync button mappings + for (key in allButtonKeys) { + val paramPackage = controlSettings[key] + if (!paramPackage.isNullOrEmpty()) { + parseAndSyncButtonMapping(editor, key, paramPackage) + syncCount++ + } + } + + // Sync axis mappings + for (key in allAxisKeys) { + val paramPackage = controlSettings[key] + if (!paramPackage.isNullOrEmpty()) { + parseAndSyncAxisMapping(editor, key, paramPackage) + syncCount++ + } + } + + editor.apply() + Log.info("[InputConfigSync] Synced $syncCount controls from config.ini to SharedPreferences") + } catch (e: Exception) { + Log.error("[InputConfigSync] Failed to sync from config: ${e.message}") + e.printStackTrace() + } + } + + private fun readControlsFromConfigIni(): Map { + val result = mutableMapOf() + + try { + val configFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) + val inputStream = context.contentResolver.openInputStream(configFile.uri) ?: return result + val reader = BufferedReader(InputStreamReader(inputStream)) + + var inControlsSection = false + + reader.useLines { lines -> + for (line in lines) { + val trimmedLine = line.trim() + + // Check for section headers + if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) { + val sectionName = trimmedLine.substring(1, trimmedLine.length - 1) + inControlsSection = (sectionName == Settings.SECTION_CONTROLS) + continue + } + + // Parse key=value pairs in Controls section + if (inControlsSection && trimmedLine.contains("=")) { + val parts = trimmedLine.split("=", limit = 2) + if (parts.size == 2) { + val key = parts[0].trim() + val value = parts[1].trim() + if (value.isNotEmpty()) { + result[key] = value + Log.debug("[InputConfigSync] Read from config.ini: $key = $value") + } + } + } + } + } + + reader.close() + } catch (e: Exception) { + Log.error("[InputConfigSync] Error reading config.ini: ${e.message}") + } + + return result + } + + private fun parseAndSyncButtonMapping(editor: SharedPreferences.Editor, key: String, paramPackage: String) { + val params = parseParamPackage(paramPackage) + + if (params.containsKey("code")) { + // Get the Android keyCode (for SharedPreferences mapping and UI) + // If keycode is present, use it; otherwise fall back to code + val keyCode = params["keycode"]?.toIntOrNull() ?: params["code"]?.toIntOrNull() ?: return + val guestButtonCode = buttonKeyToCode[key] ?: return + + val inputKey = "${INPUT_MAPPING_PREFIX}_HostAxis_$keyCode" + editor.putInt(inputKey, guestButtonCode) + + val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_$key" + editor.putString(reverseKey, inputKey) + + // Show keyCode in UI (the Android button code user pressed) + editor.putString(key, "Gamepad: Button $keyCode") + } else if (params.containsKey("axis")) { + val axis = params["axis"]?.toIntOrNull() ?: return + val direction = params["direction"] ?: "+" + val guestButtonCode = buttonKeyToCode[key] ?: return + + val axisKey = "${INPUT_MAPPING_PREFIX}_HostAxis_$axis" + editor.putInt("${axisKey}_GuestButton", guestButtonCode) + + val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_$key" + editor.putString(reverseKey, axisKey) + + editor.putString(key, "Gamepad: Axis $axis $direction") + } + } + + private fun parseAndSyncAxisMapping(editor: SharedPreferences.Editor, key: String, paramPackage: String) { + val params = parseParamPackage(paramPackage) + + if (params.containsKey("code")) { + val axis = params["code"]?.toIntOrNull() ?: return + val guestButtonCode = analogKeyToCode[key] ?: return + val isHorizontal = key.contains("horizontal", ignoreCase = true) + + val axisKey = "${INPUT_MAPPING_PREFIX}_HostAxis_$axis" + editor.putInt("${axisKey}_GuestButton", guestButtonCode) + editor.putInt("${axisKey}_GuestOrientation", if (isHorizontal) 0 else 1) + + val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${key}_${if (isHorizontal) 0 else 1}" + editor.putString(reverseKey, axisKey) + + editor.putString(key, "Gamepad: Axis $axis") + } + } + + private fun parseParamPackage(paramPackage: String): Map { + return paramPackage.split(",") + .mapNotNull { part -> + val keyValue = part.split(":") + if (keyValue.size == 2) { + keyValue[0] to keyValue[1] + } else { + null + } + } + .toMap() + } + + private fun writeToConfigIni(key: String, value: String) { + try { + val setting = object : org.citra.citra_emu.features.settings.model.AbstractStringSetting { + override var string: String = value + override val key: String = key + override val section: String = Settings.SECTION_CONTROLS + override val isRuntimeEditable: Boolean = true + override val valueAsString: String = value + override val defaultValue: String = "" + } + SettingsFile.saveFile(SettingsFile.FILE_NAME_CONFIG, setting) + } catch (e: Exception) { + Log.error("[InputConfigSync] Failed to write to config.ini: ${e.message}") + } + } +}