From 6787fcb71277e69f43fc32f4b232af916a043c7f Mon Sep 17 00:00:00 2001 From: Mike <7153163+hackbar@users.noreply.github.com> Date: Tue, 31 Oct 2017 22:43:30 -0700 Subject: [PATCH] Android: Handle duplicate axis, and fix known bad axis. Android reports the same physical axis multiple times for analog triggers, and this handles this case. There are also some controllers with broken mappings (eg the analog triggers on a PS4 DualShock 4). These axis don't center correctly. There are also some controllers (again, the PS4) that send both a button press and an axis movement. This ignores the buttons so we can use the analog axis. Otherwise, since the button comes before the axis moves far we would always take the button. --- .../activities/EmulationActivity.java | 8 ++- .../dolphinemu/dialogs/MotionAlertDialog.java | 35 ++++++++--- .../utils/ControllerMappingHelper.java | 62 +++++++++++++++++++ 3 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ControllerMappingHelper.java diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java index 2adb2f209d..4688cbce05 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java @@ -39,6 +39,7 @@ import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment; import org.dolphinemu.dolphinemu.ui.main.MainPresenter; import org.dolphinemu.dolphinemu.ui.platform.Platform; import org.dolphinemu.dolphinemu.utils.Animations; +import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper; import org.dolphinemu.dolphinemu.utils.Java_GCAdapter; import org.dolphinemu.dolphinemu.utils.Java_WiimoteAdapter; import org.dolphinemu.dolphinemu.utils.Log; @@ -57,6 +58,7 @@ public final class EmulationActivity extends AppCompatActivity private EmulationFragment mEmulationFragment; private SharedPreferences mPreferences; + private ControllerMappingHelper mControllerMappingHelper; // So that MainActivity knows which view to invalidate before the return animation. private int mPosition; @@ -164,6 +166,7 @@ public final class EmulationActivity extends AppCompatActivity mScreenPath = gameToEmulate.getStringExtra("ScreenPath"); mPosition = gameToEmulate.getIntExtra("GridPosition", -1); mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen"); + mControllerMappingHelper = new ControllerMappingHelper(); int themeId; if (mDeviceHasTouchScreen) @@ -729,7 +732,10 @@ public final class EmulationActivity extends AppCompatActivity for (InputDevice.MotionRange range : motions) { - NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), range.getAxis(), event.getAxisValue(range.getAxis())); + int axis = range.getAxis(); + float origValue = event.getAxisValue(axis); + float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); + NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), axis, value); } return true; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java index 592aee6838..844d877ac6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java @@ -9,6 +9,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import org.dolphinemu.dolphinemu.model.settings.view.InputBindingSetting; +import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper; import org.dolphinemu.dolphinemu.utils.Log; import java.util.List; @@ -21,6 +22,7 @@ public final class MotionAlertDialog extends AlertDialog { // The selected input preference private final InputBindingSetting setting; + private final ControllerMappingHelper mControllerMappingHelper; private boolean mWaitingForEvent = true; /** @@ -34,6 +36,7 @@ public final class MotionAlertDialog extends AlertDialog super(context); this.setting = setting; + this.mControllerMappingHelper = new ControllerMappingHelper(); } public boolean onKeyEvent(int keyCode, KeyEvent event) @@ -42,8 +45,11 @@ public final class MotionAlertDialog extends AlertDialog switch (event.getAction()) { case KeyEvent.ACTION_DOWN: - saveKeyInput(event); - + if (!mControllerMappingHelper.shouldKeyBeIgnored(event.getDevice(), keyCode)) + { + saveKeyInput(event); + } + // Even if we ignore the key, we still consume it. Thus return true regardless. return true; default: @@ -69,13 +75,15 @@ public final class MotionAlertDialog extends AlertDialog { if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) return false; - - Log.debug("[MotionAlertDialog] Received motion event: " + event.getAction()); + if (event.getAction() != MotionEvent.ACTION_MOVE) + return false; InputDevice input = event.getDevice(); + List motionRanges = input.getMotionRanges(); int numMovedAxis = 0; + float axisMoveValue = 0.0f; InputDevice.MotionRange lastMovedRange = null; char lastMovedDir = '?'; if (mWaitingForEvent) @@ -84,12 +92,23 @@ public final class MotionAlertDialog extends AlertDialog for (InputDevice.MotionRange range : motionRanges) { int axis = range.getAxis(); - float value = event.getAxisValue(axis); + float origValue = event.getAxisValue(axis); + float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); if (Math.abs(value) > 0.5f) { - numMovedAxis++; - lastMovedRange = range; - lastMovedDir = value < 0.0f ? '-' : '+'; + // It is common to have multiple axis with the same physical input. For example, + // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE. + // To handle this, we ignore an axis motion that's the exact same as a motion + // we already saw. This way, we ignore axis with two names, but catch the case + // where a joystick is moved in two directions. + // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html + if (value != axisMoveValue) + { + axisMoveValue = value; + numMovedAxis++; + lastMovedRange = range; + lastMovedDir = value < 0.0f ? '-' : '+'; + } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ControllerMappingHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ControllerMappingHelper.java new file mode 100644 index 0000000000..1cdc07604a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ControllerMappingHelper.java @@ -0,0 +1,62 @@ +package org.dolphinemu.dolphinemu.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. */ +public class ControllerMappingHelper +{ + /** Some controllers report extra button presses that can be ignored. */ + public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) + { + 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 + // Even though the triggers are L/R2, without mappings they generate L/R1 events. + return keyCode == KeyEvent.KEYCODE_BUTTON_L1 || keyCode == KeyEvent.KEYCODE_BUTTON_R1; + } + return false; + } + + /** Scale an axis to be zero-centered with a proper range. */ + public float scaleAxis(InputDevice inputDevice, int axis, float value) + { + 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; + } + } + return value; + } + + private boolean isDualShock4(InputDevice inputDevice) + { + // Sony DualShock 4 controller + return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc; + } + + private boolean isXboxOneWireless(InputDevice inputDevice) + { + // Microsoft Xbox One controller + return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0; + } +}