diff --git a/CMakeModules/GenerateSettingKeys.cmake b/CMakeModules/GenerateSettingKeys.cmake index 4fe58113c..28d08ceb8 100644 --- a/CMakeModules/GenerateSettingKeys.cmake +++ b/CMakeModules/GenerateSettingKeys.cmake @@ -234,6 +234,37 @@ if (ANDROID) "android_hide_images" "screen_orientation" "performance_overlay_position" + "button_a" + "button_b" + "button_x" + "button_y" + "button_home" + "circlepad_up" + "circlepad_down" + "circlepad_left" + "circlepad_right" + "button_r" + "button_l" + "button_zr" + "button_zl" + "button_start" + "button_select" + "dpad_up" + "dpad_down" + "dpad_left" + "dpad_right" + "cstick_up" + "cstick_down" + "cstick_left" + "cstick_right" + "hotkey_cycle_layout" + "hotkey_close" + "hotkey_swap" + "hotkey_pause_resume" + "hotkey_quicksave" + "hotkey_turbo_limit" + "hotkey_quickload" + "hotkey_enable" ) string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY}) set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",") diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 9d2015baa..c917011d1 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -959,6 +959,7 @@ object NativeLibrary { const val DPAD = 780 const val BUTTON_DEBUG = 781 const val BUTTON_GPIO14 = 782 + // TODO: replace these with Hotkey buttons - they aren't native! const val BUTTON_SWAP = 800 const val BUTTON_TURBO = 801 } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index c764054e6..df233afe5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -7,7 +7,6 @@ package org.citra.citra_emu.activities import android.Manifest.permission import android.annotation.SuppressLint import android.content.Intent -import android.content.SharedPreferences import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle @@ -26,25 +25,23 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.navigation.fragment.NavHostFragment -import androidx.preference.PreferenceManager -import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.NativeLibrary.ButtonState import org.citra.citra_emu.R import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult import org.citra.citra_emu.contracts.OpenFileResultContract import org.citra.citra_emu.databinding.ActivityEmulationBinding import org.citra.citra_emu.display.ScreenAdjustmentUtil import org.citra.citra_emu.display.SecondaryDisplay -import org.citra.citra_emu.features.hotkeys.HotkeyUtility +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.input.HotkeyUtility import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.Settings -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.fragments.EmulationFragment import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.model.Game import org.citra.citra_emu.utils.BuildUtil -import org.citra.citra_emu.utils.ControllerMappingHelper import org.citra.citra_emu.utils.FileBrowserHelper import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.utils.EmulationMenuSettings @@ -53,10 +50,9 @@ import org.citra.citra_emu.utils.RefreshRateUtil import org.citra.citra_emu.utils.ThemeUtil import org.citra.citra_emu.viewmodel.EmulationViewModel import org.citra.citra_emu.features.settings.utils.SettingsFile +import kotlin.math.abs class EmulationActivity : AppCompatActivity() { - private val preferences: SharedPreferences - get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) var isActivityRecreated = false val emulationViewModel: EmulationViewModel by viewModels() private lateinit var binding: ActivityEmulationBinding @@ -288,13 +284,13 @@ class EmulationActivity : AppCompatActivity() { return true } } - return hotkeyUtility.handleKeyPress(event) + return hotkeyUtility.handleKeyPress(if (event.keyCode == 0) event.scanCode else event.keyCode, event.device.descriptor) } KeyEvent.ACTION_UP -> { - return hotkeyUtility.handleKeyRelease(event) + return hotkeyUtility.handleKeyRelease(if (event.keyCode == 0) event.scanCode else event.keyCode, event.device.descriptor) } else -> { - return false; + return false } } } @@ -314,7 +310,8 @@ class EmulationActivity : AppCompatActivity() { // TODO: Move this check into native code - prevents crash if input pressed before starting emulation if (!NativeLibrary.isRunning() || (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) || - emulationFragment.isDrawerOpen()) { + emulationFragment.isDrawerOpen() + ) { return super.dispatchGenericMotionEvent(event) } @@ -322,206 +319,76 @@ class EmulationActivity : AppCompatActivity() { if (event.actionMasked == MotionEvent.ACTION_CANCEL) { return true } - val input = event.device - val motions = input.motionRanges - val axisValuesCirclePad = floatArrayOf(0.0f, 0.0f) - val axisValuesCStick = floatArrayOf(0.0f, 0.0f) - val axisValuesDPad = floatArrayOf(0.0f, 0.0f) - var isTriggerPressedLMapped = false - var isTriggerPressedRMapped = false - var isTriggerPressedZLMapped = false - var isTriggerPressedZRMapped = false - var isTriggerPressedL = false - var isTriggerPressedR = false - var isTriggerPressedZL = false - var isTriggerPressedZR = false - for (range in motions) { + val device = event.device + val manager = emulationViewModel.settings.inputMappingManager + + val stickAccumulator = HashMap>() + + for (range in device.motionRanges) { val axis = range.axis val origValue = event.getAxisValue(axis) - var value = ControllerMappingHelper.scaleAxis(input, axis, origValue) - val nextMapping = - preferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1) - val guestOrientation = - preferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1) - val inverted = preferences.getBoolean(InputBindingSetting.getInputAxisInvertedKey(axis),false); - if (nextMapping == -1 || guestOrientation == -1) { - // Axis is unmapped + var value = GamepadHelper.scaleAxis(device, axis, origValue) + if (value > -0.1f && value < 0.1f) value = 0f + + val axisPair = Pair(axis, if (value >= 0f) 1 else -1) + + // special case where the axis value is 0 - we need to send releases to both directions + if (value == 0f) { + listOf(Pair(axis, 1), Pair(axis, -1)).forEach { zeroPair -> + manager.getOutAxesForAxis(zeroPair).forEach { (outAxis, outDir) -> + val component = + GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach + val current = + stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f)) + stickAccumulator[component.joystickType] = if (component.isVertical) { + Pair(current.first, 0f) + } else { + Pair(0f, current.second) + } + } + manager.getOutButtonsForAxis(zeroPair).forEach { outButton -> + NativeLibrary.onGamePadEvent( + device.descriptor, + outButton, + ButtonState.RELEASED + ) + } + } continue } - if (value > 0f && value < 0.1f || value < 0f && value > -0.1f) { - // Skip joystick wobble - value = 0f + // Axis to Axis mappings + manager.getOutAxesForAxis(axisPair).forEach { (outAxis, outDir) -> + val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach + val current = + stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f)) + val contribution = abs(value) * outDir + stickAccumulator[component.joystickType] = if (component.isVertical) { + Pair(current.first, contribution) + } else { + Pair(contribution, current.second) + } } - if (inverted) value = -value; - when (nextMapping) { - NativeLibrary.ButtonType.STICK_LEFT -> { - axisValuesCirclePad[guestOrientation] = value - } - - NativeLibrary.ButtonType.STICK_C -> { - axisValuesCStick[guestOrientation] = value - } - - NativeLibrary.ButtonType.DPAD -> { - axisValuesDPad[guestOrientation] = value - } - - NativeLibrary.ButtonType.TRIGGER_L -> { - isTriggerPressedLMapped = true - isTriggerPressedL = value != 0f - } - - NativeLibrary.ButtonType.TRIGGER_R -> { - isTriggerPressedRMapped = true - isTriggerPressedR = value != 0f - } - - NativeLibrary.ButtonType.BUTTON_ZL -> { - isTriggerPressedZLMapped = true - isTriggerPressedZL = value != 0f - } - - NativeLibrary.ButtonType.BUTTON_ZR -> { - isTriggerPressedZRMapped = true - isTriggerPressedZR = value != 0f + // Axis to Button mappings + manager.getOutButtonsForAxis(axisPair).forEach { button -> + val mapping = manager.getMappingForButton(button) + if (abs(value) > (mapping?.threshold ?: 0.5f)) { + hotkeyUtility.handleKeyPress(button, device.descriptor) + NativeLibrary.onGamePadEvent(device.descriptor, button, ButtonState.PRESSED) + } else { + NativeLibrary.onGamePadEvent( + device.descriptor, + button, + ButtonState.RELEASED + ) } } } - // Circle-Pad and C-Stick status - NativeLibrary.onGamePadMoveEvent( - input.descriptor, - NativeLibrary.ButtonType.STICK_LEFT, - axisValuesCirclePad[0], - axisValuesCirclePad[1] - ) - NativeLibrary.onGamePadMoveEvent( - input.descriptor, - NativeLibrary.ButtonType.STICK_C, - axisValuesCStick[0], - axisValuesCStick[1] - ) - - // Triggers L/R and ZL/ZR - if (isTriggerPressedLMapped) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.TRIGGER_L, - if (isTriggerPressedL) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - ) - } - if (isTriggerPressedRMapped) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.TRIGGER_R, - if (isTriggerPressedR) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - ) - } - if (isTriggerPressedZLMapped) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.BUTTON_ZL, - if (isTriggerPressedZL) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - ) - } - if (isTriggerPressedZRMapped) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.BUTTON_ZR, - if (isTriggerPressedZR) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - ) + stickAccumulator.forEach { (outAxis, value) -> + NativeLibrary.onGamePadMoveEvent(device.descriptor, outAxis, value.first, value.second) } - // Work-around to allow D-pad axis to be bound to emulated buttons - if (axisValuesDPad[0] == 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_LEFT, - NativeLibrary.ButtonState.RELEASED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_RIGHT, - NativeLibrary.ButtonState.RELEASED - ) - } - if (axisValuesDPad[0] < 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_LEFT, - NativeLibrary.ButtonState.PRESSED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_RIGHT, - NativeLibrary.ButtonState.RELEASED - ) - } - if (axisValuesDPad[0] > 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_LEFT, - NativeLibrary.ButtonState.RELEASED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_RIGHT, - NativeLibrary.ButtonState.PRESSED - ) - } - if (axisValuesDPad[1] == 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_UP, - NativeLibrary.ButtonState.RELEASED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_DOWN, - NativeLibrary.ButtonState.RELEASED - ) - } - if (axisValuesDPad[1] < 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_UP, - NativeLibrary.ButtonState.PRESSED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_DOWN, - NativeLibrary.ButtonState.RELEASED - ) - } - if (axisValuesDPad[1] > 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_UP, - NativeLibrary.ButtonState.RELEASED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_DOWN, - NativeLibrary.ButtonState.PRESSED - ) - } return true } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/input/GamepadHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/GamepadHelper.kt new file mode 100644 index 000000000..ed32d9988 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/GamepadHelper.kt @@ -0,0 +1,374 @@ +package org.citra.citra_emu.features.input + +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.R +import org.citra.citra_emu.features.settings.model.InputMappingSetting +import org.citra.citra_emu.features.settings.model.Settings + +object GamepadHelper { + private const val BUTTON_NAME_L3 = "Button L3" + private const val BUTTON_NAME_R3 = "Button R3" + private const val NINTENDO_VENDOR_ID = 0x057e + + // Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as + // KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't + // translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to + // the scan code in that case. + private const val LINUX_BTN_DPAD_UP = 0x220 // 544 + private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545 + private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546 + private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547 + + + private val buttonNameOverrides = mapOf( + KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3, + KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3, + LINUX_BTN_DPAD_UP to "Dpad Up", + LINUX_BTN_DPAD_DOWN to "Dpad Down", + LINUX_BTN_DPAD_LEFT to "Dpad Left", + LINUX_BTN_DPAD_RIGHT to "Dpad Right" + ) + + fun getButtonName(keyCode: Int): String = + buttonNameOverrides[keyCode] + ?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_")) + + fun getAxisName(axis: Int, direction: Int?): String = "Axis " + axis + direction?.let { if(it > 0) "+" else "-" } + + private fun toTitleCase(raw: String): String = + raw.replace("_", " ").lowercase() + .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + + private data class DefaultButtonMapping( + val setting: InputMappingSetting, + val hostKeyCode: Int + ) + // Auto-map always sets inverted = false. Users needing inverted axes should remap manually. + private data class DefaultAxisMapping( + val setting: InputMappingSetting, + val hostAxis: Int, + val hostDirection: Int + ) + + private val xboxFaceButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_B), + DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_A), + DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y), + DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X) + ) + + private val nintendoFaceButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_A), + DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_B), + DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_X), + DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y) + ) + + private val commonButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1), + DefaultButtonMapping(InputMappingSetting.BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1), + DefaultButtonMapping(InputMappingSetting.BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2), + DefaultButtonMapping(InputMappingSetting.BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2), + DefaultButtonMapping(InputMappingSetting.BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT), + DefaultButtonMapping(InputMappingSetting.BUTTON_START, KeyEvent.KEYCODE_BUTTON_START) + ) + + private val dpadButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.DPAD_UP, KeyEvent.KEYCODE_DPAD_UP), + DefaultButtonMapping(InputMappingSetting.DPAD_DOWN, KeyEvent.KEYCODE_DPAD_DOWN), + DefaultButtonMapping(InputMappingSetting.DPAD_LEFT, KeyEvent.KEYCODE_DPAD_LEFT), + DefaultButtonMapping(InputMappingSetting.DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT) + ) + + private val stickAxisMappings = listOf( + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_LEFT, MotionEvent.AXIS_X, -1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_RIGHT, MotionEvent.AXIS_X, 1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_UP, MotionEvent.AXIS_Y, -1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_DOWN, MotionEvent.AXIS_Y, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_LEFT, MotionEvent.AXIS_Z, -1), + DefaultAxisMapping(InputMappingSetting.CSTICK_RIGHT, MotionEvent.AXIS_Z, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_UP, MotionEvent.AXIS_RZ, -1), + DefaultAxisMapping(InputMappingSetting.CSTICK_DOWN, MotionEvent.AXIS_RZ, 1) + ) + + private val dpadAxisMappings = listOf( + DefaultAxisMapping(InputMappingSetting.DPAD_UP, MotionEvent.AXIS_HAT_Y, -1), + DefaultAxisMapping(InputMappingSetting.DPAD_DOWN, MotionEvent.AXIS_HAT_Y, 1), + DefaultAxisMapping(InputMappingSetting.DPAD_LEFT, MotionEvent.AXIS_HAT_X, -1), + DefaultAxisMapping(InputMappingSetting.DPAD_RIGHT, MotionEvent.AXIS_HAT_X, 1) + ) + + // Nintendo Switch Joy-Con specific mappings. + // Joy-Cons connected via Bluetooth on Android have several quirks: + // - They register as two separate InputDevices (left and right) + // - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A) + // but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y) + // - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes + // - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ + + // Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not. + // This is different from both the standard Xbox table (full swap) and the + // Nintendo table (no swap). + private val joyconFaceButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_B), + DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_A), + DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_X), + DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y) + ) + + // Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN + private val joyconDpadButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.DPAD_UP, LINUX_BTN_DPAD_UP), + DefaultButtonMapping(InputMappingSetting.DPAD_DOWN, LINUX_BTN_DPAD_DOWN), + DefaultButtonMapping(InputMappingSetting.DPAD_LEFT, LINUX_BTN_DPAD_LEFT), + DefaultButtonMapping(InputMappingSetting.DPAD_RIGHT, LINUX_BTN_DPAD_RIGHT) + ) + + // Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY + // (not Z/RZ like most controllers). The horizontal axis is inverted relative to + // the standard orientation - verified empirically on paired Joy-Cons via Bluetooth. + private val joyconStickAxisMappings = listOf( + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_UP, MotionEvent.AXIS_Y, -1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_DOWN, MotionEvent.AXIS_Y, 1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_LEFT, MotionEvent.AXIS_X, -1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_RIGHT, MotionEvent.AXIS_X, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_UP, MotionEvent.AXIS_RY, -1), + DefaultAxisMapping(InputMappingSetting.CSTICK_DOWN, MotionEvent.AXIS_RY, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_LEFT, MotionEvent.AXIS_RX, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_RIGHT, MotionEvent.AXIS_RX, -1) + ) + + /** + * Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a + * Pro Controller or other Nintendo device) by checking vendor ID + device + * capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the + * right stick, while the Pro Controller has standard HAT axes and Z/RZ. + */ + fun isJoyCon(device: InputDevice?): Boolean { + if (device == null) return false + if (device.vendorId != NINTENDO_VENDOR_ID) return false + + // Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick). + // Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ. + var hasHatAxes = false + var hasStandardRightStick = false + for (range in device.motionRanges) { + when (range.axis) { + MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true + MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true + } + } + return !hasHatAxes && !hasStandardRightStick + } + + fun clearAllBindings(settings: Settings) { + InputMappingSetting.values().forEach { settings.set(it, Input()) } + } + + private fun applyBindings( + settings: Settings, + buttonMappings: List, + axisMappings: List + ) { + + buttonMappings.forEach { mapping -> + settings.set(mapping.setting, Input(key = mapping.hostKeyCode)) + } + axisMappings.forEach { mapping -> + settings.set( + mapping.setting, + Input( + axis = mapping.hostAxis, + direction = mapping.hostDirection + ) + ) + } + } + + /** + * Applies Joy-Con specific bindings: scan code D-pad, partial face button + * swap, and AXIS_RX/RY right stick. + */ + fun applyJoyConBindings(settings: Settings) { + applyBindings( + settings, + joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings, + joyconStickAxisMappings + ) + } + + /** + * Applies auto-mapped bindings based on detected controller layout and d-pad type. + * + * @param isNintendoLayout true if the controller uses Nintendo face button layout + * (A=east, B=south), false for Xbox layout (A=south, B=east) + * @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y), + * false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT) + */ + fun applyAutoMapBindings(settings: Settings, isNintendoLayout: Boolean, useAxisDpad: Boolean) { + val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings + val buttonMappings = if (useAxisDpad) { + faceButtons + commonButtonMappings + } else { + faceButtons + commonButtonMappings + dpadButtonMappings + } + val axisMappings = if (useAxisDpad) { + stickAxisMappings + dpadAxisMappings + } else { + stickAxisMappings + } + applyBindings(settings,buttonMappings, axisMappings) + } + + /** + * Some controllers report extra button presses that can be ignored. + */ + fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean { + return if (isDualShock4(inputDevice)) { + // The two analog triggers generate analog motion events as well as a keycode. + // We always prefer to use the analog values, so throw away the button press + keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2 + } else false + } + + /** + * Scale an axis to be zero-centered with a proper range. + */ + fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float { + if (isDualShock4(inputDevice)) { + // Android doesn't have correct mappings for this controller's triggers. It reports them + // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] + // Scale them to properly zero-centered with a range of [0.0, 1.0]. + if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { + return (value + 1) / 2.0f + } + } else if (isXboxOneWireless(inputDevice)) { + // Same as the DualShock 4, the mappings are missing. + if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { + return (value + 1) / 2.0f + } + if (axis == MotionEvent.AXIS_GENERIC_1) { + // This axis is stuck at ~.5. Ignore it. + return 0.0f + } + } else if (isMogaPro2Hid(inputDevice)) { + // This controller has a broken axis that reports a constant value. Ignore it. + if (axis == MotionEvent.AXIS_GENERIC_1) { + return 0.0f + } + } + return value + } + + private fun isDualShock4(inputDevice: InputDevice): Boolean { + // Sony DualShock 4 controller + return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc + } + + private fun isXboxOneWireless(inputDevice: InputDevice): Boolean { + // Microsoft Xbox One controller + return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0 + } + + private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean { + // Moga Pro 2 HID + return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271 + } + + data class JoystickComponent(val joystickType: Int, val isVertical: Boolean) + + fun getJoystickComponent(buttonType: Int): JoystickComponent? = when (buttonType) { + NativeLibrary.ButtonType.STICK_LEFT_UP, + NativeLibrary.ButtonType.STICK_LEFT_DOWN -> JoystickComponent(NativeLibrary.ButtonType.STICK_LEFT, true) + NativeLibrary.ButtonType.STICK_LEFT_LEFT, + NativeLibrary.ButtonType.STICK_LEFT_RIGHT -> JoystickComponent(NativeLibrary.ButtonType.STICK_LEFT, false) + NativeLibrary.ButtonType.STICK_C_UP, + NativeLibrary.ButtonType.STICK_C_DOWN -> JoystickComponent(NativeLibrary.ButtonType.STICK_C, true) + NativeLibrary.ButtonType.STICK_C_LEFT, + NativeLibrary.ButtonType.STICK_C_RIGHT -> JoystickComponent(NativeLibrary.ButtonType.STICK_C, false) + else -> null + } + + val buttonKeys = listOf( + InputMappingSetting.BUTTON_A, + InputMappingSetting.BUTTON_B, + InputMappingSetting.BUTTON_X, + InputMappingSetting.BUTTON_Y, + InputMappingSetting.BUTTON_SELECT, + InputMappingSetting.BUTTON_START, + InputMappingSetting.BUTTON_HOME + ) + val buttonTitles = listOf( + R.string.button_a, + R.string.button_b, + R.string.button_x, + R.string.button_y, + R.string.button_select, + R.string.button_start, + R.string.button_home + ) + val circlePadKeys = listOf( + InputMappingSetting.CIRCLEPAD_UP, + InputMappingSetting.CIRCLEPAD_DOWN, + InputMappingSetting.CIRCLEPAD_LEFT, + InputMappingSetting.CIRCLEPAD_RIGHT + ) + val cStickKeys = listOf( + InputMappingSetting.CSTICK_UP, + InputMappingSetting.CSTICK_DOWN, + InputMappingSetting.CSTICK_LEFT, + InputMappingSetting.CSTICK_RIGHT + ) + + val dPadButtonKeys = listOf( + InputMappingSetting.DPAD_UP, + InputMappingSetting.DPAD_DOWN, + InputMappingSetting.DPAD_LEFT, + InputMappingSetting.DPAD_RIGHT + ) + + val dPadTitles = listOf( + R.string.direction_up, + R.string.direction_down, + R.string.direction_left, + R.string.direction_right + ) + val axisTitles = dPadTitles + + val triggerKeys = listOf( + InputMappingSetting.BUTTON_L, + InputMappingSetting.BUTTON_R, + InputMappingSetting.BUTTON_ZL, + InputMappingSetting.BUTTON_ZR + ) + val triggerTitles = listOf( + R.string.button_l, + R.string.button_r, + R.string.button_zl, + R.string.button_zr + ) + val hotKeys = listOf( + InputMappingSetting.HOTKEY_ENABLE, + InputMappingSetting.HOTKEY_SWAP, + InputMappingSetting.HOTKEY_CYCLE_LAYOUT, + InputMappingSetting.HOTKEY_CLOSE_GAME, + InputMappingSetting.HOTKEY_PAUSE_OR_RESUME, + InputMappingSetting.HOTKEY_QUICKSAVE, + InputMappingSetting.HOTKEY_QUICKLOAD, + InputMappingSetting.HOTKEY_TURBO_LIMIT + ) + val hotkeyTitles = listOf( + R.string.controller_hotkey_enable_button, + R.string.emulation_swap_screens, + R.string.emulation_cycle_landscape_layouts, + R.string.emulation_close_game, + R.string.emulation_toggle_pause, + R.string.emulation_quicksave, + R.string.emulation_quickload, + R.string.turbo_limit_hotkey + ) + +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/Hotkey.kt similarity index 89% rename from src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt rename to src/android/app/src/main/java/org/citra/citra_emu/features/input/Hotkey.kt index e2319a7e4..c347659f0 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/Hotkey.kt @@ -2,7 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -package org.citra.citra_emu.features.hotkeys +package org.citra.citra_emu.features.input enum class Hotkey(val button: Int) { SWAP_SCREEN(10001), diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/HotkeyUtility.kt similarity index 63% rename from src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt rename to src/android/app/src/main/java/org/citra/citra_emu/features/input/HotkeyUtility.kt index d57ff5e8b..72e0ffd49 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/HotkeyUtility.kt @@ -2,7 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -package org.citra.citra_emu.features.hotkeys +package org.citra.citra_emu.features.input import android.content.Context import android.view.KeyEvent @@ -16,6 +16,7 @@ import org.citra.citra_emu.utils.TurboHelper import org.citra.citra_emu.display.ScreenAdjustmentUtil import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.features.settings.model.Settings +import kotlin.math.abs class HotkeyUtility( private val screenAdjustmentUtil: ScreenAdjustmentUtil, @@ -27,17 +28,18 @@ class HotkeyUtility( private var hotkeyIsEnabled = false var hotkeyIsPressed = false private val currentlyPressedButtons = mutableSetOf() + /** Store which axis directions are currently pressed as (axis, direction) pairs. */ + private val pressedAxisDirections = HashSet>() // (outAxis, outDir) - fun handleKeyPress(keyEvent: KeyEvent): Boolean { + fun handleKeyPress(key: Int, descriptor: String): Boolean { var handled = false - val buttonSet = InputBindingSetting.getButtonSet(keyEvent) - val enableButton = - PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - .getString(Settings.HOTKEY_ENABLE, "") + val buttonSet = settings.inputMappingManager.getOutButtonsForKey(key) + val axisSet = settings.inputMappingManager.getOutAxesForKey(key) + val enableButtonMapped = settings.inputMappingManager.getMappingForButton(Hotkey.ENABLE.button) != null val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button) val thisKeyIsHotkey = !thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) } - hotkeyIsEnabled = hotkeyIsEnabled || enableButton == "" || thisKeyIsEnableButton + hotkeyIsEnabled = hotkeyIsEnabled || !enableButtonMapped || thisKeyIsEnableButton // Now process all internal buttons associated with this keypress for (button in buttonSet) { @@ -58,19 +60,23 @@ class HotkeyUtility( // the normal key event. if (!thisKeyIsHotkey || !hotkeyIsEnabled) { handled = NativeLibrary.onGamePadEvent( - keyEvent.device.descriptor, + descriptor, button, NativeLibrary.ButtonState.PRESSED ) || handled } } } + // Handle axes in helper functions + updateAxisStateForKey(axisSet,true) + handled = sendAxisState(descriptor, axisSet) || handled return handled } - fun handleKeyRelease(keyEvent: KeyEvent): Boolean { + fun handleKeyRelease(key: Int, descriptor: String): Boolean { var handled = false - val buttonSet = InputBindingSetting.getButtonSet(keyEvent) + val buttonSet = settings.inputMappingManager.getOutButtonsForKey(key) + val axisSet = settings.inputMappingManager.getOutAxesForKey(key) val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button) val thisKeyIsHotkey = !thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) } @@ -96,7 +102,7 @@ class HotkeyUtility( ) ) { handled = NativeLibrary.onGamePadEvent( - keyEvent.device.descriptor, + descriptor, button, NativeLibrary.ButtonState.RELEASED ) || handled @@ -104,6 +110,8 @@ class HotkeyUtility( } } } + updateAxisStateForKey(axisSet,false) + handled = sendAxisState(descriptor, axisSet) || handled return handled } @@ -142,4 +150,42 @@ class HotkeyUtility( hotkeyIsPressed = true return true } + + private fun updateAxisStateForKey(axisSet: List>, pressed: Boolean) { + axisSet.forEach { (outAxis, outDir) -> + if (pressed) pressedAxisDirections.add(Pair(outAxis, outDir)) + else pressedAxisDirections.remove(Pair(outAxis, outDir)) + } + } + + /** + * Update axis state based on currently pressed buttons + */ + private fun sendAxisState(descriptor: String, affectedAxes: List>): Boolean { + val stickAccumulator = HashMap>() + // to make sure that when both directions are released, we still get a return to 0, but only for + // axes that have been touched by buttons + affectedAxes.forEach { (outAxis, outDir) -> + val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach + stickAccumulator.putIfAbsent(component.joystickType, Pair(0f,0f)) + } + + pressedAxisDirections.forEach{ (outAxis, outDir) -> + val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach + val current = + stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f)) + // if opposite directions of the same stick are held, this will let them cancel each other out + stickAccumulator[component.joystickType] = if (component.isVertical) { + Pair(current.first, current.second + outDir) + } else { + Pair(current.first + outDir, current.second) + } + } + var handled = false + stickAccumulator.forEach { (joystickType, value) -> + handled = NativeLibrary.onGamePadMoveEvent(descriptor, joystickType, value.first, value.second) || handled + } + return handled + + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/input/Input.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/Input.kt new file mode 100644 index 000000000..f2b4b03be --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/Input.kt @@ -0,0 +1,16 @@ +package org.citra.citra_emu.features.input + +/** + * Stores information about a particular input + */ +data class Input( + val key: Int? = null, + val axis: Int? = null, + // +1 or -1 + val direction: Int? = null, + val threshold: Float? = null +) { + val empty: Boolean + get() = key == null && axis == null + +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/input/InputMappingManager.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/InputMappingManager.kt new file mode 100644 index 000000000..db676dd6f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/InputMappingManager.kt @@ -0,0 +1,149 @@ +package org.citra.citra_emu.features.input + +import org.citra.citra_emu.features.settings.model.InputMappingSetting +import org.citra.citra_emu.features.settings.model.Settings + +class InputMappingManager() { + + /** + * input keys are represented by a single int, and can be mapped to 3ds buttons or axis directions + * input axes are represented by an pair, where direction is either 1 or -1 + * and can be mapped to output axes or an output button + * output keys are a single int + * output axes are also a pair + * + * For now, we are only allowing one output *axis* per input, but the code is designed to change + * that if necessary. There can be more than on output *button* per input because hotkeys are treated + * as buttons and it is possible to map an input to both a 3ds button and a hotkey. + */ + + private val keyToOutButtons = HashMap>() + private val keyToOutAxes = HashMap>>() + + private val axisToOutAxes = HashMap, MutableList>>() + private val axisToOutButtons = HashMap, MutableList>() + + private val outAxisToMapping = HashMap, Input>() + private val buttonToMapping = HashMap() + + + /** Rebuilds the input maps from the given settings instance */ + fun rebuild(settings: Settings) { + clear() + InputMappingSetting.values().forEach { setting -> + val mapping = settings.get(setting) ?: return@forEach + register(setting, mapping) + } + } + + fun clear() { + axisToOutButtons.clear() + buttonToMapping.clear() + axisToOutAxes.clear() + outAxisToMapping.clear() + keyToOutButtons.clear() + keyToOutAxes.clear() + } + + /** Rebind a particular setting */ + fun rebind(setting: InputMappingSetting, newMapping: Input?) { + clearMapping(setting) + if (newMapping != null) register(setting, newMapping) + } + + /** Clear a mapping from all hashmaps */ + fun clearMapping(setting: InputMappingSetting) { + val outPair = if (setting.outAxis != null && setting.outDirection != null) Pair( + setting.outAxis, + setting.outDirection + ) else null + + val oldMapping = if (setting.outKey != null) { + buttonToMapping.get(setting.outKey) + } else if (setting.outAxis != null && setting.outDirection != null) { + outAxisToMapping.get(Pair(setting.outAxis, setting.outDirection)) + } else { + null + } + + val oldPair = if (oldMapping?.axis != null && oldMapping?.direction != null) Pair( + oldMapping.axis, + oldMapping.direction + ) else null + + // if our old mapping was a key, remove its binds + if (oldMapping?.key != null) { + keyToOutButtons[oldMapping.key]?.remove(setting.outKey) + if (outPair != null) { + keyToOutAxes[oldMapping.key]?.remove(outPair) + } + } + // if our old mapping was an axis, remove its binds + if (oldPair != null) { + if (setting.outAxis != null && setting.outDirection != null) + axisToOutAxes[oldPair]?.remove(outPair) + if (setting.outKey != null) + axisToOutButtons[oldPair]?.remove(setting.outKey) + } + + // remove the reverse binds + if (setting.outKey != null) { + buttonToMapping.remove(setting.outKey) + } + if (outPair != null) { + outAxisToMapping.remove(outPair) + } + } + + /** + * Add a single item to the maps based on the value of the InputMapping and the InputMappingSetting + */ + private fun register(setting: InputMappingSetting, mapping: Input) { + val outPair = if (setting.outAxis != null && setting.outDirection != null) Pair( + setting.outAxis, + setting.outDirection + ) else null + + val inPair = if (mapping.axis != null && mapping.direction != null) Pair( + mapping.axis, + mapping.direction + ) else null + + if (setting.outKey != null) { + if (mapping.key != null) + keyToOutButtons.getOrPut(mapping.key, { mutableListOf() }).add(setting.outKey) + if (inPair != null) { + if (setting.outKey != null) + axisToOutButtons.getOrPut(inPair, { mutableListOf() }).add(setting.outKey) + } + buttonToMapping[setting.outKey] = mapping + } + if (outPair != null) { + if (mapping.key != null) + keyToOutAxes.getOrPut(mapping.key, { mutableListOf() }) + .add(outPair) + if (inPair != null) + axisToOutAxes.getOrPut(inPair, { mutableListOf() }) + .add(outPair) + outAxisToMapping[outPair] = mapping + } + } + + fun getOutAxesForAxis(pair: Pair): List> = + axisToOutAxes[pair] ?: emptyList() + + fun getOutButtonsForAxis(pair: Pair): List = + axisToOutButtons[pair] ?: emptyList() + + fun getMappingForOutAxis(pair: Pair): Input? = + outAxisToMapping[pair] + + fun getOutButtonsForKey(keyCode: Int): List = + keyToOutButtons[keyCode] ?: emptyList() + + fun getOutAxesForKey(keyCode: Int): List> = + keyToOutAxes[keyCode] ?: emptyList() + + fun getMappingForButton(outKey: Int): Input? = + buttonToMapping[outKey] +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt index 1d6e0dcee..6280455a9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt @@ -138,4 +138,38 @@ object SettingKeys { external fun android_hide_images(): String external fun screen_orientation(): String external fun performance_overlay_position(): String + + external fun button_a(): String + external fun button_b(): String + external fun button_x(): String + external fun button_y(): String + external fun button_home(): String + external fun circlepad_up(): String + external fun circlepad_down(): String + external fun circlepad_left(): String + external fun circlepad_right(): String + external fun button_r(): String + external fun button_l(): String + external fun button_zr(): String + external fun button_zl(): String + external fun button_start(): String + external fun button_select(): String + external fun dpad_up(): String + external fun dpad_down(): String + external fun dpad_left(): String + external fun dpad_right(): String + external fun cstick_up(): String + external fun cstick_down(): String + external fun cstick_left(): String + external fun cstick_right(): String + external fun hotkey_cycle_layout(): String + external fun hotkey_close(): String + external fun hotkey_swap(): String + external fun hotkey_pause_resume(): String + external fun hotkey_quicksave(): String + external fun hotkey_turbo_limit(): String + external fun hotkey_quickload(): String + external fun hotkey_enable(): String + + } \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/InputMappingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/InputMappingSetting.kt new file mode 100644 index 000000000..24dd03073 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/InputMappingSetting.kt @@ -0,0 +1,221 @@ +// 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.model + +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.features.input.Input +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.input.Hotkey +import org.citra.citra_emu.features.settings.SettingKeys + +enum class InputMappingSetting( + override val key: String, + override val section: String, + override val defaultValue: Input = Input(), + val outKey: Int? = null, + val outAxis: Int? = null, + val outDirection: Int? = null +) : AbstractSetting { + BUTTON_A( + SettingKeys.button_a(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_A + ), + BUTTON_B( + SettingKeys.button_b(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_B + ), + BUTTON_X( + SettingKeys.button_x(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_X + ), + BUTTON_Y( + SettingKeys.button_y(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_Y + ), + BUTTON_HOME( + SettingKeys.button_home(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_HOME + ), + BUTTON_L( + SettingKeys.button_l(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.TRIGGER_L + ), + BUTTON_R( + SettingKeys.button_r(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.TRIGGER_R + ), + BUTTON_SELECT( + SettingKeys.button_select(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_SELECT + ), + BUTTON_START( + SettingKeys.button_start(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_START + ), + BUTTON_ZL( + SettingKeys.button_zl(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_ZL + ), + BUTTON_ZR( + SettingKeys.button_zr(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_ZR + ), + DPAD_UP( + SettingKeys.dpad_up(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.DPAD_UP + ), + DPAD_DOWN( + SettingKeys.dpad_down(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.DPAD_DOWN + ), + DPAD_LEFT( + SettingKeys.dpad_left(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.DPAD_LEFT + ), + DPAD_RIGHT( + SettingKeys.dpad_right(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.DPAD_RIGHT + ), + + CIRCLEPAD_UP( + SettingKeys.circlepad_up(), + Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_LEFT_UP, + outDirection = -1 + ), + CIRCLEPAD_DOWN( + SettingKeys.circlepad_down(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_LEFT_DOWN, outDirection = 1 + ), + CIRCLEPAD_LEFT( + SettingKeys.circlepad_left(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_LEFT_LEFT, outDirection = -1 + ), + CIRCLEPAD_RIGHT( + SettingKeys.circlepad_right(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_LEFT_RIGHT, outDirection = 1 + ), + CSTICK_UP( + SettingKeys.cstick_up(), + Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_C_UP, + outDirection = -1 + ), + CSTICK_DOWN( + SettingKeys.cstick_down(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_C_DOWN, outDirection = 1 + ), + CSTICK_LEFT( + SettingKeys.cstick_left(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_C_LEFT, outDirection = -1 + ), + CSTICK_RIGHT( + SettingKeys.cstick_right(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_C_RIGHT, outDirection = 1 + ), + HOTKEY_CYCLE_LAYOUT( + SettingKeys.hotkey_cycle_layout(), Settings.SECTION_CONTROLS, + outKey = Hotkey.CYCLE_LAYOUT.button + ), + HOTKEY_CLOSE_GAME( + SettingKeys.hotkey_close(), Settings.SECTION_CONTROLS, + outKey = Hotkey.CLOSE_GAME.button + ), + HOTKEY_SWAP( + SettingKeys.hotkey_swap(), Settings.SECTION_CONTROLS, + outKey = Hotkey.SWAP_SCREEN.button + ), + HOTKEY_PAUSE_OR_RESUME( + SettingKeys.hotkey_pause_resume(), Settings.SECTION_CONTROLS, + outKey = Hotkey.PAUSE_OR_RESUME.button + ), + HOTKEY_QUICKSAVE( + SettingKeys.hotkey_quicksave(), Settings.SECTION_CONTROLS, + outKey = Hotkey.QUICKSAVE.button + ), + HOTKEY_TURBO_LIMIT( + SettingKeys.hotkey_turbo_limit(), Settings.SECTION_CONTROLS, + outKey = Hotkey.TURBO_LIMIT.button + ), + HOTKEY_QUICKLOAD( + SettingKeys.hotkey_quickload(), Settings.SECTION_CONTROLS, + outKey = Hotkey.QUICKLOAD.button + ), + HOTKEY_ENABLE( + SettingKeys.hotkey_enable(), Settings.SECTION_CONTROLS, + outKey = Hotkey.ENABLE.button + ); + + + /** Parse a configuration string into an input binding */ + override fun valueFromString(string: String): Input { + if (string.isBlank()) return defaultValue + + val params = string.split(",") + .mapNotNull { part -> + val split = part.split(":", limit = 2) + if (split.size < 2) null else split[0] to split[1] + }.toMap() + if (params["engine"] != "gamepad") return defaultValue + return Input( + key = params["code"]?.toIntOrNull(), + axis = params["axis"]?.toIntOrNull(), + direction = params["direction"]?.toIntOrNull(), + threshold = params["threshold"]?.toFloatOrNull(), + ).takeIf { it.key != null || it.axis != null } ?: defaultValue + } + + + /** Create a configuration string from an input binding */ + override fun valueToString(binding: Input?): String { + binding ?: return "" + if (binding.empty) return "" + val ret = "engine:gamepad" + return ret + when { + binding.key != null -> ",code:${binding.key}" + binding.axis != null -> buildString { + append(",axis:${binding.axis}") + binding.threshold?.let { append(",threshold:$it") } + binding.direction?.let { append(",direction:$it") } + } + + else -> "" + } + } + + /** What will display is different from what is saved in this case */ + fun displayValue(binding: Input?): String { + if (binding?.key != null) { + return GamepadHelper.getButtonName(binding.key) + } else if (binding?.axis != null) { + return GamepadHelper.getAxisName(binding.axis, binding.direction) + } else { + return "" + } + } + + override val isRuntimeEditable: Boolean = true + + companion object { + + fun from(key: String): InputMappingSetting? = values().firstOrNull { it.key == key } + + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index c2b54d12e..7c0647760 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -4,12 +4,16 @@ package org.citra.citra_emu.features.settings.model -import org.citra.citra_emu.R +import org.citra.citra_emu.features.input.Input +import org.citra.citra_emu.features.input.InputMappingManager +import org.citra.citra_emu.features.settings.utils.SettingsFile class Settings { private val globalValues = HashMap() private val perGameOverrides = HashMap() + val inputMappingManager = InputMappingManager() + var gameId: String? = null fun isPerGame(): Boolean = gameId != null && gameId != "" @@ -26,24 +30,33 @@ class Settings { return (globalValues[setting.key] ?: setting.defaultValue) as T } + /** Sets the global value specifically */ fun setGlobal(setting: AbstractSetting, value: T) { globalValues[setting.key] = value as Any + // only update the InputMapping if this global setting actually is in effect now + if (setting is InputMappingSetting && !hasOverride(setting)) { + inputMappingManager.rebind(setting, value as? Input) + } } + /** Sets the override specifically */ fun setOverride(setting: AbstractSetting, value: T) { perGameOverrides[setting.key] = value as Any + if (setting is InputMappingSetting) { + inputMappingManager.rebind(setting, value as? Input) + } } /** Sets the per-game or global setting based on whether this file has ANY per-game setting. - * This should be used, for example, by the Settings Activity + * This should be used by the Custom Settings Activity */ fun set(setting: AbstractSetting, value: T) { if (isPerGame()) setOverride(setting, value) else setGlobal(setting, value) } /** - * Updates an existing setting honoring whether it is *currently* global or local. This will - * be used by the Quick Menu + * Updates an existing setting honoring whether this particular setting is *currently* global or local. + * This should be used by the Quick Menu */ fun update(setting: AbstractSetting, value: T) { if (hasOverride(setting)) setOverride(setting, value) else setGlobal(setting, value) @@ -61,10 +74,15 @@ class Settings { other.perGameOverrides.forEach{ (key, value) -> perGameOverrides[key] = value } + + inputMappingManager.rebuild(this) } fun clearOverride(setting: AbstractSetting) { perGameOverrides.remove(setting.key) + if (setting is InputMappingSetting) { + inputMappingManager.rebind(setting, getGlobal(setting)) + } } fun hasOverride(setting: AbstractSetting<*>): Boolean { @@ -78,10 +96,12 @@ class Settings { fun clearAll() { globalValues.clear() perGameOverrides.clear() + inputMappingManager.clear() } fun clearOverrides() { perGameOverrides.clear() + inputMappingManager.rebuild(this) } fun removePerGameSettings() { @@ -91,7 +111,6 @@ class Settings { companion object { - const val SECTION_CORE = "Core" const val SECTION_SYSTEM = "System" const val SECTION_CAMERA = "Camera" @@ -108,115 +127,6 @@ class Settings { const val SECTION_STORAGE = "Storage" const val SECTION_MISC = "Miscellaneous" - const val KEY_BUTTON_A = "button_a" - const val KEY_BUTTON_B = "button_b" - const val KEY_BUTTON_X = "button_x" - const val KEY_BUTTON_Y = "button_y" - const val KEY_BUTTON_SELECT = "button_select" - const val KEY_BUTTON_START = "button_start" - const val KEY_BUTTON_HOME = "button_home" - const val KEY_BUTTON_UP = "button_up" - const val KEY_BUTTON_DOWN = "button_down" - const val KEY_BUTTON_LEFT = "button_left" - const val KEY_BUTTON_RIGHT = "button_right" - const val KEY_BUTTON_L = "button_l" - const val KEY_BUTTON_R = "button_r" - const val KEY_BUTTON_ZL = "button_zl" - const val KEY_BUTTON_ZR = "button_zr" - const val KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical" - const val KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal" - const val KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical" - const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal" - const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical" - const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal" - const val HOTKEY_ENABLE = "hotkey_enable" - const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap" - const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout" - const val HOTKEY_CLOSE_GAME = "hotkey_close_game" - const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game" - const val HOTKEY_QUICKSAVE = "hotkey_quickload" - const val HOTKEY_QUICKlOAD = "hotkey_quickpause" - const val HOTKEY_TURBO_LIMIT = "hotkey_turbo_limit" - - val buttonKeys = listOf( - KEY_BUTTON_A, - KEY_BUTTON_B, - KEY_BUTTON_X, - KEY_BUTTON_Y, - KEY_BUTTON_SELECT, - KEY_BUTTON_START, - KEY_BUTTON_HOME - ) - val buttonTitles = listOf( - R.string.button_a, - R.string.button_b, - R.string.button_x, - R.string.button_y, - R.string.button_select, - R.string.button_start, - R.string.button_home - ) - val circlePadKeys = listOf( - KEY_CIRCLEPAD_AXIS_VERTICAL, - KEY_CIRCLEPAD_AXIS_HORIZONTAL - ) - val cStickKeys = listOf( - KEY_CSTICK_AXIS_VERTICAL, - KEY_CSTICK_AXIS_HORIZONTAL - ) - val dPadAxisKeys = listOf( - KEY_DPAD_AXIS_VERTICAL, - KEY_DPAD_AXIS_HORIZONTAL - ) - val dPadButtonKeys = listOf( - KEY_BUTTON_UP, - KEY_BUTTON_DOWN, - KEY_BUTTON_LEFT, - KEY_BUTTON_RIGHT - ) - val axisTitles = listOf( - R.string.controller_axis_vertical, - R.string.controller_axis_horizontal - ) - val dPadTitles = listOf( - R.string.direction_up, - R.string.direction_down, - R.string.direction_left, - R.string.direction_right - ) - val triggerKeys = listOf( - KEY_BUTTON_L, - KEY_BUTTON_R, - KEY_BUTTON_ZL, - KEY_BUTTON_ZR - ) - val triggerTitles = listOf( - R.string.button_l, - R.string.button_r, - R.string.button_zl, - R.string.button_zr - ) - val hotKeys = listOf( - HOTKEY_ENABLE, - HOTKEY_SCREEN_SWAP, - HOTKEY_CYCLE_LAYOUT, - HOTKEY_CLOSE_GAME, - HOTKEY_PAUSE_OR_RESUME, - HOTKEY_QUICKSAVE, - HOTKEY_QUICKlOAD, - HOTKEY_TURBO_LIMIT - ) - val hotkeyTitles = listOf( - R.string.controller_hotkey_enable_button, - R.string.emulation_swap_screens, - R.string.emulation_cycle_landscape_layouts, - R.string.emulation_close_game, - R.string.emulation_toggle_pause, - R.string.emulation_quicksave, - R.string.emulation_quickload, - R.string.turbo_limit_hotkey - ) - // TODO: Move these in with the other setting keys in GenerateSettingKeys.cmake const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" const val PREF_MATERIAL_YOU = "MaterialYouTheme" 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 aa67dd15b..532cfa2e9 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 @@ -4,593 +4,58 @@ package org.citra.citra_emu.features.settings.model.view -import android.content.Context -import android.content.SharedPreferences -import android.view.InputDevice import android.view.InputDevice.MotionRange import android.view.KeyEvent -import android.view.MotionEvent -import android.widget.Toast -import androidx.preference.PreferenceManager -import org.citra.citra_emu.CitraApplication -import org.citra.citra_emu.NativeLibrary -import org.citra.citra_emu.R -import org.citra.citra_emu.features.hotkeys.Hotkey -import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.input.Input +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.settings.model.InputMappingSetting import org.citra.citra_emu.features.settings.model.Settings class InputBindingSetting( - val abstractSetting: AbstractSetting<*>, + val inputSetting: InputMappingSetting, + val settings: Settings, titleId: Int -) : SettingsItem(abstractSetting, titleId, 0) { - private val context: Context get() = CitraApplication.appContext - private val preferences: SharedPreferences - get() = PreferenceManager.getDefaultSharedPreferences(context) - - var value: String - get() = preferences.getString(abstractSetting.key, "")!! - set(string) { - preferences.edit() - .putString(abstractSetting.key, string) - .apply() - } - - /** - * Returns true if this key is for the 3DS Circle Pad - */ - fun isCirclePad(): Boolean = - when (abstractSetting.key) { - Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, - Settings.KEY_CIRCLEPAD_AXIS_VERTICAL -> true - - else -> false - } - - /** - * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad - */ - fun isHorizontalOrientation(): Boolean = - when (abstractSetting.key) { - Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, - Settings.KEY_CSTICK_AXIS_HORIZONTAL, - Settings.KEY_DPAD_AXIS_HORIZONTAL -> true - - else -> false - } - - /** - * Returns true if this key is for the 3DS C-Stick - */ - fun isCStick(): Boolean = - when (abstractSetting.key) { - Settings.KEY_CSTICK_AXIS_HORIZONTAL, - Settings.KEY_CSTICK_AXIS_VERTICAL -> true - - else -> false - } - - /** - * Returns true if this key is for the 3DS D-Pad - */ - fun isDPad(): Boolean = - when (abstractSetting.key) { - Settings.KEY_DPAD_AXIS_HORIZONTAL, - Settings.KEY_DPAD_AXIS_VERTICAL -> true - - else -> false - } - /** - * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real - * triggers on the 3DS, but we support them as such on a physical gamepad. - */ - fun isTrigger(): Boolean = - when (abstractSetting.key) { - Settings.KEY_BUTTON_L, - Settings.KEY_BUTTON_R, - Settings.KEY_BUTTON_ZL, - Settings.KEY_BUTTON_ZR -> true - - else -> false - } - - /** - * Returns true if a gamepad axis can be used to map this key. - */ - fun isAxisMappingSupported(): Boolean { - return isCirclePad() || isCStick() || isDPad() || isTrigger() - } - - /** - * Returns true if a gamepad button can be used to map this key. - */ - fun isButtonMappingSupported(): Boolean { - return !isAxisMappingSupported() || isTrigger() - } - - /** - * Returns the Citra button code for the settings key. - */ - private val buttonCode: Int - get() = - when (abstractSetting.key) { - Settings.KEY_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A - Settings.KEY_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B - Settings.KEY_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X - Settings.KEY_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y - Settings.KEY_BUTTON_L -> NativeLibrary.ButtonType.TRIGGER_L - Settings.KEY_BUTTON_R -> NativeLibrary.ButtonType.TRIGGER_R - Settings.KEY_BUTTON_ZL -> NativeLibrary.ButtonType.BUTTON_ZL - Settings.KEY_BUTTON_ZR -> NativeLibrary.ButtonType.BUTTON_ZR - Settings.KEY_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_SELECT - Settings.KEY_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_START - Settings.KEY_BUTTON_HOME -> NativeLibrary.ButtonType.BUTTON_HOME - Settings.KEY_BUTTON_UP -> NativeLibrary.ButtonType.DPAD_UP - Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN - Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT - Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT - Settings.HOTKEY_ENABLE -> Hotkey.ENABLE.button - Settings.HOTKEY_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button - Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button - Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button - Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button - Settings.HOTKEY_QUICKSAVE -> Hotkey.QUICKSAVE.button - Settings.HOTKEY_QUICKlOAD -> Hotkey.QUICKLOAD.button - Settings.HOTKEY_TURBO_LIMIT -> Hotkey.TURBO_LIMIT.button - else -> -1 - } - - /** - * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old - * settings on re-mapping or clearing of a setting. - */ - private val reverseKey: String +) : SettingsItem(inputSetting, titleId, 0) { + val value: String get() { - var reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${abstractSetting.key}" - if (isAxisMappingSupported() && !isTrigger()) { - // Triggers are the only axis-supported mappings without orientation - reverseKey += "_" + if (isHorizontalOrientation()) { - 0 - } else { - 1 - } - } - return reverseKey + val mapping = settings.get(inputSetting) ?: return "" + return inputSetting.displayValue(mapping) } - /** - * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. - */ - @Suppress("UNCHECKED_CAST") fun removeOldMapping() { - // Try remove all possible keys we wrote for this setting - val oldKey = preferences.getString(reverseKey, "") - if (oldKey != "") { - //settings.set(setting as AbstractSetting,"") - preferences.edit() - .remove(abstractSetting.key) // Used for ui text - .remove(oldKey + "_GuestOrientation") // Used for axis orientation - .remove(oldKey + "_GuestButton") // Used for axis button - .remove(oldKey + "_Inverted") // used for axis inversion - .remove(reverseKey) - val buttonCodes = try { - preferences.getStringSet(oldKey, mutableSetOf())!!.toMutableSet() - } catch (e: ClassCastException) { - // if this is an int pref, either old button or an axis, so just remove it - preferences.edit().remove(oldKey).apply() - return; - } - buttonCodes.remove(buttonCode.toString()); - preferences.edit().putStringSet(oldKey,buttonCodes).apply() - } + /** Default value is cleared */ + settings.set(inputSetting, inputSetting.defaultValue) } /** - * Helper function to write a gamepad button mapping for the setting. - */ - private fun writeButtonMapping(keyEvent: KeyEvent) { - val editor = preferences.edit() - val key = getInputButtonKey(keyEvent) - // Pull in all codes associated with this key - // Migrate from the old int preference if need be - val buttonCodes = InputBindingSetting.getButtonSet(keyEvent) - buttonCodes.add(buttonCode) - // Cleanup old mapping for this setting - removeOldMapping() - - editor.putStringSet(key, buttonCodes.mapTo(mutableSetOf()) {it.toString()}) - - // Write next reverse mapping for future cleanup - editor.putString(reverseKey, key) - - // Apply changes - editor.apply() - } - - /** - * Helper function to write a gamepad axis mapping for the setting. - */ - private fun writeAxisMapping(axis: Int, value: Int, inverted: Boolean) { - // Cleanup old mapping - removeOldMapping() - - // Write new mapping - preferences.edit() - .putInt(getInputAxisOrientationKey(axis), if (isHorizontalOrientation()) 0 else 1) - .putInt(getInputAxisButtonKey(axis), value) - .putBoolean(getInputAxisInvertedKey(axis),inverted) - // Write next reverse mapping for future cleanup - .putString(reverseKey, getInputAxisKey(axis)) - .apply() - } - - /** - * Saves the provided key input setting as an Android preference. + * Saves the provided key input to the setting * * @param keyEvent KeyEvent of this key press. */ fun onKeyInput(keyEvent: KeyEvent) { - if (!isButtonMappingSupported()) { - Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show() - return - } - val code = translateEventToKeyId(keyEvent) - writeButtonMapping(keyEvent) - value = "${keyEvent.device.name}: ${getButtonName(code)}" + val mapping = Input(key = code) + settings.set(inputSetting, mapping) } /** * Saves the provided motion input setting as an Android preference. * - * @param device InputDevice from which the input event originated. * @param motionRange MotionRange of the movement - * @param axisDir Either '-' or '+' + * @param axisDir Either -1 or 1 */ - fun onMotionInput(device: InputDevice, motionRange: MotionRange, axisDir: Char) { - if (!isAxisMappingSupported()) { - Toast.makeText(context, R.string.input_message_button_only, Toast.LENGTH_LONG).show() - return - } - val button = if (isCirclePad()) { - NativeLibrary.ButtonType.STICK_LEFT - } else if (isCStick()) { - NativeLibrary.ButtonType.STICK_C - } else if (isDPad()) { - NativeLibrary.ButtonType.DPAD + fun onMotionInput(motionRange: MotionRange, axisDir: Int) { + val mapping = Input(axis = motionRange.axis, direction = axisDir, threshold = 0.5f) + settings.set(inputSetting, mapping) + } + + private fun translateEventToKeyId(event: KeyEvent): Int { + return if (event.keyCode == 0) { + event.scanCode } else { - buttonCode + event.keyCode } - // use UP (-) to map vertical, but use RIGHT (+) to map horizontal - val inverted = if (isHorizontalOrientation()) axisDir == '-' else axisDir == '+' - writeAxisMapping(motionRange.axis, button, inverted) - value = "Axis ${motionRange.axis}$axisDir" } override val type = TYPE_INPUT_BINDING - - companion object { - private const val INPUT_MAPPING_PREFIX = "InputMapping" - - private fun toTitleCase(raw: String): String = - raw.replace("_", " ").lowercase() - .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } - - private const val BUTTON_NAME_L3 = "Button L3" - private const val BUTTON_NAME_R3 = "Button R3" - - private val buttonNameOverrides = mapOf( - KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3, - KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3, - LINUX_BTN_DPAD_UP to "Dpad Up", - LINUX_BTN_DPAD_DOWN to "Dpad Down", - LINUX_BTN_DPAD_LEFT to "Dpad Left", - LINUX_BTN_DPAD_RIGHT to "Dpad Right" - ) - - fun getButtonName(keyCode: Int): String = - buttonNameOverrides[keyCode] - ?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_")) - - private data class DefaultButtonMapping( - val settingKey: String, - val hostKeyCode: Int, - val guestButtonCode: Int - ) - // Auto-map always sets inverted = false. Users needing inverted axes should remap manually. - private data class DefaultAxisMapping( - val settingKey: String, - val hostAxis: Int, - val guestButton: Int, - val orientation: Int, - val inverted: Boolean - ) - - private val xboxFaceButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), - DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), - DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_X), - DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_Y) - ) - - private val nintendoFaceButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_A), - DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_B), - DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), - DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) - ) - - private val commonButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1, NativeLibrary.ButtonType.TRIGGER_L), - DefaultButtonMapping(Settings.KEY_BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1, NativeLibrary.ButtonType.TRIGGER_R), - DefaultButtonMapping(Settings.KEY_BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2, NativeLibrary.ButtonType.BUTTON_ZL), - DefaultButtonMapping(Settings.KEY_BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2, NativeLibrary.ButtonType.BUTTON_ZR), - DefaultButtonMapping(Settings.KEY_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT, NativeLibrary.ButtonType.BUTTON_SELECT), - DefaultButtonMapping(Settings.KEY_BUTTON_START, KeyEvent.KEYCODE_BUTTON_START, NativeLibrary.ButtonType.BUTTON_START) - ) - - private val dpadButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_UP, KeyEvent.KEYCODE_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), - DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), - DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, KeyEvent.KEYCODE_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), - DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) - ) - - private val stickAxisMappings = listOf( - DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), - DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), - DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_Z, NativeLibrary.ButtonType.STICK_C, 0, false), - DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RZ, NativeLibrary.ButtonType.STICK_C, 1, false) - ) - - private val dpadAxisMappings = listOf( - DefaultAxisMapping(Settings.KEY_DPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_HAT_X, NativeLibrary.ButtonType.DPAD, 0, false), - DefaultAxisMapping(Settings.KEY_DPAD_AXIS_VERTICAL, MotionEvent.AXIS_HAT_Y, NativeLibrary.ButtonType.DPAD, 1, false) - ) - - // Nintendo Switch Joy-Con specific mappings. - // Joy-Cons connected via Bluetooth on Android have several quirks: - // - They register as two separate InputDevices (left and right) - // - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A) - // but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y) - // - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes - // - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ - private const val NINTENDO_VENDOR_ID = 0x057e - - // Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as - // KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't - // translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to - // the scan code in that case. - private const val LINUX_BTN_DPAD_UP = 0x220 // 544 - private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545 - private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546 - private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547 - - // Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not. - // This is different from both the standard Xbox table (full swap) and the - // Nintendo table (no swap). - private val joyconFaceButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), - DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), - DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), - DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) - ) - - // Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN - private val joyconDpadButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_UP, LINUX_BTN_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), - DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, LINUX_BTN_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), - DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, LINUX_BTN_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), - DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, LINUX_BTN_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) - ) - - // Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY - // (not Z/RZ like most controllers). The horizontal axis is inverted relative to - // the standard orientation - verified empirically on paired Joy-Cons via Bluetooth. - private val joyconStickAxisMappings = listOf( - DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), - DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), - DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_RX, NativeLibrary.ButtonType.STICK_C, 0, true), - DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RY, NativeLibrary.ButtonType.STICK_C, 1, false) - ) - - /** - * Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a - * Pro Controller or other Nintendo device) by checking vendor ID + device - * capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the - * right stick, while the Pro Controller has standard HAT axes and Z/RZ. - */ - fun isJoyCon(device: InputDevice?): Boolean { - if (device == null) return false - if (device.vendorId != NINTENDO_VENDOR_ID) return false - - // Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick). - // Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ. - var hasHatAxes = false - var hasStandardRightStick = false - for (range in device.motionRanges) { - when (range.axis) { - MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true - MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true - } - } - return !hasHatAxes && !hasStandardRightStick - } - - private val allBindingKeys: Set by lazy { - (Settings.buttonKeys + Settings.triggerKeys + - Settings.circlePadKeys + Settings.cStickKeys + Settings.dPadAxisKeys + - Settings.dPadButtonKeys).toSet() - } - - fun clearAllBindings() { - val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - val editor = prefs.edit() - val allKeys = prefs.all.keys.toList() - for (key in allKeys) { - if (key.startsWith(INPUT_MAPPING_PREFIX) || key in allBindingKeys) { - editor.remove(key) - } - } - editor.apply() - } - - private fun applyBindings( - buttonMappings: List, - axisMappings: List - ) { - val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - val editor = prefs.edit() - buttonMappings.forEach { applyDefaultButtonMapping(editor, it) } - axisMappings.forEach { applyDefaultAxisMapping(editor, it) } - editor.apply() - } - - /** - * Applies Joy-Con specific bindings: scan code D-pad, partial face button - * swap, and AXIS_RX/RY right stick. - */ - fun applyJoyConBindings() { - applyBindings( - joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings, - joyconStickAxisMappings - ) - } - - /** - * Applies auto-mapped bindings based on detected controller layout and d-pad type. - * - * @param isNintendoLayout true if the controller uses Nintendo face button layout - * (A=east, B=south), false for Xbox layout (A=south, B=east) - * @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y), - * false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT) - */ - fun applyAutoMapBindings(isNintendoLayout: Boolean, useAxisDpad: Boolean) { - val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings - val buttonMappings = if (useAxisDpad) { - faceButtons + commonButtonMappings - } else { - faceButtons + commonButtonMappings + dpadButtonMappings - } - val axisMappings = if (useAxisDpad) { - stickAxisMappings + dpadAxisMappings - } else { - stickAxisMappings - } - applyBindings(buttonMappings, axisMappings) - } - - private fun applyDefaultButtonMapping( - editor: SharedPreferences.Editor, - mapping: DefaultButtonMapping - ) { - val prefKey = getInputButtonKey(mapping.hostKeyCode) - editor.putInt(prefKey, mapping.guestButtonCode) - editor.putString(mapping.settingKey, getButtonName(mapping.hostKeyCode)) - editor.putString( - "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}", - prefKey - ) - } - - private fun applyDefaultAxisMapping( - editor: SharedPreferences.Editor, - mapping: DefaultAxisMapping - ) { - val axisKey = getInputAxisKey(mapping.hostAxis) - editor.putInt(getInputAxisOrientationKey(mapping.hostAxis), mapping.orientation) - editor.putInt(getInputAxisButtonKey(mapping.hostAxis), mapping.guestButton) - editor.putBoolean(getInputAxisInvertedKey(mapping.hostAxis), mapping.inverted) - val dir = if (mapping.orientation == 0) '+' else '-' - editor.putString(mapping.settingKey, "Axis ${mapping.hostAxis}$dir") - val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}_${mapping.orientation}" - editor.putString(reverseKey, axisKey) - } - - /** - * Returns the settings key for the specified Citra button code. - */ - private fun getButtonKey(buttonCode: Int): String = - when (buttonCode) { - NativeLibrary.ButtonType.BUTTON_A -> Settings.KEY_BUTTON_A - NativeLibrary.ButtonType.BUTTON_B -> Settings.KEY_BUTTON_B - NativeLibrary.ButtonType.BUTTON_X -> Settings.KEY_BUTTON_X - NativeLibrary.ButtonType.BUTTON_Y -> Settings.KEY_BUTTON_Y - NativeLibrary.ButtonType.TRIGGER_L -> Settings.KEY_BUTTON_L - NativeLibrary.ButtonType.TRIGGER_R -> Settings.KEY_BUTTON_R - NativeLibrary.ButtonType.BUTTON_ZL -> Settings.KEY_BUTTON_ZL - NativeLibrary.ButtonType.BUTTON_ZR -> Settings.KEY_BUTTON_ZR - NativeLibrary.ButtonType.BUTTON_SELECT -> Settings.KEY_BUTTON_SELECT - NativeLibrary.ButtonType.BUTTON_START -> Settings.KEY_BUTTON_START - NativeLibrary.ButtonType.BUTTON_HOME -> Settings.KEY_BUTTON_HOME - NativeLibrary.ButtonType.DPAD_UP -> Settings.KEY_BUTTON_UP - NativeLibrary.ButtonType.DPAD_DOWN -> Settings.KEY_BUTTON_DOWN - NativeLibrary.ButtonType.DPAD_LEFT -> Settings.KEY_BUTTON_LEFT - NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT - else -> "" - } - /** - * Get the mutable set of int button values this key should map to given an event - */ - fun getButtonSet(keyCode: KeyEvent):MutableSet { - val key = getInputButtonKey(keyCode) - val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - var buttonCodes = try { - preferences.getStringSet(key, mutableSetOf()) - } catch (e: ClassCastException) { - val prefInt = preferences.getInt(key, -1); - val migratedSet = if (prefInt != -1) { - mutableSetOf(prefInt.toString()) - } else { - mutableSetOf() - } - migratedSet - } - if (buttonCodes == null) buttonCodes = mutableSetOf() - return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet() - } - - private fun getInputButtonKey(keyId: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyId}" - - /** Falls back to the scan code when keyCode is KEYCODE_UNKNOWN. */ - fun getInputButtonKey(event: KeyEvent): String = getInputButtonKey(translateEventToKeyId(event)) - - /** - * Helper function to get the settings key for an gamepad axis. - */ - fun getInputAxisKey(axis: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${axis}" - - /** - * Helper function to get the settings key for an gamepad axis button (stick or trigger). - */ - fun getInputAxisButtonKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestButton" - - /** - * Helper function to get the settings key for an whether a gamepad axis is inverted. - */ - fun getInputAxisInvertedKey(axis: Int): String = "${getInputAxisKey(axis)}_Inverted" - - /** - * Helper function to get the settings key for an gamepad axis orientation. - */ - fun getInputAxisOrientationKey(axis: Int): String = - "${getInputAxisKey(axis)}_GuestOrientation" - - - /** - * This function translates a keyEvent into an "keyid" - * This key id is either the keyCode from the event, or - * the raw scanCode. - * Only when the keyCode itself is 0, (so it is an unknown key) - * we fall back to the raw scan code. - * This handles keys like the media-keys on google statia-controllers - * that don't have a conventional "mapping" and report as "unknown" - */ - fun translateEventToKeyId(event: KeyEvent): Int { - return if (event.keyCode == 0) { - event.scanCode - } else { - event.keyCode - } - } - } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt index 7366406df..15b190fa0 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt @@ -19,7 +19,6 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding -import androidx.preference.PreferenceManager import com.google.android.material.color.MaterialColors import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary @@ -199,13 +198,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { // Prevents saving to a non-existent settings file presenter.onSettingsReset() - val controllerKeys = Settings.buttonKeys + Settings.circlePadKeys + Settings.cStickKeys + - Settings.dPadAxisKeys + Settings.dPadButtonKeys + Settings.triggerKeys - val editor = - PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext).edit() - controllerKeys.forEach { editor.remove(it) } - editor.apply() - // Delete settings file because the user may have changed values that do not exist in the UI val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) if (!settingsFile.delete()) { 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 095241097..c8bec4aad 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 @@ -33,7 +33,7 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView, this.menuTag = menuTag this.gameId = gameId - perGameInGlobalContext = gameId != "" && !Settings.settings.isPerGame() + perGameInGlobalContext = gameId != "" && ! Settings.settings.isPerGame() // sync the active settings into my local settings appropriately // if we are editing global settings rom a game, this should just work diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index f08ebc07c..f6087ab98 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -248,7 +248,7 @@ class SettingsAdapter( private fun onMultiChoiceClick(item: MultiChoiceSetting) { clickedItem = item - val value: BooleanArray = getSelectionForMultiChoiceValue(item); + val value: BooleanArray = getSelectionForMultiChoiceValue(item) dialog = MaterialAlertDialogBuilder(context) .setTitle(item.nameId) .setMultiChoiceItems(item.choicesId, value, this) @@ -268,7 +268,7 @@ class SettingsAdapter( private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { clickedItem = item - dialog = context?.let { + dialog = context.let { MaterialAlertDialogBuilder(it) .setTitle(item.nameId) .setSingleChoiceItems(item.choices, item.selectValueIndex, this) @@ -370,9 +370,9 @@ class SettingsAdapter( value = sliderProgress textSliderValue?.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable) { - var textValue = s.toString().toFloatOrNull(); + var textValue = s.toString().toFloatOrNull() if (item.setting !is FloatSetting) { - textValue = textValue?.roundToInt()?.toFloat(); + textValue = textValue?.roundToInt()?.toFloat() } if (textValue == null || textValue < valueFrom || textValue > valueTo) { textInputLayout?.error = "Inappropriate value" @@ -469,7 +469,7 @@ class SettingsAdapter( val scSetting = clickedItem as? SingleChoiceSetting scSetting?.let { val value = getValueForSingleChoiceSelection(it, which) - if (it.selectedValue != value) fragmentView?.onSettingChanged() + if (it.selectedValue != value) fragmentView.onSettingChanged() it.setSelectedValue(value) fragmentView.loadSettingsList() closeDialog() @@ -480,7 +480,7 @@ class SettingsAdapter( val scSetting = clickedItem as? StringSingleChoiceSetting scSetting?.let { val value = it.getValueAt(which) ?: "" - if (it.selectedValue != value) fragmentView?.onSettingChanged() + if (it.selectedValue != value) fragmentView.onSettingChanged() it.setSelectedValue(value) fragmentView.loadSettingsList() closeDialog() @@ -492,7 +492,6 @@ class SettingsAdapter( sliderSetting?.let { val sliderval = it.roundedFloat(sliderProgress) if (sliderval != it.selectedFloat) fragmentView.onSettingChanged() - val s = it.setting when { it.setting?.defaultValue is Int -> it.setSelectedValue(sliderProgress.roundToInt()) else -> it.setSelectedValue(sliderProgress) @@ -506,7 +505,7 @@ class SettingsAdapter( val inputSetting = clickedItem as? StringInputSetting inputSetting?.let { if (it.selectedValue != textInputValue) { - fragmentView?.onSettingChanged() + fragmentView.onSettingChanged() } it.setSelectedValue(textInputValue) fragmentView.loadSettingsList() @@ -602,8 +601,9 @@ class SettingsAdapter( } fun onLongClickAutoMap(): Boolean { + val settings = fragmentView.activityView?.settings ?: return false showConfirmationDialog(R.string.controller_clear_all, R.string.controller_clear_all_confirm) { - InputBindingSetting.clearAllBindings() + settings.inputMappingManager.clear() fragmentView.loadSettingsList() fragmentView.onSettingChanged() } @@ -682,18 +682,18 @@ class SettingsAdapter( } private fun getSelectionForMultiChoiceValue(item: MultiChoiceSetting): BooleanArray { - val value = item.selectedValues; - val valuesId = item.valuesId; + val value = item.selectedValues + val valuesId = item.valuesId if (valuesId > 0) { - val valuesArray = context.resources.getIntArray(valuesId); + val valuesArray = context.resources.getIntArray(valuesId) val res = BooleanArray(valuesArray.size){false} for (index in valuesArray.indices) { if (value.contains(valuesArray[index])) { - res[index] = true; + res[index] = true } } - return res; + return res } - return BooleanArray(1){false}; + return BooleanArray(1){false} } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index e9506b44b..e01c16e80 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -19,7 +19,6 @@ import org.citra.citra_emu.R import org.citra.citra_emu.display.ScreenLayout import org.citra.citra_emu.display.StereoMode import org.citra.citra_emu.display.StereoWhichDisplay -import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting import org.citra.citra_emu.features.settings.model.IntSetting @@ -45,6 +44,8 @@ import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.ThemeUtil import kotlin.math.roundToInt +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.settings.model.InputMappingSetting class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { private var menuTag: String? = null @@ -754,45 +755,36 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) ) add(HeaderSetting(R.string.generic_buttons)) - Settings.buttonKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.buttonTitles[i])) + GamepadHelper.buttonKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.buttonTitles[i])) } add(HeaderSetting(R.string.controller_circlepad)) - Settings.circlePadKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.axisTitles[i])) + GamepadHelper.circlePadKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings,GamepadHelper.axisTitles[i])) } add(HeaderSetting(R.string.controller_c)) - Settings.cStickKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.axisTitles[i])) + GamepadHelper.cStickKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.axisTitles[i])) } - add(HeaderSetting(R.string.controller_dpad_axis,R.string.controller_dpad_axis_description)) - Settings.dPadAxisKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.axisTitles[i])) - } - add(HeaderSetting(R.string.controller_dpad_button,R.string.controller_dpad_button_description)) - Settings.dPadButtonKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.dPadTitles[i])) + add(HeaderSetting(R.string.controller_dpad,R.string.controller_dpad_axis_description)) + + GamepadHelper.dPadButtonKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.axisTitles[i])) } add(HeaderSetting(R.string.controller_triggers)) - Settings.triggerKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.triggerTitles[i])) + GamepadHelper.triggerKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.triggerTitles[i])) } add(HeaderSetting(R.string.controller_hotkeys,R.string.controller_hotkeys_description)) - Settings.hotKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.hotkeyTitles[i])) + GamepadHelper.hotKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.hotkeyTitles[i])) } + add(HeaderSetting(R.string.miscellaneous)) add( SwitchSetting( @@ -807,19 +799,6 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } } - private fun getInputObject(key: String): AbstractSetting { - return object : AbstractSetting { - override val key = key - override val section = Settings.SECTION_CONTROLS - override val isRuntimeEditable = true - override val defaultValue = "" - override fun valueFromString(string: String): String = string - override fun valueToString(value: String): String = value - // TODO: make input mappings also work per-game, which will be easy if we move - // them to config files - } - } - private fun addGraphicsSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) sl.apply { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt index 44e26758e..9f9965940 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt @@ -15,12 +15,10 @@ import org.citra.citra_emu.features.settings.ui.SettingsAdapter class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { override lateinit var setting: InputBindingSetting - override fun bind(item: SettingsItem) { - val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) setting = item as InputBindingSetting binding.textSettingName.setText(item.nameId) - val uiString = preferences.getString(setting.abstractSetting.key, "")!! + val uiString = setting.value if (uiString.isNotEmpty()) { binding.textSettingDescription.visibility = View.GONE binding.textSettingValue.visibility = View.VISIBLE @@ -39,6 +37,8 @@ class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter binding.textSettingDescription.alpha = 0.5f binding.textSettingValue.alpha = 0.5f } + + showGlobalButtonIfNeeded(binding.buttonUseGlobal, position) } override fun onClick(clicked: View) { 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 2e03fc722..888d723d6 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 @@ -12,6 +12,7 @@ import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.InputMappingSetting import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.Settings @@ -37,7 +38,8 @@ object SettingsFile { IntSetting.values().toList() + FloatSetting.values().toList() + StringSetting.values().toList() + - IntListSetting.values().toList() + IntListSetting.values().toList() + + InputMappingSetting.values().toList() } private fun findSettingByKey(key: String): AbstractSetting<*>? = @@ -96,6 +98,7 @@ object SettingsFile { */ fun loadSettings(settings: Settings, view: SettingsActivityView? = null) { readFile(getSettingsFile(FILE_NAME_CONFIG),settings,false,view) + settings.inputMappingManager.rebuild(settings) } /** @@ -106,6 +109,7 @@ object SettingsFile { loadSettings(settings, view) val file = findCustomGameSettingsFile(gameId) ?: return readFile(file, settings, true, view) + settings.inputMappingManager.rebuild(settings) } /** diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt index 569a0caca..8b3d01e65 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt @@ -11,12 +11,14 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import androidx.fragment.app.activityViewModels import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.citra.citra_emu.R import org.citra.citra_emu.databinding.DialogAutoMapBinding -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.utils.Log +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.settings.model.SettingsViewModel /** * Captures a single button press to detect controller layout (Xbox vs Nintendo) @@ -25,7 +27,7 @@ import org.citra.citra_emu.utils.Log class AutoMapDialogFragment : BottomSheetDialogFragment() { private var _binding: DialogAutoMapBinding? = null private val binding get() = _binding!! - + private val settingsViewModel: SettingsViewModel by activityViewModels() private var onComplete: (() -> Unit)? = null override fun onCreateView( @@ -72,12 +74,11 @@ class AutoMapDialogFragment : BottomSheetDialogFragment() { // Check if this is a Nintendo Switch Joy-Con (not Pro Controller). // Joy-Cons have unique quirks: split devices, non-standard D-pad scan codes, // partial A/B swap but no X/Y swap from Android's evdev layer. - val isJoyCon = InputBindingSetting.isJoyCon(device) + val isJoyCon = GamepadHelper.isJoyCon(device) if (isJoyCon) { Log.info("[AutoMap] Detected Joy-Con - using Joy-Con mappings") - InputBindingSetting.clearAllBindings() - InputBindingSetting.applyJoyConBindings() + GamepadHelper.applyJoyConBindings(settingsViewModel.settings) onComplete?.invoke() dismiss() return true @@ -106,8 +107,7 @@ class AutoMapDialogFragment : BottomSheetDialogFragment() { val dpadName = if (useAxisDpad) "axis" else "button" Log.info("[AutoMap] Detected $dpadName d-pad (device=${device?.name})") - InputBindingSetting.clearAllBindings() - InputBindingSetting.applyAutoMapBindings(isNintendoLayout, useAxisDpad) + GamepadHelper.applyAutoMapBindings(settingsViewModel.settings, isNintendoLayout, useAxisDpad) onComplete?.invoke() dismiss() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index f82a36724..d59088439 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -1270,6 +1270,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram binding.surfaceInputOverlay.resetButtonPlacement() } + + fun updateShowPerformanceOverlay() { if (perfStatsUpdater != null) { perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt index cf42bed12..b0d3e0732 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt @@ -56,36 +56,18 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { isCancelable = false view.requestFocus() view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } - if (setting!!.isButtonMappingSupported()) { - dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } - } - if (setting!!.isAxisMappingSupported()) { - binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } - } + dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } + binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } + - val inputTypeId = when { - setting!!.isCirclePad() -> R.string.controller_circlepad - setting!!.isCStick() -> R.string.controller_c - setting!!.isDPad() -> R.string.controller_dpad - setting!!.isTrigger() -> R.string.controller_trigger - else -> R.string.button - } binding.textTitle.text = String.format( getString(R.string.input_dialog_title), - getString(inputTypeId), - getString(setting!!.nameId) + setting!!.value,"" ) - var messageResId: Int = R.string.input_dialog_description - if (setting!!.isAxisMappingSupported() && !setting!!.isTrigger()) { - // Use specialized message for axis left/right or up/down - messageResId = if (setting!!.isHorizontalOrientation()) { - R.string.input_binding_description_horizontal_axis - } else { - R.string.input_binding_description_vertical_axis - } - } + val messageResId: Int = R.string.input_dialog_description + binding.textMessage.text = getString(messageResId) binding.buttonClear.setOnClickListener { @@ -140,7 +122,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { var numMovedAxis = 0 var axisMoveValue = 0.0f var lastMovedRange: InputDevice.MotionRange? = null - var lastMovedDir = '?' + var lastMovedDir = 0 if (waitingForEvent) { for (i in motionRanges.indices) { val range = motionRanges[i] @@ -164,7 +146,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { axisMoveValue = origValue numMovedAxis++ lastMovedRange = range - lastMovedDir = if (origValue < 0.0f) '-' else '+' + lastMovedDir = if (origValue < 0.0f) -1 else 1 } } else if (abs(origValue) < 0.25f && abs(previousValue) > 0.75f) { // Special case for d-pads (axis value jumps between 0 and 1 without any values @@ -172,7 +154,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { // due to the first press being caught by the "if (firstEvent)" case further up. numMovedAxis++ lastMovedRange = range - lastMovedDir = if (previousValue < 0.0f) '-' else '+' + lastMovedDir = if (previousValue < 0.0f) -1 else 1 } } previousValues[i] = origValue @@ -181,7 +163,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { // If only one axis moved, that's the winner. if (numMovedAxis == 1) { waitingForEvent = false - setting?.onMotionInput(input, lastMovedRange!!, lastMovedDir) + setting?.onMotionInput(lastMovedRange!!, lastMovedDir) dismiss() } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt index c50b7d8f7..15d95878d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt @@ -173,7 +173,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex continue } anyOverlayStateChanged = true - + // TODO - switch these to using standard hotkey buttons instead of nativelibrary buttons if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) { swapScreen() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt deleted file mode 100644 index 7bba904b5..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.utils - -import android.view.InputDevice -import android.view.KeyEvent -import android.view.MotionEvent - -/** - * Some controllers have incorrect mappings. This class has special-case fixes for them. - */ -object ControllerMappingHelper { - /** - * Some controllers report extra button presses that can be ignored. - */ - fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean { - return if (isDualShock4(inputDevice)) { - // The two analog triggers generate analog motion events as well as a keycode. - // We always prefer to use the analog values, so throw away the button press - keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2 - } else false - } - - /** - * Scale an axis to be zero-centered with a proper range. - */ - fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float { - if (isDualShock4(inputDevice)) { - // Android doesn't have correct mappings for this controller's triggers. It reports them - // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] - // Scale them to properly zero-centered with a range of [0.0, 1.0]. - if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { - return (value + 1) / 2.0f - } - } else if (isXboxOneWireless(inputDevice)) { - // Same as the DualShock 4, the mappings are missing. - if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { - return (value + 1) / 2.0f - } - if (axis == MotionEvent.AXIS_GENERIC_1) { - // This axis is stuck at ~.5. Ignore it. - return 0.0f - } - } else if (isMogaPro2Hid(inputDevice)) { - // This controller has a broken axis that reports a constant value. Ignore it. - if (axis == MotionEvent.AXIS_GENERIC_1) { - return 0.0f - } - } - return value - } - - private fun isDualShock4(inputDevice: InputDevice): Boolean { - // Sony DualShock 4 controller - return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc - } - - private fun isXboxOneWireless(inputDevice: InputDevice): Boolean { - // Microsoft Xbox One controller - return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0 - } - - private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean { - // Moga Pro 2 HID - return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271 - } -} diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 654d94b3e..4a8035139 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -141,21 +141,17 @@ void Config::ReadSetting(const std::string& group, Settings::SettingGetString( - "Controls", Settings::NativeButton::mapping[i], default_param); - if (Settings::values.current_input_profile.buttons[i].empty()) - Settings::values.current_input_profile.buttons[i] = default_param; + Settings::values.current_input_profile.buttons[i] = + InputManager::GenerateButtonParamPackage(default_buttons[i]); } for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { - std::string default_param = InputManager::GenerateAnalogParamPackage(default_analogs[i]); - Settings::values.current_input_profile.analogs[i] = android_config->GetString( - "Controls", Settings::NativeAnalog::mapping[i], default_param); - if (Settings::values.current_input_profile.analogs[i].empty()) - Settings::values.current_input_profile.analogs[i] = default_param; + Settings::values.current_input_profile.analogs[i] = + InputManager::GenerateAnalogParamPackage(default_analogs[i]); } Settings::values.current_input_profile.motion_device = android_config->GetString( diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 5e2eab1d1..abcdfac7b 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -77,6 +77,39 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"( # Use Artic Controller when connected to Artic Base Server. (Default 0) )") DECLARE_KEY(use_artic_base_controller) BOOST_HANA_STRING(R"( +# Configuration strings for 3ds buttons and axes. On Android, all such strings use "engine:gamepad" once bound. Default unbound. +)") DECLARE_KEY(button_a) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_b) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_x) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_y) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_l) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_r) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_zl) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_zr) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_start) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_select) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_home) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(dpad_up) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(dpad_down) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(dpad_left) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(dpad_right) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(circlepad_up) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(circlepad_down) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(circlepad_left) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(circlepad_right) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(cstick_up) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(cstick_down) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(cstick_left) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(cstick_right) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_close) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_cycle_layout) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_enable) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_pause_resume) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_quickload) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_quicksave) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_swap) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_turbo_limit) BOOST_HANA_STRING(R"( + [Core] # Whether to use the Just-In-Time (JIT) compiler for CPU emulation # 0: Interpreter (slow), 1 (default): JIT (fast) @@ -88,6 +121,7 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"( # Range is any positive integer (but we suspect 25 - 400 is a good idea) Default is 100 )") DECLARE_KEY(cpu_clock_percentage) BOOST_HANA_STRING(R"( + [Renderer] # Whether to render using OpenGL # 1: OpenGL ES (default), 2: Vulkan