// Copyright 2020 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include #include "common/threadsafe_queue.h" #include "input_common/gcadapter/gc_adapter.h" #include "input_common/gcadapter/gc_poller.h" namespace InputCommon { class GCButton final : public Input::ButtonDevice { public: explicit GCButton(int port_, int button_, GCAdapter::Adapter* adapter) : port(port_), button(button_), gcadapter(adapter) {} ~GCButton() override; bool GetStatus() const override { return gcadapter->GetPadState()[port].buttons.at(button); } private: const int port; const int button; GCAdapter::Adapter* gcadapter; }; class GCAxisButton final : public Input::ButtonDevice { public: explicit GCAxisButton(int port_, int axis_, float threshold_, bool trigger_if_greater_, GCAdapter::Adapter* adapter) : port(port_), axis(axis_), threshold(threshold_), trigger_if_greater(trigger_if_greater_), gcadapter(adapter), origin_value(adapter->GetOriginValue(port_, axis_)) {} bool GetStatus() const override { const float current_axis_value = gcadapter->GetPadState()[port].axes.at(axis); const float axis_value = (current_axis_value - origin_value) / 128.0f; if (trigger_if_greater) { // TODO: Might be worthwile to set a slider for the trigger threshold. It is currently // always set to 0.5 in configure_input_player.cpp ZL/ZR HandleClick return axis_value > threshold; } return axis_value < -threshold; } private: const int port; const int axis; float threshold; bool trigger_if_greater; GCAdapter::Adapter* gcadapter; const float origin_value; }; GCButtonFactory::GCButtonFactory(std::shared_ptr adapter_) : adapter(std::move(adapter_)) {} GCButton::~GCButton() = default; std::unique_ptr GCButtonFactory::Create(const Common::ParamPackage& params) { const int button_id = params.Get("button", 0); const int port = params.Get("port", 0); constexpr int PAD_STICK_ID = static_cast(GCAdapter::PadButton::PAD_STICK); // button is not an axis/stick button if (button_id != PAD_STICK_ID) { auto button = std::make_unique(port, button_id, adapter.get()); return std::move(button); } // For Axis buttons, used by the binary sticks. if (button_id == PAD_STICK_ID) { const int axis = params.Get("axis", 0); const float threshold = params.Get("threshold", 0.25f); const std::string direction_name = params.Get("direction", ""); bool trigger_if_greater; if (direction_name == "+") { trigger_if_greater = true; } else if (direction_name == "-") { trigger_if_greater = false; } else { trigger_if_greater = true; LOG_ERROR(Input, "Unknown direction {}", direction_name); } return std::make_unique(port, axis, threshold, trigger_if_greater, adapter.get()); } } Common::ParamPackage GCButtonFactory::GetNextInput() { Common::ParamPackage params; GCAdapter::GCPadStatus pad; auto& queue = adapter->GetPadQueue(); for (std::size_t port = 0; port < queue.size(); ++port) { while (queue[port].Pop(pad)) { // This while loop will break on the earliest detected button params.Set("engine", "gcpad"); params.Set("port", static_cast(port)); for (const auto& button : GCAdapter::PadButtonArray) { const u16 button_value = static_cast(button); if (pad.button & button_value) { params.Set("button", button_value); break; } } // For Axis button implementation if (pad.axis != GCAdapter::PadAxes::Undefined) { params.Set("axis", static_cast(pad.axis)); params.Set("button", static_cast(GCAdapter::PadButton::PAD_STICK)); if (pad.axis_value > 128) { params.Set("direction", "+"); params.Set("threshold", "0.25"); } else { params.Set("direction", "-"); params.Set("threshold", "-0.25"); } break; } } } return params; } void GCButtonFactory::BeginConfiguration() { polling = true; adapter->BeginConfiguration(); } void GCButtonFactory::EndConfiguration() { polling = false; adapter->EndConfiguration(); } class GCAnalog final : public Input::AnalogDevice { public: GCAnalog(int port_, int axis_x_, int axis_y_, float deadzone_, GCAdapter::Adapter* adapter) : port(port_), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_), gcadapter(adapter), origin_value_x(adapter->GetOriginValue(port_, axis_x_)), origin_value_y(adapter->GetOriginValue(port_, axis_y_)) {} float GetAxis(int axis) const { std::lock_guard lock{mutex}; const auto origin_value = axis % 2 == 0 ? origin_value_x : origin_value_y; // division is not by a perfect 128 to account for some variance in center location // e.g. my device idled at 131 in X, 120 in Y, and full range of motion was in range // [20-230] return (gcadapter->GetPadState()[port].axes.at(axis) - origin_value) / 95.0f; } std::pair GetAnalog(int axis_x, int axis_y) const { float x = GetAxis(axis_x); float y = GetAxis(axis_y); // Make sure the coordinates are in the unit circle, // otherwise normalize it. float r = x * x + y * y; if (r > 1.0f) { r = std::sqrt(r); x /= r; y /= r; } return {x, y}; } std::tuple GetStatus() const override { const auto [x, y] = GetAnalog(axis_x, axis_y); const float r = std::sqrt((x * x) + (y * y)); if (r > deadzone) { return {x / r * (r - deadzone) / (1 - deadzone), y / r * (r - deadzone) / (1 - deadzone)}; } return {0.0f, 0.0f}; } bool GetAnalogDirectionStatus(Input::AnalogDirection direction) const override { const auto [x, y] = GetStatus(); const float directional_deadzone = 0.4f; switch (direction) { case Input::AnalogDirection::RIGHT: return x > directional_deadzone; case Input::AnalogDirection::LEFT: return x < -directional_deadzone; case Input::AnalogDirection::UP: return y > directional_deadzone; case Input::AnalogDirection::DOWN: return y < -directional_deadzone; } return false; } private: const int port; const int axis_x; const int axis_y; const float deadzone; GCAdapter::Adapter* gcadapter; const float origin_value_x; const float origin_value_y; mutable std::mutex mutex; }; /// An analog device factory that creates analog devices from GC Adapter GCAnalogFactory::GCAnalogFactory(std::shared_ptr adapter_) : adapter(std::move(adapter_)) {} /** * Creates analog device from joystick axes * @param params contains parameters for creating the device: * - "port": the nth gcpad on the adapter * - "axis_x": the index of the axis to be bind as x-axis * - "axis_y": the index of the axis to be bind as y-axis */ std::unique_ptr GCAnalogFactory::Create(const Common::ParamPackage& params) { const int port = params.Get("port", 0); const int axis_x = params.Get("axis_x", 0); const int axis_y = params.Get("axis_y", 1); const float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, .99f); return std::make_unique(port, axis_x, axis_y, deadzone, adapter.get()); } void GCAnalogFactory::BeginConfiguration() { polling = true; adapter->BeginConfiguration(); } void GCAnalogFactory::EndConfiguration() { polling = false; adapter->EndConfiguration(); } Common::ParamPackage GCAnalogFactory::GetNextInput() { GCAdapter::GCPadStatus pad; auto& queue = adapter->GetPadQueue(); for (std::size_t port = 0; port < queue.size(); ++port) { while (queue[port].Pop(pad)) { if (pad.axis == GCAdapter::PadAxes::Undefined || std::abs((pad.axis_value - 128.0f) / 128.0f) < 0.1) { continue; } // An analog device needs two axes, so we need to store the axis for later and wait for // a second input event. The axes also must be from the same joystick. const u8 axis = static_cast(pad.axis); if (analog_x_axis == -1) { analog_x_axis = axis; controller_number = port; } else if (analog_y_axis == -1 && analog_x_axis != axis && controller_number == port) { analog_y_axis = axis; } } } Common::ParamPackage params; if (analog_x_axis != -1 && analog_y_axis != -1) { params.Set("engine", "gcpad"); params.Set("port", controller_number); params.Set("axis_x", analog_x_axis); params.Set("axis_y", analog_y_axis); analog_x_axis = -1; analog_y_axis = -1; controller_number = -1; return params; } return params; } } // namespace InputCommon