From da7904a767c332e76e0890bf852a3724d745c984 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 1 Feb 2015 15:06:18 -0500 Subject: [PATCH] Add multiple controller support --- app/src/main/java/com/limelight/Game.java | 2 +- .../binding/input/ControllerHandler.java | 443 +++++++++--------- .../preferences/PreferenceConfiguration.java | 5 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 13 +- 5 files changed, 248 insertions(+), 217 deletions(-) diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index ed0f88c5..b7d714b7 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -193,7 +193,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, // Initialize the connection conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this)); keybTranslator = new KeyboardTranslator(conn); - controllerHandler = new ControllerHandler(conn, this, prefConfig.deadzonePercentage); + controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage); SurfaceHolder sh = sv.getHolder(); if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) { diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index df9f13a3..2ede0647 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -14,25 +14,9 @@ import com.limelight.ui.GameGestures; import com.limelight.utils.Vector2d; public class ControllerHandler { - private short inputMap = 0x0000; - private byte leftTrigger = 0x00; - private byte rightTrigger = 0x00; - private short rightStickX = 0x0000; - private short rightStickY = 0x0000; - private short leftStickX = 0x0000; - private short leftStickY = 0x0000; - private int emulatingButtonFlags = 0; - - // Used for OUYA bumper state tracking since they force all buttons - // up when the OUYA button goes down. We watch the last time we get - // a bumper up and compare that to our maximum delay when we receive - // a Start button press to see if we should activate one of our - // emulated button combos. - private long lastLbUpTime = 0; - private long lastRbUpTime = 0; + private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100; - private long startDownTime = 0; private static final int START_DOWN_TIME_KEYB_MS = 750; private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25; @@ -45,17 +29,21 @@ public class ControllerHandler { private Vector2d inputVector = new Vector2d(); - private HashMap mappings = new HashMap(); + private HashMap contexts = new HashMap(); private NvConnection conn; private double stickDeadzone; - private final ControllerMapping defaultMapping = new ControllerMapping(); + private final ControllerContext defaultContext = new ControllerContext(); private GameGestures gestures; private boolean hasGameController; + + private boolean multiControllerEnabled; + private short nextControllerNumber; - public ControllerHandler(NvConnection conn, GameGestures gestures, int deadzonePercentage) { + public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) { this.conn = conn; this.gestures = gestures; + this.multiControllerEnabled = multiControllerEnabled; // HACK: For now we're hardcoding a 10% deadzone. Some deadzone // is required for controller batching support to work. @@ -82,15 +70,16 @@ public class ControllerHandler { this.stickDeadzone = (double)deadzonePercentage / 100.0; - // Initialize the default mapping for events with no device - defaultMapping.leftStickXAxis = MotionEvent.AXIS_X; - defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y; - defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone; - defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z; - defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ; - defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone; - defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE; - defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS; + // Initialize the default context for events with no device + defaultContext.leftStickXAxis = MotionEvent.AXIS_X; + defaultContext.leftStickYAxis = MotionEvent.AXIS_Y; + defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone; + defaultContext.rightStickXAxis = MotionEvent.AXIS_Z; + defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ; + defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone; + defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE; + defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS; + defaultContext.controllerNumber = (short) 0; } private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) { @@ -106,19 +95,19 @@ public class ControllerHandler { return range; } - private ControllerMapping createMappingForDevice(InputDevice dev) { - ControllerMapping mapping = new ControllerMapping(); + private ControllerContext createContextForDevice(InputDevice dev) { + ControllerContext context = new ControllerContext(); String devName = dev.getName(); - LimeLog.info("Creating controller mapping for device: "+devName); + LimeLog.info("Creating controller context for device: "+devName); - mapping.leftStickXAxis = MotionEvent.AXIS_X; - mapping.leftStickYAxis = MotionEvent.AXIS_Y; - if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null && - getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) { + context.leftStickXAxis = MotionEvent.AXIS_X; + context.leftStickYAxis = MotionEvent.AXIS_Y; + if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null && + getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) { // This is a gamepad hasGameController = true; - mapping.hasJoystickAxes = true; + context.hasJoystickAxes = true; } InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER); @@ -128,14 +117,14 @@ public class ControllerHandler { if (leftTriggerRange != null && rightTriggerRange != null) { // Some controllers use LTRIGGER and RTRIGGER (like Ouya) - mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER; - mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER; + context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER; + context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER; } else if (brakeRange != null && gasRange != null) { // Others use GAS and BRAKE (like Moga) - mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE; - mapping.rightTriggerAxis = MotionEvent.AXIS_GAS; + context.leftTriggerAxis = MotionEvent.AXIS_BRAKE; + context.rightTriggerAxis = MotionEvent.AXIS_GAS; } else { @@ -144,34 +133,34 @@ public class ControllerHandler { if (rxRange != null && ryRange != null && devName != null) { if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) { // Xbox controllers use RX and RY for right stick - mapping.rightStickXAxis = MotionEvent.AXIS_RX; - mapping.rightStickYAxis = MotionEvent.AXIS_RY; + context.rightStickXAxis = MotionEvent.AXIS_RX; + context.rightStickYAxis = MotionEvent.AXIS_RY; // Xbox controllers use Z and RZ for triggers - mapping.leftTriggerAxis = MotionEvent.AXIS_Z; - mapping.rightTriggerAxis = MotionEvent.AXIS_RZ; - mapping.triggersIdleNegative = true; - mapping.isXboxController = true; + context.leftTriggerAxis = MotionEvent.AXIS_Z; + context.rightTriggerAxis = MotionEvent.AXIS_RZ; + context.triggersIdleNegative = true; + context.isXboxController = true; } else { // DS4 controller uses RX and RY for triggers - mapping.leftTriggerAxis = MotionEvent.AXIS_RX; - mapping.rightTriggerAxis = MotionEvent.AXIS_RY; - mapping.triggersIdleNegative = true; + context.leftTriggerAxis = MotionEvent.AXIS_RX; + context.rightTriggerAxis = MotionEvent.AXIS_RY; + context.triggersIdleNegative = true; - mapping.isDualShock4 = true; + context.isDualShock4 = true; } } } - if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) { + if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) { InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z); InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ); // Most other controllers use Z and RZ for the right stick if (zRange != null && rzRange != null) { - mapping.rightStickXAxis = MotionEvent.AXIS_Z; - mapping.rightStickYAxis = MotionEvent.AXIS_RZ; + context.rightStickXAxis = MotionEvent.AXIS_Z; + context.rightStickYAxis = MotionEvent.AXIS_RZ; } else { InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); @@ -179,8 +168,8 @@ public class ControllerHandler { // Try RX and RY now if (rxRange != null && ryRange != null) { - mapping.rightStickXAxis = MotionEvent.AXIS_RX; - mapping.rightStickYAxis = MotionEvent.AXIS_RY; + context.rightStickXAxis = MotionEvent.AXIS_RX; + context.rightStickYAxis = MotionEvent.AXIS_RY; } } } @@ -189,30 +178,30 @@ public class ControllerHandler { InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X); InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y); if (hatXRange != null && hatYRange != null) { - mapping.hatXAxis = MotionEvent.AXIS_HAT_X; - mapping.hatYAxis = MotionEvent.AXIS_HAT_Y; + context.hatXAxis = MotionEvent.AXIS_HAT_X; + context.hatYAxis = MotionEvent.AXIS_HAT_Y; } - if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { - mapping.leftStickDeadzoneRadius = (float) stickDeadzone; + if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) { + context.leftStickDeadzoneRadius = (float) stickDeadzone; } - if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { - mapping.rightStickDeadzoneRadius = (float) stickDeadzone; + if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) { + context.rightStickDeadzoneRadius = (float) stickDeadzone; } - if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { - InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis); - InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis); + if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) { + InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis); + InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis); // It's important to have a valid deadzone so controller packet batching works properly - mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat())); + context.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat())); // For triggers without (valid) deadzones, we'll use 13% (around XInput's default) - if (mapping.triggerDeadzone < 0.13f || - mapping.triggerDeadzone > 0.30f) + if (context.triggerDeadzone < 0.13f || + context.triggerDeadzone > 0.30f) { - mapping.triggerDeadzone = 0.13f; + context.triggerDeadzone = 0.13f; } } @@ -226,71 +215,82 @@ public class ControllerHandler { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0); if (!hasStartKey[0] && !hasStartKey[1]) { - mapping.backIsStart = true; + context.backIsStart = true; } } // The ASUS Gamepad has triggers that sit far forward and are prone to false presses // so we increase the deadzone on them to minimize this - mapping.triggerDeadzone = 0.30f; + context.triggerDeadzone = 0.30f; } // Classify this device as a remote by name else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) { // It's only a remote if it doesn't any sticks - if (!mapping.hasJoystickAxes) { - mapping.isRemote = true; + if (!context.hasJoystickAxes) { + context.isRemote = true; } } // NYKO Playpad has a fake hat that mimics the left stick for some reason else if (devName.contains("NYKO PLAYPAD")) { - mapping.hatXAxis = -1; - mapping.hatYAxis = -1; + context.hatXAxis = -1; + context.hatYAxis = -1; } } - LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius); - LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone); - - return mapping; + LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius); + LimeLog.info("Trigger deadzone: "+context.triggerDeadzone); + + if (multiControllerEnabled) { + context.controllerNumber = nextControllerNumber; + nextControllerNumber = (short)((nextControllerNumber + 1) % 4); + } + else { + context.controllerNumber = 0; + } + LimeLog.info("Assigned as controller "+context.controllerNumber); + + return context; } - private ControllerMapping getMappingForDevice(InputDevice dev) { - // Unknown devices use the default mapping + private ControllerContext getContextForDevice(InputDevice dev) { + // Unknown devices use the default context if (dev == null) { - return defaultMapping; + return defaultContext; } String descriptor = dev.getDescriptor(); - // Return the existing mapping if it exists - ControllerMapping mapping = mappings.get(descriptor); - if (mapping != null) { - return mapping; + // Return the existing context if it exists + ControllerContext context = contexts.get(descriptor); + if (context != null) { + return context; } - // Otherwise create a new mapping - mapping = createMappingForDevice(dev); - mappings.put(descriptor, mapping); + // Otherwise create a new context + context = createContextForDevice(dev); + contexts.put(descriptor, context); - return mapping; + return context; } - private void sendControllerInputPacket() { - conn.sendControllerInput(inputMap, leftTrigger, rightTrigger, - leftStickX, leftStickY, rightStickX, rightStickY); + private void sendControllerInputPacket(ControllerContext context) { + conn.sendControllerInput(context.controllerNumber, context.inputMap, + context.leftTrigger, context.rightTrigger, + context.leftStickX, context.leftStickY, + context.rightStickX, context.rightStickY); } // Return a valid keycode, 0 to consume, or -1 to not consume the event // Device MAY BE NULL - private int handleRemapping(ControllerMapping mapping, KeyEvent event) { + private int handleRemapping(ControllerContext context, KeyEvent event) { // For remotes, don't capture the back button - if (mapping.isRemote) { + if (context.isRemote) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { return -1; } } - if (mapping.isDualShock4) { + if (context.isDualShock4) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_BUTTON_Y: return KeyEvent.KEYCODE_BUTTON_L1; @@ -329,7 +329,7 @@ public class ControllerHandler { } } - if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { + if (context.hatXAxis != -1 && context.hatYAxis != -1) { switch (event.getKeyCode()) { // These are duplicate dpad events for hat input case KeyEvent.KEYCODE_DPAD_LEFT: @@ -340,9 +340,9 @@ public class ControllerHandler { return 0; } } - else if (mapping.hatXAxis == -1 && - mapping.hatYAxis == -1 && - mapping.isXboxController && + else if (context.hatXAxis == -1 && + context.hatYAxis == -1 && + context.isXboxController && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) { // If there's not a proper Xbox controller mapping, we'll translate the raw d-pad // scan codes into proper key codes @@ -375,9 +375,9 @@ public class ControllerHandler { if (keyCode == KeyEvent.KEYCODE_BUTTON_START || keyCode == KeyEvent.KEYCODE_MENU) { // Ensure that we never use back as start if we have a real start - mapping.backIsStart = false; + context.backIsStart = false; } - else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) { + else if (context.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) { // Emulate the start button with back return KeyEvent.KEYCODE_BUTTON_START; } @@ -402,101 +402,101 @@ public class ControllerHandler { // evaluates the deadzone. } - private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX, + private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX, float rsY, float lt, float rt, float hatX, float hatY) { - if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { + if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) { Vector2d leftStickVector = populateCachedVector(lsX, lsY); - handleDeadZone(leftStickVector, mapping.leftStickDeadzoneRadius); + handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius); - leftStickX = (short) (leftStickVector.getX() * 0x7FFE); - leftStickY = (short) (-leftStickVector.getY() * 0x7FFE); + context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE); + context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE); } - if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { + if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) { Vector2d rightStickVector = populateCachedVector(rsX, rsY); - handleDeadZone(rightStickVector, mapping.rightStickDeadzoneRadius); + handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius); - rightStickX = (short) (rightStickVector.getX() * 0x7FFE); - rightStickY = (short) (-rightStickVector.getY() * 0x7FFE); + context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE); + context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE); } - if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { - if (mapping.triggersIdleNegative) { + if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) { + if (context.triggersIdleNegative) { lt = (lt + 1) / 2; rt = (rt + 1) / 2; } - if (lt <= mapping.triggerDeadzone) { + if (lt <= context.triggerDeadzone) { lt = 0; } - if (rt <= mapping.triggerDeadzone) { + if (rt <= context.triggerDeadzone) { rt = 0; } - leftTrigger = (byte)(lt * 0xFF); - rightTrigger = (byte)(rt * 0xFF); + context.leftTrigger = (byte)(lt * 0xFF); + context.rightTrigger = (byte)(rt * 0xFF); } - if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { - inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG); + if (context.hatXAxis != -1 && context.hatYAxis != -1) { + context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG); if (hatX < -0.5) { - inputMap |= ControllerPacket.LEFT_FLAG; + context.inputMap |= ControllerPacket.LEFT_FLAG; } else if (hatX > 0.5) { - inputMap |= ControllerPacket.RIGHT_FLAG; + context.inputMap |= ControllerPacket.RIGHT_FLAG; } - inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG); + context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG); if (hatY < -0.5) { - inputMap |= ControllerPacket.UP_FLAG; + context.inputMap |= ControllerPacket.UP_FLAG; } else if (hatY > 0.5) { - inputMap |= ControllerPacket.DOWN_FLAG; + context.inputMap |= ControllerPacket.DOWN_FLAG; } } - sendControllerInputPacket(); + sendControllerInputPacket(context); } public boolean handleMotionEvent(MotionEvent event) { - ControllerMapping mapping = getMappingForDevice(event.getDevice()); + ControllerContext context = getContextForDevice(event.getDevice()); float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0; // We purposefully ignore the historical values in the motion event as it makes // the controller feel sluggish for some users. - if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { - lsX = event.getAxisValue(mapping.leftStickXAxis); - lsY = event.getAxisValue(mapping.leftStickYAxis); + if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) { + lsX = event.getAxisValue(context.leftStickXAxis); + lsY = event.getAxisValue(context.leftStickYAxis); } - if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { - rsX = event.getAxisValue(mapping.rightStickXAxis); - rsY = event.getAxisValue(mapping.rightStickYAxis); + if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) { + rsX = event.getAxisValue(context.rightStickXAxis); + rsY = event.getAxisValue(context.rightStickYAxis); } - if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { - lt = event.getAxisValue(mapping.leftTriggerAxis); - rt = event.getAxisValue(mapping.rightTriggerAxis); + if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) { + lt = event.getAxisValue(context.leftTriggerAxis); + rt = event.getAxisValue(context.rightTriggerAxis); } - if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { + if (context.hatXAxis != -1 && context.hatYAxis != -1) { hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X); hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y); } - handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY); + handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY); return true; } public boolean handleButtonUp(KeyEvent event) { - ControllerMapping mapping = getMappingForDevice(event.getDevice()); + ControllerContext context = getContextForDevice(event.getDevice()); - int keyCode = handleRemapping(mapping, event); + int keyCode = handleRemapping(context, event); if (keyCode == 0) { return true; } @@ -515,78 +515,78 @@ public class ControllerHandler { switch (keyCode) { case KeyEvent.KEYCODE_BUTTON_MODE: - inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; + context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; break; case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_MENU: - if (SystemClock.uptimeMillis() - startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) { + if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) { gestures.showKeyboard(); } - inputMap &= ~ControllerPacket.PLAY_FLAG; + context.inputMap &= ~ControllerPacket.PLAY_FLAG; break; case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BUTTON_SELECT: - inputMap &= ~ControllerPacket.BACK_FLAG; + context.inputMap &= ~ControllerPacket.BACK_FLAG; break; case KeyEvent.KEYCODE_DPAD_LEFT: - inputMap &= ~ControllerPacket.LEFT_FLAG; + context.inputMap &= ~ControllerPacket.LEFT_FLAG; break; case KeyEvent.KEYCODE_DPAD_RIGHT: - inputMap &= ~ControllerPacket.RIGHT_FLAG; + context.inputMap &= ~ControllerPacket.RIGHT_FLAG; break; case KeyEvent.KEYCODE_DPAD_UP: - inputMap &= ~ControllerPacket.UP_FLAG; + context.inputMap &= ~ControllerPacket.UP_FLAG; break; case KeyEvent.KEYCODE_DPAD_DOWN: - inputMap &= ~ControllerPacket.DOWN_FLAG; + context.inputMap &= ~ControllerPacket.DOWN_FLAG; break; case KeyEvent.KEYCODE_BUTTON_B: - inputMap &= ~ControllerPacket.B_FLAG; + context.inputMap &= ~ControllerPacket.B_FLAG; break; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_BUTTON_A: - inputMap &= ~ControllerPacket.A_FLAG; + context.inputMap &= ~ControllerPacket.A_FLAG; break; case KeyEvent.KEYCODE_BUTTON_X: - inputMap &= ~ControllerPacket.X_FLAG; + context.inputMap &= ~ControllerPacket.X_FLAG; break; case KeyEvent.KEYCODE_BUTTON_Y: - inputMap &= ~ControllerPacket.Y_FLAG; + context.inputMap &= ~ControllerPacket.Y_FLAG; break; case KeyEvent.KEYCODE_BUTTON_L1: - inputMap &= ~ControllerPacket.LB_FLAG; - lastLbUpTime = SystemClock.uptimeMillis(); + context.inputMap &= ~ControllerPacket.LB_FLAG; + context.lastLbUpTime = SystemClock.uptimeMillis(); break; case KeyEvent.KEYCODE_BUTTON_R1: - inputMap &= ~ControllerPacket.RB_FLAG; - lastRbUpTime = SystemClock.uptimeMillis(); + context.inputMap &= ~ControllerPacket.RB_FLAG; + context.lastRbUpTime = SystemClock.uptimeMillis(); break; case KeyEvent.KEYCODE_BUTTON_THUMBL: - inputMap &= ~ControllerPacket.LS_CLK_FLAG; + context.inputMap &= ~ControllerPacket.LS_CLK_FLAG; break; case KeyEvent.KEYCODE_BUTTON_THUMBR: - inputMap &= ~ControllerPacket.RS_CLK_FLAG; + context.inputMap &= ~ControllerPacket.RS_CLK_FLAG; break; case KeyEvent.KEYCODE_BUTTON_L2: - leftTrigger = 0; + context.leftTrigger = 0; break; case KeyEvent.KEYCODE_BUTTON_R2: - rightTrigger = 0; + context.rightTrigger = 0; break; default: return false; } // Check if we're emulating the select button - if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0) + if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0) { // If either start or LB is up, select comes up too - if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 || - (inputMap & ControllerPacket.LB_FLAG) == 0) + if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 || + (context.inputMap & ControllerPacket.LB_FLAG) == 0) { - inputMap &= ~ControllerPacket.BACK_FLAG; - - emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT; + context.inputMap &= ~ControllerPacket.BACK_FLAG; + + context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT; try { Thread.sleep(EMULATED_SELECT_UP_DELAY_MS); @@ -595,16 +595,16 @@ public class ControllerHandler { } // Check if we're emulating the special button - if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0) + if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0) { // If either start or select and RB is up, the special button comes up too - if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 || - ((inputMap & ControllerPacket.BACK_FLAG) == 0 && - (inputMap & ControllerPacket.RB_FLAG) == 0)) + if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 || + ((context.inputMap & ControllerPacket.BACK_FLAG) == 0 && + (context.inputMap & ControllerPacket.RB_FLAG) == 0)) { - inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; - - emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL; + context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; + + context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL; try { Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS); @@ -612,112 +612,112 @@ public class ControllerHandler { } } - sendControllerInputPacket(); + sendControllerInputPacket(context); return true; } public boolean handleButtonDown(KeyEvent event) { - ControllerMapping mapping = getMappingForDevice(event.getDevice()); + ControllerContext context = getContextForDevice(event.getDevice()); - int keyCode = handleRemapping(mapping, event); + int keyCode = handleRemapping(context, event); if (keyCode == 0) { return true; } switch (keyCode) { case KeyEvent.KEYCODE_BUTTON_MODE: - inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; + context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; break; case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_MENU: if (event.getRepeatCount() == 0) { - startDownTime = SystemClock.uptimeMillis(); + context.startDownTime = SystemClock.uptimeMillis(); } - inputMap |= ControllerPacket.PLAY_FLAG; + context.inputMap |= ControllerPacket.PLAY_FLAG; break; case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BUTTON_SELECT: - inputMap |= ControllerPacket.BACK_FLAG; + context.inputMap |= ControllerPacket.BACK_FLAG; break; case KeyEvent.KEYCODE_DPAD_LEFT: - inputMap |= ControllerPacket.LEFT_FLAG; + context.inputMap |= ControllerPacket.LEFT_FLAG; break; case KeyEvent.KEYCODE_DPAD_RIGHT: - inputMap |= ControllerPacket.RIGHT_FLAG; + context.inputMap |= ControllerPacket.RIGHT_FLAG; break; case KeyEvent.KEYCODE_DPAD_UP: - inputMap |= ControllerPacket.UP_FLAG; + context.inputMap |= ControllerPacket.UP_FLAG; break; case KeyEvent.KEYCODE_DPAD_DOWN: - inputMap |= ControllerPacket.DOWN_FLAG; + context.inputMap |= ControllerPacket.DOWN_FLAG; break; case KeyEvent.KEYCODE_BUTTON_B: - inputMap |= ControllerPacket.B_FLAG; + context.inputMap |= ControllerPacket.B_FLAG; break; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_BUTTON_A: - inputMap |= ControllerPacket.A_FLAG; + context.inputMap |= ControllerPacket.A_FLAG; break; case KeyEvent.KEYCODE_BUTTON_X: - inputMap |= ControllerPacket.X_FLAG; + context.inputMap |= ControllerPacket.X_FLAG; break; case KeyEvent.KEYCODE_BUTTON_Y: - inputMap |= ControllerPacket.Y_FLAG; + context.inputMap |= ControllerPacket.Y_FLAG; break; case KeyEvent.KEYCODE_BUTTON_L1: - inputMap |= ControllerPacket.LB_FLAG; + context.inputMap |= ControllerPacket.LB_FLAG; break; case KeyEvent.KEYCODE_BUTTON_R1: - inputMap |= ControllerPacket.RB_FLAG; + context.inputMap |= ControllerPacket.RB_FLAG; break; case KeyEvent.KEYCODE_BUTTON_THUMBL: - inputMap |= ControllerPacket.LS_CLK_FLAG; + context.inputMap |= ControllerPacket.LS_CLK_FLAG; break; case KeyEvent.KEYCODE_BUTTON_THUMBR: - inputMap |= ControllerPacket.RS_CLK_FLAG; + context.inputMap |= ControllerPacket.RS_CLK_FLAG; break; case KeyEvent.KEYCODE_BUTTON_L2: - leftTrigger = (byte)0xFF; + context.leftTrigger = (byte)0xFF; break; case KeyEvent.KEYCODE_BUTTON_R2: - rightTrigger = (byte)0xFF; + context.rightTrigger = (byte)0xFF; break; default: return false; } // Start+LB acts like select for controllers with one button - if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 && - ((inputMap & ControllerPacket.LB_FLAG) != 0 || - SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS)) + if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 && + ((context.inputMap & ControllerPacket.LB_FLAG) != 0 || + SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS)) { - inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG); - inputMap |= ControllerPacket.BACK_FLAG; - - emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT; + context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG); + context.inputMap |= ControllerPacket.BACK_FLAG; + + context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT; } // We detect select+start or start+RB as the special button combo - if (((inputMap & ControllerPacket.RB_FLAG) != 0 || - (SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) || - (inputMap & ControllerPacket.BACK_FLAG) != 0) && - (inputMap & ControllerPacket.PLAY_FLAG) != 0) + if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 || + (SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) || + (context.inputMap & ControllerPacket.BACK_FLAG) != 0) && + (context.inputMap & ControllerPacket.PLAY_FLAG) != 0) { - inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG); - inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; - - emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL; + context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG); + context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; + + context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL; } // Send a new input packet if this is the first instance of a button down event // or anytime if we're emulating a button - if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) { - sendControllerInputPacket(); + if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) { + sendControllerInputPacket(context); } return true; } - class ControllerMapping { + class ControllerContext { public int leftStickXAxis = -1; public int leftStickYAxis = -1; public float leftStickDeadzoneRadius; @@ -739,5 +739,26 @@ public class ControllerHandler { public boolean backIsStart; public boolean isRemote; public boolean hasJoystickAxes; + + public short controllerNumber; + + public short inputMap = 0x0000; + public byte leftTrigger = 0x00; + public byte rightTrigger = 0x00; + public short rightStickX = 0x0000; + public short rightStickY = 0x0000; + public short leftStickX = 0x0000; + public short leftStickY = 0x0000; + public int emulatingButtonFlags = 0; + + // Used for OUYA bumper state tracking since they force all buttons + // up when the OUYA button goes down. We watch the last time we get + // a bumper up and compare that to our maximum delay when we receive + // a Start button press to see if we should activate one of our + // emulated button combos. + public long lastLbUpTime = 0; + public long lastRbUpTime = 0; + + public long startDownTime = 0; } } diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index b9a9df67..7b7bb109 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -16,6 +16,7 @@ public class PreferenceConfiguration { private static final String LANGUAGE_PREF_STRING = "list_languages"; private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode"; private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode"; + private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller"; private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_60 = 10; @@ -32,6 +33,7 @@ public class PreferenceConfiguration { private static final int DEFAULT_DEADZONE = 15; public static final String DEFAULT_LANGUAGE = "default"; private static final boolean DEFAULT_LIST_MODE = false; + private static final boolean DEFAULT_MULTI_CONTROLLER = true; public static final int FORCE_HARDWARE_DECODER = -1; public static final int AUTOSELECT_DECODER = 0; @@ -43,7 +45,7 @@ public class PreferenceConfiguration { public int deadzonePercentage; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public String language; - public boolean listMode, smallIconMode; + public boolean listMode, smallIconMode, multiController; public static int getDefaultBitrate(String resFpsString) { if (resFpsString.equals("720p30")) { @@ -156,6 +158,7 @@ public class PreferenceConfiguration { config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE); config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)); + config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER); return config; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d5258ef4..1301092f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,6 +95,8 @@ Disable on-screen connection warning messages while streaming Gamepad Settings + Enable multiple controller support + When unchecked, all controllers appear as controller one Adjust analog stick deadzone % diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b56f0d15..678d0b7e 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -26,14 +26,19 @@ android:summary="@string/summary_checkbox_disable_warnings" android:defaultValue="false" /> - + android:title="@string/title_seekbar_deadzone"/--> + +