diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/PhysicalControllerManager.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/PhysicalControllerManager.kt index ec05d45e3..5172e57a0 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/PhysicalControllerManager.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/PhysicalControllerManager.kt @@ -4,10 +4,17 @@ import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent import org.kenjinx.android.viewmodels.QuickSettings +import kotlin.math.abs class PhysicalControllerManager(val activity: MainActivity) { private var controllerId: Int = -1 + // Trigger debouncing (analog → digital) + private var leftTriggerPressed = false + private var rightTriggerPressed = false + private val pressThreshold = 0.65f + private val releaseThreshold = 0.45f + fun onKeyEvent(event: KeyEvent): Boolean { // Make sure we are connected if (controllerId == -1) { @@ -17,69 +24,116 @@ class PhysicalControllerManager(val activity: MainActivity) { val id = getGamePadButtonInputId(event.keyCode) if (id != GamePadButtonInputId.None) { val isNotFallback = (event.flags and KeyEvent.FLAG_FALLBACK) == 0 - // Many gamepads send additional fallback events – we suppress them. if (isNotFallback) { when (event.action) { - KeyEvent.ACTION_UP -> { - KenjinxNative.inputSetButtonReleased(id.ordinal, controllerId) - } - KeyEvent.ACTION_DOWN -> { - KenjinxNative.inputSetButtonPressed(id.ordinal, controllerId) - } + KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(id.ordinal, controllerId) + KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(id.ordinal, controllerId) } } return true } - return false } fun onMotionEvent(ev: MotionEvent) { - if (ev.action == MotionEvent.ACTION_MOVE) { - if (controllerId == -1) { - controllerId = KenjinxNative.inputConnectGamepad(0) - } + if (ev.action != MotionEvent.ACTION_MOVE) return - val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X) - val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y) - val rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z) - val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ) + if (controllerId == -1) { + controllerId = KenjinxNative.inputConnectGamepad(0) + } - KenjinxNative.inputSetStickAxis(1, leftStickX, -leftStickY, controllerId) - KenjinxNative.inputSetStickAxis(2, rightStickX, -rightStickY, controllerId) + val device = ev.device + val source = InputDevice.SOURCE_JOYSTICK - ev.device?.apply { - if (sources and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD) { - // Controller uses HAT instead of “real” DPAD - val dPadHor = ev.getAxisValue(MotionEvent.AXIS_HAT_X) - val dPadVert = ev.getAxisValue(MotionEvent.AXIS_HAT_Y) + fun hasAxis(axis: Int): Boolean = + device?.getMotionRange(axis, source) != null - if (dPadVert == 0.0f) { - KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, controllerId) - KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, controllerId) - } - if (dPadHor == 0.0f) { - KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, controllerId) - KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, controllerId) - } + fun axisValue(axis: Int): Float = ev.getAxisValue(axis) - if (dPadVert < 0.0f) { - KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, controllerId) - KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, controllerId) - } - if (dPadHor < 0.0f) { - KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, controllerId) - KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, controllerId) - } + // --- Sticks (prefer RX/RY on the right, fallback to Z/RZ) --- + val rightXaxis = if (hasAxis(MotionEvent.AXIS_RX)) MotionEvent.AXIS_RX else MotionEvent.AXIS_Z + val rightYaxis = if (hasAxis(MotionEvent.AXIS_RY)) MotionEvent.AXIS_RY else MotionEvent.AXIS_RZ - if (dPadVert > 0.0f) { - KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, controllerId) - KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, controllerId) - } - if (dPadHor > 0.0f) { - KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, controllerId) - KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, controllerId) - } + val leftStickX = if (hasAxis(MotionEvent.AXIS_X)) axisValue(MotionEvent.AXIS_X) else 0f + val leftStickY = if (hasAxis(MotionEvent.AXIS_Y)) axisValue(MotionEvent.AXIS_Y) else 0f + val rightStickX = if (hasAxis(rightXaxis)) axisValue(rightXaxis) else 0f + val rightStickY = if (hasAxis(rightYaxis)) axisValue(rightYaxis) else 0f + + KenjinxNative.inputSetStickAxis(1, leftStickX, -leftStickY, controllerId) + KenjinxNative.inputSetStickAxis(2, rightStickX, -rightStickY, controllerId) + + // --- Read triggers (with fallbacks) --- + // Preferred: LTRIGGER/RTRIGGER, then BRAKE/GAS. + // If the right stick uses RX/RY (standard on Xbox), Z/RZ are free → use as an additional fallback. + // If the stick uses Z/RZ, do NOT use them for triggers (to avoid conflicts). + val rightStickUsesZ = (rightXaxis == MotionEvent.AXIS_Z) + val rightStickUsesRZ = (rightYaxis == MotionEvent.AXIS_RZ) + + val rawLT = when { + hasAxis(MotionEvent.AXIS_LTRIGGER) -> axisValue(MotionEvent.AXIS_LTRIGGER) + hasAxis(MotionEvent.AXIS_BRAKE) -> axisValue(MotionEvent.AXIS_BRAKE) + !rightStickUsesZ && hasAxis(MotionEvent.AXIS_Z) -> axisValue(MotionEvent.AXIS_Z) + else -> 0f + } + val rawRT = when { + hasAxis(MotionEvent.AXIS_RTRIGGER) -> axisValue(MotionEvent.AXIS_RTRIGGER) + hasAxis(MotionEvent.AXIS_GAS) -> axisValue(MotionEvent.AXIS_GAS) + !rightStickUsesRZ && hasAxis(MotionEvent.AXIS_RZ) -> axisValue(MotionEvent.AXIS_RZ) + else -> 0f + } + + // Some pads report slight offsets — normalize + val lt = if (abs(rawLT) < 0.02f) 0f else rawLT.coerceIn(0f, 1f) + val rt = if (abs(rawRT) < 0.02f) 0f else rawRT.coerceIn(0f, 1f) + + // Analog → digital with hysteresis + if (!leftTriggerPressed && lt >= pressThreshold) { + leftTriggerPressed = true + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.LeftTrigger.ordinal, controllerId) + } else if (leftTriggerPressed && lt <= releaseThreshold) { + leftTriggerPressed = false + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.LeftTrigger.ordinal, controllerId) + } + + if (!rightTriggerPressed && rt >= pressThreshold) { + rightTriggerPressed = true + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.RightTrigger.ordinal, controllerId) + } else if (rightTriggerPressed && rt <= releaseThreshold) { + rightTriggerPressed = false + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.RightTrigger.ordinal, controllerId) + } + + // --- DPAD as HAT (as before) --- + device?.apply { + if (sources and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD) { + val dPadHor = ev.getAxisValue(MotionEvent.AXIS_HAT_X) + val dPadVert = ev.getAxisValue(MotionEvent.AXIS_HAT_Y) + + if (dPadVert == 0.0f) { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, controllerId) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, controllerId) + } + if (dPadHor == 0.0f) { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, controllerId) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, controllerId) + } + + if (dPadVert < 0.0f) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, controllerId) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, controllerId) + } + if (dPadHor < 0.0f) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, controllerId) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, controllerId) + } + + if (dPadVert > 0.0f) { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, controllerId) + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, controllerId) + } + if (dPadHor > 0.0f) { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, controllerId) + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, controllerId) } } } @@ -92,39 +146,46 @@ class PhysicalControllerManager(val activity: MainActivity) { fun disconnect() { controllerId = -1 + // If a trigger was "stuck" on disconnect, release it just in case + if (leftTriggerPressed) { + leftTriggerPressed = false + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.LeftTrigger.ordinal, controllerId) + } + if (rightTriggerPressed) { + rightTriggerPressed = false + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.RightTrigger.ordinal, controllerId) + } } private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId { val quickSettings = QuickSettings(activity) return when (keycode) { - // ABXY (Switch/Xbox layout switchable) + // ABXY (Switch/Xbox layout) KeyEvent.KEYCODE_BUTTON_A -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.A else GamePadButtonInputId.B KeyEvent.KEYCODE_BUTTON_B -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.B else GamePadButtonInputId.A KeyEvent.KEYCODE_BUTTON_X -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.X else GamePadButtonInputId.Y KeyEvent.KEYCODE_BUTTON_Y -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.Y else GamePadButtonInputId.X - // Shoulder buttons + // Shoulder & Trigger (if a pad sends them as key events) KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder KeyEvent.KEYCODE_BUTTON_R2 -> GamePadButtonInputId.RightTrigger - // **L3 / R3 (Stick-Click) – CORRECT: *_Button** + // L3 / R3 KeyEvent.KEYCODE_BUTTON_THUMBL -> GamePadButtonInputId.LeftStickButton KeyEvent.KEYCODE_BUTTON_THUMBR -> GamePadButtonInputId.RightStickButton - - // Additional fallback keycodes for some pads (optional) - KeyEvent.KEYCODE_BUTTON_11 -> GamePadButtonInputId.LeftStickButton // isolated L3 - KeyEvent.KEYCODE_BUTTON_12 -> GamePadButtonInputId.RightStickButton // isolated R3 + KeyEvent.KEYCODE_BUTTON_11 -> GamePadButtonInputId.LeftStickButton + KeyEvent.KEYCODE_BUTTON_12 -> GamePadButtonInputId.RightStickButton // D-Pad - KeyEvent.KEYCODE_DPAD_UP -> GamePadButtonInputId.DpadUp - KeyEvent.KEYCODE_DPAD_DOWN -> GamePadButtonInputId.DpadDown - KeyEvent.KEYCODE_DPAD_LEFT -> GamePadButtonInputId.DpadLeft + KeyEvent.KEYCODE_DPAD_UP -> GamePadButtonInputId.DpadUp + KeyEvent.KEYCODE_DPAD_DOWN -> GamePadButtonInputId.DpadDown + KeyEvent.KEYCODE_DPAD_LEFT -> GamePadButtonInputId.DpadLeft KeyEvent.KEYCODE_DPAD_RIGHT -> GamePadButtonInputId.DpadRight // Plus/Minus - KeyEvent.KEYCODE_BUTTON_START -> GamePadButtonInputId.Plus + KeyEvent.KEYCODE_BUTTON_START -> GamePadButtonInputId.Plus KeyEvent.KEYCODE_BUTTON_SELECT -> GamePadButtonInputId.Minus else -> GamePadButtonInputId.None