From ea4b7226a6af157be13798b2fcc7121f66d769d8 Mon Sep 17 00:00:00 2001
From: german77 <juangerman-13@hotmail.com>
Date: Sat, 15 May 2021 22:48:37 -0500
Subject: input_common: Add dual joycon support

---
 src/input_common/sdl/sdl_impl.cpp | 281 ++++++++++++++++++++++++++++++--------
 src/input_common/sdl/sdl_impl.h   |  37 ++++-
 2 files changed, 258 insertions(+), 60 deletions(-)

(limited to 'src/input_common/sdl')

diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp
index 822d0b555..314b0e773 100644
--- a/src/input_common/sdl/sdl_impl.cpp
+++ b/src/input_common/sdl/sdl_impl.cpp
@@ -18,16 +18,6 @@
 #include <utility>
 #include <vector>
 
-// Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307
-#ifdef __clang__
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wimplicit-fallthrough"
-#endif
-#include <SDL.h>
-#ifdef __clang__
-#pragma clang diagnostic pop
-#endif
-
 #include "common/logging/log.h"
 #include "common/math_util.h"
 #include "common/param_package.h"
@@ -214,6 +204,40 @@ public:
         sdl_controller.reset(controller);
     }
 
+    bool IsJoyconLeft() const {
+        return std::strstr(GetControllerName().c_str(), "Joy-Con Left") != nullptr;
+    }
+
+    bool IsJoyconRight() const {
+        return std::strstr(GetControllerName().c_str(), "Joy-Con Right") != nullptr;
+    }
+
+    std::string GetControllerName() const {
+        if (sdl_controller) {
+            switch (SDL_GameControllerGetType(sdl_controller.get())) {
+            case SDL_CONTROLLER_TYPE_XBOX360:
+                return "XBox 360 Controller";
+            case SDL_CONTROLLER_TYPE_XBOXONE:
+                return "XBox One Controller";
+            default:
+                break;
+            }
+            const auto name = SDL_GameControllerName(sdl_controller.get());
+            if (name) {
+                return name;
+            }
+        }
+
+        if (sdl_joystick) {
+            const auto name = SDL_JoystickName(sdl_joystick.get());
+            if (name) {
+                return name;
+            }
+        }
+
+        return "Unknown";
+    }
+
 private:
     struct State {
         std::unordered_map<int, bool> buttons;
@@ -856,23 +880,42 @@ SDLState::~SDLState() {
 std::vector<Common::ParamPackage> SDLState::GetInputDevices() {
     std::scoped_lock lock(joystick_map_mutex);
     std::vector<Common::ParamPackage> devices;
+    std::unordered_map<int, std::shared_ptr<SDLJoystick>> joycon_pairs;
+    for (const auto& [key, value] : joystick_map) {
+        for (const auto& joystick : value) {
+            if (!joystick->GetSDLJoystick()) {
+                continue;
+            }
+            std::string name =
+                fmt::format("{} {}", joystick->GetControllerName(), joystick->GetPort());
+            devices.emplace_back(Common::ParamPackage{
+                {"class", "sdl"},
+                {"display", std::move(name)},
+                {"guid", joystick->GetGUID()},
+                {"port", std::to_string(joystick->GetPort())},
+            });
+            if (joystick->IsJoyconLeft()) {
+                joycon_pairs.insert_or_assign(joystick->GetPort(), joystick);
+            }
+        }
+    }
+
+    // Add dual controllers
     for (const auto& [key, value] : joystick_map) {
         for (const auto& joystick : value) {
-            if (auto* const controller = joystick->GetSDLGameController()) {
+            if (joystick->IsJoyconRight()) {
+                if (!joycon_pairs.contains(joystick->GetPort())) {
+                    continue;
+                }
+                const auto joystick2 = joycon_pairs.at(joystick->GetPort());
+
                 std::string name =
-                    fmt::format("{} {}", GetControllerName(controller), joystick->GetPort());
-                devices.emplace_back(Common::ParamPackage{
-                    {"class", "sdl"},
-                    {"display", std::move(name)},
-                    {"guid", joystick->GetGUID()},
-                    {"port", std::to_string(joystick->GetPort())},
-                });
-            } else if (auto* const joy = joystick->GetSDLJoystick()) {
-                std::string name = fmt::format("{} {}", SDL_JoystickName(joy), joystick->GetPort());
+                    fmt::format("{} {}", "Nintendo Dual Joy-Con", joystick->GetPort());
                 devices.emplace_back(Common::ParamPackage{
                     {"class", "sdl"},
                     {"display", std::move(name)},
                     {"guid", joystick->GetGUID()},
+                    {"guid2", joystick2->GetGUID()},
                     {"port", std::to_string(joystick->GetPort())},
                 });
             }
@@ -881,17 +924,6 @@ std::vector<Common::ParamPackage> SDLState::GetInputDevices() {
     return devices;
 }
 
-std::string SDLState::GetControllerName(SDL_GameController* controller) const {
-    switch (SDL_GameControllerGetType(controller)) {
-    case SDL_CONTROLLER_TYPE_XBOX360:
-        return "XBox 360 Controller";
-    case SDL_CONTROLLER_TYPE_XBOXONE:
-        return "XBox One Controller";
-    default:
-        return SDL_GameControllerName(controller);
-    }
-}
-
 namespace {
 Common::ParamPackage BuildAnalogParamPackageForButton(int port, std::string guid, s32 axis,
                                                       float value = 0.1f) {
@@ -1071,24 +1103,48 @@ ButtonMapping SDLState::GetButtonMappingForDevice(const Common::ParamPackage& pa
         return {};
     }
     const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
+
     auto* controller = joystick->GetSDLGameController();
     if (controller == nullptr) {
         return {};
     }
 
-    const bool invert =
-        SDL_GameControllerGetType(controller) != SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO;
-
     // This list is missing ZL/ZR since those are not considered buttons in SDL GameController.
     // We will add those afterwards
     // This list also excludes Screenshot since theres not really a mapping for that
-    using ButtonBindings =
-        std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerButton>, 17>;
-    const ButtonBindings switch_to_sdl_button{{
-        {Settings::NativeButton::A, invert ? SDL_CONTROLLER_BUTTON_B : SDL_CONTROLLER_BUTTON_A},
-        {Settings::NativeButton::B, invert ? SDL_CONTROLLER_BUTTON_A : SDL_CONTROLLER_BUTTON_B},
-        {Settings::NativeButton::X, invert ? SDL_CONTROLLER_BUTTON_Y : SDL_CONTROLLER_BUTTON_X},
-        {Settings::NativeButton::Y, invert ? SDL_CONTROLLER_BUTTON_X : SDL_CONTROLLER_BUTTON_Y},
+    ButtonBindings switch_to_sdl_button;
+
+    if (SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO) {
+        switch_to_sdl_button = GetNintendoButtonBinding(joystick);
+    } else {
+        switch_to_sdl_button = GetDefaultButtonBinding();
+    }
+
+    // Add the missing bindings for ZL/ZR
+    static constexpr ZButtonBindings switch_to_sdl_axis{{
+        {Settings::NativeButton::ZL, SDL_CONTROLLER_AXIS_TRIGGERLEFT},
+        {Settings::NativeButton::ZR, SDL_CONTROLLER_AXIS_TRIGGERRIGHT},
+    }};
+
+    // Parameters contain two joysticks return dual
+    if (params.Has("guid2")) {
+        const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
+
+        if (joystick2->GetSDLGameController() != nullptr) {
+            return GetDualControllerMapping(joystick, joystick2, switch_to_sdl_button,
+                                            switch_to_sdl_axis);
+        }
+    }
+
+    return GetSingleControllerMapping(joystick, switch_to_sdl_button, switch_to_sdl_axis);
+}
+
+ButtonBindings SDLState::GetDefaultButtonBinding() const {
+    return {
+        std::pair{Settings::NativeButton::A, SDL_CONTROLLER_BUTTON_B},
+        {Settings::NativeButton::B, SDL_CONTROLLER_BUTTON_A},
+        {Settings::NativeButton::X, SDL_CONTROLLER_BUTTON_Y},
+        {Settings::NativeButton::Y, SDL_CONTROLLER_BUTTON_X},
         {Settings::NativeButton::LStick, SDL_CONTROLLER_BUTTON_LEFTSTICK},
         {Settings::NativeButton::RStick, SDL_CONTROLLER_BUTTON_RIGHTSTICK},
         {Settings::NativeButton::L, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
@@ -1102,18 +1158,51 @@ ButtonMapping SDLState::GetButtonMappingForDevice(const Common::ParamPackage& pa
         {Settings::NativeButton::SL, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
         {Settings::NativeButton::SR, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
         {Settings::NativeButton::Home, SDL_CONTROLLER_BUTTON_GUIDE},
-    }};
+    };
+}
 
-    // Add the missing bindings for ZL/ZR
-    using ZBindings =
-        std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerAxis>, 2>;
-    static constexpr ZBindings switch_to_sdl_axis{{
-        {Settings::NativeButton::ZL, SDL_CONTROLLER_AXIS_TRIGGERLEFT},
-        {Settings::NativeButton::ZR, SDL_CONTROLLER_AXIS_TRIGGERRIGHT},
-    }};
+ButtonBindings SDLState::GetNintendoButtonBinding(
+    const std::shared_ptr<SDLJoystick>& joystick) const {
+    // Default SL/SR mapping for pro controllers
+    auto sl_button = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
+    auto sr_button = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
+
+    if (joystick->IsJoyconLeft()) {
+        sl_button = SDL_CONTROLLER_BUTTON_PADDLE2;
+        sr_button = SDL_CONTROLLER_BUTTON_PADDLE4;
+    }
+    if (joystick->IsJoyconRight()) {
+        sl_button = SDL_CONTROLLER_BUTTON_PADDLE3;
+        sr_button = SDL_CONTROLLER_BUTTON_PADDLE1;
+    }
 
+    return {
+        std::pair{Settings::NativeButton::A, SDL_CONTROLLER_BUTTON_A},
+        {Settings::NativeButton::B, SDL_CONTROLLER_BUTTON_B},
+        {Settings::NativeButton::X, SDL_CONTROLLER_BUTTON_X},
+        {Settings::NativeButton::Y, SDL_CONTROLLER_BUTTON_Y},
+        {Settings::NativeButton::LStick, SDL_CONTROLLER_BUTTON_LEFTSTICK},
+        {Settings::NativeButton::RStick, SDL_CONTROLLER_BUTTON_RIGHTSTICK},
+        {Settings::NativeButton::L, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
+        {Settings::NativeButton::R, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
+        {Settings::NativeButton::Plus, SDL_CONTROLLER_BUTTON_START},
+        {Settings::NativeButton::Minus, SDL_CONTROLLER_BUTTON_BACK},
+        {Settings::NativeButton::DLeft, SDL_CONTROLLER_BUTTON_DPAD_LEFT},
+        {Settings::NativeButton::DUp, SDL_CONTROLLER_BUTTON_DPAD_UP},
+        {Settings::NativeButton::DRight, SDL_CONTROLLER_BUTTON_DPAD_RIGHT},
+        {Settings::NativeButton::DDown, SDL_CONTROLLER_BUTTON_DPAD_DOWN},
+        {Settings::NativeButton::SL, sl_button},
+        {Settings::NativeButton::SR, sr_button},
+        {Settings::NativeButton::Home, SDL_CONTROLLER_BUTTON_GUIDE},
+    };
+}
+
+ButtonMapping SDLState::GetSingleControllerMapping(
+    const std::shared_ptr<SDLJoystick>& joystick, const ButtonBindings& switch_to_sdl_button,
+    const ZButtonBindings& switch_to_sdl_axis) const {
     ButtonMapping mapping;
     mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
+    auto* controller = joystick->GetSDLGameController();
 
     for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
         const auto& binding = SDL_GameControllerGetBindForButton(controller, sdl_button);
@@ -1131,11 +1220,68 @@ ButtonMapping SDLState::GetButtonMappingForDevice(const Common::ParamPackage& pa
     return mapping;
 }
 
+ButtonMapping SDLState::GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
+                                                 const std::shared_ptr<SDLJoystick>& joystick2,
+                                                 const ButtonBindings& switch_to_sdl_button,
+                                                 const ZButtonBindings& switch_to_sdl_axis) const {
+    ButtonMapping mapping;
+    mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
+    auto* controller = joystick->GetSDLGameController();
+    auto* controller2 = joystick2->GetSDLGameController();
+
+    for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
+        if (IsButtonOnLeftSide(switch_button)) {
+            const auto& binding = SDL_GameControllerGetBindForButton(controller2, sdl_button);
+            mapping.insert_or_assign(
+                switch_button,
+                BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
+            continue;
+        }
+        const auto& binding = SDL_GameControllerGetBindForButton(controller, sdl_button);
+        mapping.insert_or_assign(
+            switch_button,
+            BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
+    }
+    for (const auto& [switch_button, sdl_axis] : switch_to_sdl_axis) {
+        if (IsButtonOnLeftSide(switch_button)) {
+            const auto& binding = SDL_GameControllerGetBindForAxis(controller2, sdl_axis);
+            mapping.insert_or_assign(
+                switch_button,
+                BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
+            continue;
+        }
+        const auto& binding = SDL_GameControllerGetBindForAxis(controller, sdl_axis);
+        mapping.insert_or_assign(
+            switch_button,
+            BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
+    }
+
+    return mapping;
+}
+
+bool SDLState::IsButtonOnLeftSide(Settings::NativeButton::Values button) const {
+    switch (button) {
+    case Settings::NativeButton::DDown:
+    case Settings::NativeButton::DLeft:
+    case Settings::NativeButton::DRight:
+    case Settings::NativeButton::DUp:
+    case Settings::NativeButton::L:
+    case Settings::NativeButton::LStick:
+    case Settings::NativeButton::Minus:
+    case Settings::NativeButton::Screenshot:
+    case Settings::NativeButton::ZL:
+        return true;
+    default:
+        return false;
+    }
+}
+
 AnalogMapping SDLState::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
     if (!params.Has("guid") || !params.Has("port")) {
         return {};
     }
     const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
+    const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
     auto* controller = joystick->GetSDLGameController();
     if (controller == nullptr) {
         return {};
@@ -1146,10 +1292,17 @@ AnalogMapping SDLState::GetAnalogMappingForDevice(const Common::ParamPackage& pa
         SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX);
     const auto& binding_left_y =
         SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY);
-    mapping.insert_or_assign(Settings::NativeAnalog::LStick,
-                             BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(),
-                                                        binding_left_x.value.axis,
-                                                        binding_left_y.value.axis));
+    if (params.Has("guid2")) {
+        mapping.insert_or_assign(
+            Settings::NativeAnalog::LStick,
+            BuildParamPackageForAnalog(joystick2->GetPort(), joystick2->GetGUID(),
+                                       binding_left_x.value.axis, binding_left_y.value.axis));
+    } else {
+        mapping.insert_or_assign(
+            Settings::NativeAnalog::LStick,
+            BuildParamPackageForAnalog(joystick->GetPort(), joystick->GetGUID(),
+                                       binding_left_x.value.axis, binding_left_y.value.axis));
+    }
     const auto& binding_right_x =
         SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX);
     const auto& binding_right_y =
@@ -1166,20 +1319,32 @@ MotionMapping SDLState::GetMotionMappingForDevice(const Common::ParamPackage& pa
         return {};
     }
     const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
+    const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
     auto* controller = joystick->GetSDLGameController();
     if (controller == nullptr) {
         return {};
     }
 
+    MotionMapping mapping = {};
     joystick->EnableMotion();
 
-    if (!joystick->HasGyro() && !joystick->HasAccel()) {
-        return {};
+    if (joystick->HasGyro() || joystick->HasAccel()) {
+        mapping.insert_or_assign(Settings::NativeMotion::MotionRight,
+                                 BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
+    }
+    if (params.Has("guid2")) {
+        joystick2->EnableMotion();
+        if (joystick2->HasGyro() || joystick2->HasAccel()) {
+            mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
+                                     BuildMotionParam(joystick2->GetPort(), joystick2->GetGUID()));
+        }
+    } else {
+        if (joystick->HasGyro() || joystick->HasAccel()) {
+            mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
+                                     BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
+        }
     }
 
-    MotionMapping mapping = {};
-    mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
-                             BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
     return mapping;
 }
 namespace Polling {
diff --git a/src/input_common/sdl/sdl_impl.h b/src/input_common/sdl/sdl_impl.h
index 121e01913..b77afcbd8 100644
--- a/src/input_common/sdl/sdl_impl.h
+++ b/src/input_common/sdl/sdl_impl.h
@@ -9,6 +9,17 @@
 #include <mutex>
 #include <thread>
 #include <unordered_map>
+
+// Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
+#endif
+#include <SDL.h>
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+
 #include "common/common_types.h"
 #include "common/threadsafe_queue.h"
 #include "input_common/sdl/sdl.h"
@@ -18,6 +29,11 @@ using SDL_GameController = struct _SDL_GameController;
 using SDL_Joystick = struct _SDL_Joystick;
 using SDL_JoystickID = s32;
 
+using ButtonBindings =
+    std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerButton>, 17>;
+using ZButtonBindings =
+    std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerAxis>, 2>;
+
 namespace InputCommon::SDL {
 
 class SDLAnalogFactory;
@@ -66,8 +82,25 @@ private:
     /// Needs to be called before SDL_QuitSubSystem.
     void CloseJoysticks();
 
-    /// Returns a custom name for specific controllers because the default name is not correct
-    std::string GetControllerName(SDL_GameController* controller) const;
+    /// Returns the default button bindings list for generic controllers
+    ButtonBindings GetDefaultButtonBinding() const;
+
+    /// Returns the default button bindings list for nintendo controllers
+    ButtonBindings GetNintendoButtonBinding(const std::shared_ptr<SDLJoystick>& joystick) const;
+
+    /// Returns the button mappings from a single controller
+    ButtonMapping GetSingleControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
+                                             const ButtonBindings& switch_to_sdl_button,
+                                             const ZButtonBindings& switch_to_sdl_axis) const;
+
+    /// Returns the button mappings from two different controllers
+    ButtonMapping GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
+                                           const std::shared_ptr<SDLJoystick>& joystick2,
+                                           const ButtonBindings& switch_to_sdl_button,
+                                           const ZButtonBindings& switch_to_sdl_axis) const;
+
+    /// Returns true if the button is on the left joycon
+    bool IsButtonOnLeftSide(Settings::NativeButton::Values button) const;
 
     // Set to true if SDL supports game controller subsystem
     bool has_gamecontroller = false;
-- 
cgit v1.2.3