mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2025-01-18 06:25:37 +00:00
Support for SDL Joysticks
This enables use of non-controller peripherals, such as DirectInput steering wheels or flight sticks
This commit is contained in:
parent
99ec667b20
commit
2880b71b48
|
@ -63,9 +63,8 @@ void NeGcon::SetAxisState(s32 axis_code, float value)
|
|||
return;
|
||||
}
|
||||
|
||||
// I, II, L: 0..1 -> 0..255 or -1..0 -> 0..255 to support negative axis ranges,
|
||||
// e.g. if bound to analog stick instead of trigger
|
||||
const u8 u8_value = static_cast<u8>(std::clamp(std::abs(value) * 255.0f, 0.0f, 255.0f));
|
||||
// I, II, L: -1..1 -> 0..255
|
||||
const u8 u8_value = static_cast<u8>(std::clamp(((value + 1.0f) / 2.0f) * 255.0f, 0.0f, 255.0f));
|
||||
|
||||
SetAxisState(static_cast<Axis>(axis_code), u8_value);
|
||||
}
|
||||
|
|
|
@ -128,16 +128,18 @@ void InputBindingDialog::addNewBinding(std::string new_binding)
|
|||
saveListToSettings();
|
||||
}
|
||||
|
||||
void InputBindingDialog::bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive)
|
||||
void InputBindingDialog::bindToControllerAxis(int controller_index, int axis_index, bool inverted,
|
||||
std::optional<bool> half_axis_positive)
|
||||
{
|
||||
const char* invert_char = inverted ? "-" : "";
|
||||
const char* sign_char = "";
|
||||
if (positive)
|
||||
if (half_axis_positive)
|
||||
{
|
||||
sign_char = *positive ? "+" : "-";
|
||||
sign_char = *half_axis_positive ? "+" : "-";
|
||||
}
|
||||
|
||||
std::string binding =
|
||||
StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index);
|
||||
StringUtil::StdStringFromFormat("Controller%d/%sAxis%d%s", controller_index, sign_char, axis_index, invert_char);
|
||||
addNewBinding(std::move(binding));
|
||||
stopListeningForInput();
|
||||
}
|
||||
|
@ -149,6 +151,14 @@ void InputBindingDialog::bindToControllerButton(int controller_index, int button
|
|||
stopListeningForInput();
|
||||
}
|
||||
|
||||
void InputBindingDialog::bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction)
|
||||
{
|
||||
std::string binding = StringUtil::StdStringFromFormat("Controller%d/Hat%d %s", controller_index, hat_index,
|
||||
hat_direction.toLatin1().constData());
|
||||
addNewBinding(std::move(binding));
|
||||
stopListeningForInput();
|
||||
}
|
||||
|
||||
void InputBindingDialog::onAddBindingButtonClicked()
|
||||
{
|
||||
if (isListeningForInput())
|
||||
|
|
|
@ -19,8 +19,10 @@ public:
|
|||
~InputBindingDialog();
|
||||
|
||||
protected Q_SLOTS:
|
||||
void bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive);
|
||||
void bindToControllerAxis(int controller_index, int axis_index, bool inverted,
|
||||
std::optional<bool> half_axis_positive);
|
||||
void bindToControllerButton(int controller_index, int button_index);
|
||||
void bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction);
|
||||
void onAddBindingButtonClicked();
|
||||
void onRemoveBindingButtonClicked();
|
||||
void onClearBindingsButtonClicked();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "inputbindingmonitor.h"
|
||||
#include <cmath>
|
||||
|
||||
ControllerInterface::Hook::CallbackResult
|
||||
InputButtonBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
|
||||
|
@ -6,20 +7,32 @@ InputButtonBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
|
|||
if (ei.type == ControllerInterface::Hook::Type::Axis)
|
||||
{
|
||||
// wait until it's at least half pushed so we don't get confused between axises with small movement
|
||||
if (std::abs(ei.value) < 0.5f)
|
||||
if (std::abs(std::get<float>(ei.value)) < 0.5f)
|
||||
return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
|
||||
|
||||
// TODO: this probably should consider the "last value"
|
||||
QMetaObject::invokeMethod(m_parent, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
|
||||
Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, ei.value > 0));
|
||||
Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, false),
|
||||
Q_ARG(std::optional<bool>, std::get<float>(ei.value) > 0));
|
||||
return ControllerInterface::Hook::CallbackResult::StopMonitoring;
|
||||
}
|
||||
else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
|
||||
else if (ei.type == ControllerInterface::Hook::Type::Button && std::get<float>(ei.value) > 0.0f)
|
||||
{
|
||||
QMetaObject::invokeMethod(m_parent, "bindToControllerButton", Q_ARG(int, ei.controller_index),
|
||||
Q_ARG(int, ei.button_or_axis_number));
|
||||
return ControllerInterface::Hook::CallbackResult::StopMonitoring;
|
||||
}
|
||||
else if (ei.type == ControllerInterface::Hook::Type::Hat)
|
||||
{
|
||||
const std::string_view hat_position = std::get<std::string_view>(ei.value);
|
||||
if (!hat_position.empty())
|
||||
{
|
||||
QString str = QString::fromLatin1(hat_position.data(), static_cast<int>(hat_position.size()));
|
||||
QMetaObject::invokeMethod(m_parent, "bindToControllerHat", Q_ARG(int, ei.controller_index),
|
||||
Q_ARG(int, ei.button_or_axis_number), Q_ARG(QString, std::move(str)));
|
||||
return ControllerInterface::Hook::CallbackResult::StopMonitoring;
|
||||
}
|
||||
}
|
||||
|
||||
return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
|
||||
}
|
||||
|
@ -28,16 +41,17 @@ ControllerInterface::Hook::CallbackResult InputAxisBindingMonitor::operator()(co
|
|||
{
|
||||
if (ei.type == ControllerInterface::Hook::Type::Axis)
|
||||
{
|
||||
// wait until it's at least half pushed so we don't get confused between axises with small movement
|
||||
if (std::abs(ei.value) < 0.5f)
|
||||
std::optional<bool> half_axis_positive, inverted;
|
||||
if (!ProcessAxisInput(ei, half_axis_positive, inverted))
|
||||
return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
|
||||
|
||||
QMetaObject::invokeMethod(m_parent, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
|
||||
Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, std::nullopt));
|
||||
Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, inverted.value_or(false)),
|
||||
Q_ARG(std::optional<bool>, half_axis_positive));
|
||||
return ControllerInterface::Hook::CallbackResult::StopMonitoring;
|
||||
}
|
||||
else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half &&
|
||||
ei.value > 0.0f)
|
||||
std::get<float>(ei.value) > 0.0f)
|
||||
{
|
||||
QMetaObject::invokeMethod(m_parent, "bindToControllerButton", Q_ARG(int, ei.controller_index),
|
||||
Q_ARG(int, ei.button_or_axis_number));
|
||||
|
@ -47,10 +61,84 @@ ControllerInterface::Hook::CallbackResult InputAxisBindingMonitor::operator()(co
|
|||
return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
|
||||
}
|
||||
|
||||
bool InputAxisBindingMonitor::ProcessAxisInput(const ControllerInterface::Hook& ei,
|
||||
std::optional<bool>& half_axis_positive,
|
||||
std::optional<bool>& inverted) const
|
||||
{
|
||||
const float value = std::get<float>(ei.value);
|
||||
|
||||
if (!ei.track_history) // Keyboard, mouse, game controller
|
||||
{
|
||||
// wait until it's at least half pushed so we don't get confused between axises with small movement
|
||||
if (std::abs(value) < 0.5f)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
else // Joystick
|
||||
{
|
||||
auto& history = m_context->m_inputs_history;
|
||||
// Reject inputs coming from multiple sources
|
||||
if (!history.empty())
|
||||
{
|
||||
const auto& item = history.front();
|
||||
if (ei.controller_index != item.controller_index || ei.button_or_axis_number != item.axis_number)
|
||||
return false;
|
||||
}
|
||||
history.push_back({ei.controller_index, ei.button_or_axis_number, value});
|
||||
return AnalyzeInputHistory(half_axis_positive, inverted);
|
||||
}
|
||||
}
|
||||
|
||||
bool InputAxisBindingMonitor::AnalyzeInputHistory(std::optional<bool>& half_axis_positive,
|
||||
std::optional<bool>& inverted) const
|
||||
{
|
||||
const auto& history = m_context->m_inputs_history;
|
||||
const auto [min, max] = std::minmax_element(
|
||||
history.begin(), history.end(), [](const auto& left, const auto& right) { return left.value < right.value; });
|
||||
|
||||
// Ignore small input magnitudes
|
||||
if (std::abs(max->value - min->value) < 0.5f)
|
||||
return false;
|
||||
|
||||
// Used heuristics:
|
||||
// * If history contains inputs with both - and + sign (ignoring 0), bind a full axis
|
||||
// * If history contains only 0 and inputs of the same sign AND maxes out at 1.0/-1.0, bind a half axis
|
||||
// * Use the direction of input changes to determine whether the axis is inverted or not
|
||||
if (std::signbit(min->value) != std::signbit(max->value))
|
||||
{
|
||||
if (min->value != 0.0f && max->value != 0.0f)
|
||||
{
|
||||
// If max value comes before the min value, invert the half axis
|
||||
if (m_axis_type == Controller::AxisType::Half)
|
||||
{
|
||||
inverted = std::distance(min, max) < 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((std::abs(min->value) > 0.99f || std::abs(max->value) > 0.99f) &&
|
||||
(std::abs(min->value) < 0.01f || std::abs(max->value) < 0.01f))
|
||||
{
|
||||
|
||||
if (m_axis_type == Controller::AxisType::Half)
|
||||
{
|
||||
half_axis_positive = max->value > 0.0f;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ControllerInterface::Hook::CallbackResult
|
||||
InputRumbleBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
|
||||
{
|
||||
if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
|
||||
if (ei.type == ControllerInterface::Hook::Type::Button && std::get<float>(ei.value) > 0.0f)
|
||||
{
|
||||
QMetaObject::invokeMethod(m_parent, "bindToControllerRumble", Q_ARG(int, ei.controller_index));
|
||||
return ControllerInterface::Hook::CallbackResult::StopMonitoring;
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#include "frontend-common/controller_interface.h"
|
||||
#include <QtCore/QObject>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
// NOTE: Those Monitor classes must be copyable to meet the requirements of std::function, but at the same time we want
|
||||
// copies to be opaque to the caling code and share context. Therefore, all mutable context of the monitor (if required)
|
||||
|
@ -29,8 +31,25 @@ public:
|
|||
ControllerInterface::Hook::CallbackResult operator()(const ControllerInterface::Hook& ei) const;
|
||||
|
||||
private:
|
||||
bool ProcessAxisInput(const ControllerInterface::Hook& ei, std::optional<bool>& half_axis_positive,
|
||||
std::optional<bool>& inverted) const;
|
||||
bool AnalyzeInputHistory(std::optional<bool>& half_axis_positive, std::optional<bool>& inverted) const;
|
||||
|
||||
struct Context
|
||||
{
|
||||
struct History
|
||||
{
|
||||
int controller_index;
|
||||
int axis_number;
|
||||
float value;
|
||||
};
|
||||
|
||||
std::vector<History> m_inputs_history;
|
||||
};
|
||||
|
||||
QObject* m_parent;
|
||||
Controller::AxisType m_axis_type;
|
||||
std::shared_ptr<Context> m_context = std::make_shared<Context>();
|
||||
};
|
||||
|
||||
class InputRumbleBindingMonitor
|
||||
|
|
|
@ -41,16 +41,18 @@ void InputBindingWidget::updateText()
|
|||
setText(QString::fromStdString(m_bindings[0]));
|
||||
}
|
||||
|
||||
void InputBindingWidget::bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive)
|
||||
void InputBindingWidget::bindToControllerAxis(int controller_index, int axis_index, bool inverted,
|
||||
std::optional<bool> half_axis_positive)
|
||||
{
|
||||
const char* invert_char = inverted ? "-" : "";
|
||||
const char* sign_char = "";
|
||||
if (positive)
|
||||
if (half_axis_positive)
|
||||
{
|
||||
sign_char = *positive ? "+" : "-";
|
||||
sign_char = *half_axis_positive ? "+" : "-";
|
||||
}
|
||||
|
||||
m_new_binding_value =
|
||||
StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index);
|
||||
StringUtil::StdStringFromFormat("Controller%d/%sAxis%d%s", controller_index, sign_char, axis_index, invert_char);
|
||||
setNewBinding();
|
||||
stopListeningForInput();
|
||||
}
|
||||
|
@ -62,6 +64,14 @@ void InputBindingWidget::bindToControllerButton(int controller_index, int button
|
|||
stopListeningForInput();
|
||||
}
|
||||
|
||||
void InputBindingWidget::bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction)
|
||||
{
|
||||
m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Hat%d %s", controller_index, hat_index,
|
||||
hat_direction.toLatin1().constData());
|
||||
setNewBinding();
|
||||
stopListeningForInput();
|
||||
}
|
||||
|
||||
void InputBindingWidget::beginRebindAll()
|
||||
{
|
||||
m_is_binding_all = true;
|
||||
|
|
|
@ -20,8 +20,10 @@ public:
|
|||
ALWAYS_INLINE void setNextWidget(InputBindingWidget* widget) { m_next_widget = widget; }
|
||||
|
||||
public Q_SLOTS:
|
||||
void bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive);
|
||||
void bindToControllerAxis(int controller_index, int axis_index, bool inverted,
|
||||
std::optional<bool> half_axis_positive);
|
||||
void bindToControllerButton(int controller_index, int button_index);
|
||||
void bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction);
|
||||
void beginRebindAll();
|
||||
void clearBinding();
|
||||
void reloadBinding();
|
||||
|
|
|
@ -1242,6 +1242,30 @@ bool CommonHostInterface::AddButtonToInputMap(const std::string& binding, const
|
|||
|
||||
return true;
|
||||
}
|
||||
else if (StringUtil::StartsWith(button, "Hat"))
|
||||
{
|
||||
const std::optional<int> hat_index = StringUtil::FromChars<int>(button.substr(3));
|
||||
const std::optional<std::string_view> hat_direction = [](const auto& button) {
|
||||
std::optional<std::string_view> result;
|
||||
|
||||
const size_t pos = button.find(' ');
|
||||
if (pos != button.npos)
|
||||
{
|
||||
result = button.substr(pos + 1);
|
||||
}
|
||||
return result;
|
||||
}(button);
|
||||
|
||||
if (!hat_index || !hat_direction ||
|
||||
!m_controller_interface->BindControllerHatToButton(*controller_index, *hat_index, *hat_direction,
|
||||
std::move(handler)))
|
||||
{
|
||||
Log_WarningPrintf("Failed to bind controller hat '%s' to button", binding.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Log_WarningPrintf("Malformed controller binding '%s' in button", binding.c_str());
|
||||
return false;
|
||||
|
@ -1266,7 +1290,8 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
|
|||
return false;
|
||||
}
|
||||
|
||||
m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler));
|
||||
m_keyboard_input_handlers.emplace(key_id.value(),
|
||||
[cb = std::move(handler)](bool pressed) { cb(pressed ? 1.0f : -1.0f); });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1281,7 +1306,8 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
|
|||
return false;
|
||||
}
|
||||
|
||||
m_mouse_input_handlers.emplace(static_cast<HostMouseButton>(button_index.value()), std::move(handler));
|
||||
m_mouse_input_handlers.emplace(static_cast<HostMouseButton>(button_index.value()),
|
||||
[cb = std::move(handler)](bool pressed) { cb(pressed ? 1.0f : -1.0f); });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1305,18 +1331,39 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
|
|||
return false;
|
||||
}
|
||||
|
||||
if (StringUtil::StartsWith(axis, "Axis"))
|
||||
if (StringUtil::StartsWith(axis, "Axis") || StringUtil::StartsWith(axis, "+Axis") ||
|
||||
StringUtil::StartsWith(axis, "-Axis"))
|
||||
{
|
||||
const std::optional<int> axis_index = StringUtil::FromChars<int>(axis.substr(4));
|
||||
if (!axis_index ||
|
||||
!m_controller_interface->BindControllerAxis(*controller_index, *axis_index, std::move(handler)))
|
||||
const std::optional<int> axis_index =
|
||||
StringUtil::FromChars<int>(axis.substr(axis[0] == '+' || axis[0] == '-' ? 5 : 4));
|
||||
if (axis_index)
|
||||
{
|
||||
ControllerInterface::AxisSide axis_side = ControllerInterface::AxisSide::Full;
|
||||
if (axis[0] == '+')
|
||||
axis_side = ControllerInterface::AxisSide::Positive;
|
||||
else if (axis[0] == '-')
|
||||
axis_side = ControllerInterface::AxisSide::Negative;
|
||||
|
||||
const bool inverted = StringUtil::EndsWith(axis, "-");
|
||||
if (!inverted)
|
||||
{
|
||||
if (m_controller_interface->BindControllerAxis(*controller_index, *axis_index, axis_side, std::move(handler)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_controller_interface->BindControllerAxis(*controller_index, *axis_index, axis_side,
|
||||
[cb = std::move(handler)](float value) { cb(-value); }))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log_WarningPrintf("Failed to bind controller axis '%s' to axis", binding.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (StringUtil::StartsWith(axis, "Button") && axis_type == Controller::AxisType::Half)
|
||||
{
|
||||
const std::optional<int> button_index = StringUtil::FromChars<int>(axis.substr(6));
|
||||
|
|
|
@ -36,13 +36,14 @@ void ControllerInterface::ClearHook()
|
|||
m_event_intercept_callback = {};
|
||||
}
|
||||
|
||||
bool ControllerInterface::DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, float value)
|
||||
bool ControllerInterface::DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number,
|
||||
std::variant<float, std::string_view> value, bool track_history)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||
if (!m_event_intercept_callback)
|
||||
return false;
|
||||
|
||||
const Hook ei{type, controller_index, button_or_axis_number, value};
|
||||
const Hook ei{type, controller_index, button_or_axis_number, std::move(value), track_history};
|
||||
const Hook::CallbackResult action = m_event_intercept_callback(ei);
|
||||
if (action == Hook::CallbackResult::StopMonitoring)
|
||||
m_event_intercept_callback = {};
|
||||
|
@ -64,7 +65,8 @@ void ControllerInterface::OnControllerDisconnected(int host_id)
|
|||
|
||||
void ControllerInterface::ClearBindings() {}
|
||||
|
||||
bool ControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback)
|
||||
bool ControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
class HostInterface;
|
||||
class Controller;
|
||||
|
@ -31,6 +32,13 @@ public:
|
|||
MAX_NUM_BUTTONS = 15
|
||||
};
|
||||
|
||||
enum AxisSide
|
||||
{
|
||||
Full,
|
||||
Positive,
|
||||
Negative
|
||||
};
|
||||
|
||||
using AxisCallback = CommonHostInterface::InputAxisHandler;
|
||||
using ButtonCallback = CommonHostInterface::InputButtonHandler;
|
||||
|
||||
|
@ -50,10 +58,12 @@ public:
|
|||
virtual void ClearBindings() = 0;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
virtual bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) = 0;
|
||||
virtual bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) = 0;
|
||||
virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0;
|
||||
virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) = 0;
|
||||
virtual bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) = 0;
|
||||
virtual bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) = 0;
|
||||
|
||||
virtual void PollEvents() = 0;
|
||||
|
@ -71,7 +81,8 @@ public:
|
|||
enum class Type
|
||||
{
|
||||
Axis,
|
||||
Button
|
||||
Button,
|
||||
Hat // Only for joysticks
|
||||
};
|
||||
|
||||
enum class CallbackResult
|
||||
|
@ -85,13 +96,15 @@ public:
|
|||
Type type;
|
||||
int controller_index;
|
||||
int button_or_axis_number;
|
||||
float value; // 0/1 for buttons, -1..1 for axises
|
||||
std::variant<float, std::string_view> value; // 0/1 for buttons, -1..1 for axes, hat direction name for hats
|
||||
bool track_history; // Track axis movement to spot inversion/half axes
|
||||
};
|
||||
void SetHook(Hook::Callback callback);
|
||||
void ClearHook();
|
||||
|
||||
protected:
|
||||
bool DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, float value);
|
||||
bool DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number,
|
||||
std::variant<float, std::string_view> value, bool track_history = false);
|
||||
|
||||
void OnControllerConnected(int host_id);
|
||||
void OnControllerDisconnected(int host_id);
|
||||
|
|
|
@ -106,18 +106,29 @@ bool SDLControllerInterface::ProcessSDLEvent(const SDL_Event* event)
|
|||
case SDL_CONTROLLERBUTTONUP:
|
||||
return HandleControllerButtonEvent(event);
|
||||
|
||||
case SDL_JOYDEVICEADDED:
|
||||
if (SDL_IsGameController(event->jdevice.which))
|
||||
return true;
|
||||
|
||||
Log_InfoPrintf("Joystick %d inserted", event->jdevice.which);
|
||||
OpenJoystick(event->jdevice.which);
|
||||
return true;
|
||||
|
||||
case SDL_JOYAXISMOTION:
|
||||
return HandleJoystickAxisEvent(&event->jaxis);
|
||||
|
||||
case SDL_JOYHATMOTION:
|
||||
return HandleJoystickHatEvent(&event->jhat);
|
||||
|
||||
case SDL_JOYBUTTONDOWN:
|
||||
case SDL_JOYBUTTONUP:
|
||||
return HandleJoystickButtonEvent(&event->jbutton);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SDLControllerInterface::ControllerDataVector::iterator
|
||||
SDLControllerInterface::GetControllerDataForController(void* controller)
|
||||
{
|
||||
return std::find_if(m_controllers.begin(), m_controllers.end(),
|
||||
[controller](const ControllerData& cd) { return cd.controller == controller; });
|
||||
}
|
||||
|
||||
SDLControllerInterface::ControllerDataVector::iterator SDLControllerInterface::GetControllerDataForJoystickId(int id)
|
||||
{
|
||||
return std::find_if(m_controllers.begin(), m_controllers.end(),
|
||||
|
@ -179,10 +190,10 @@ bool SDLControllerInterface::OpenGameController(int index)
|
|||
SDL_GameControllerName(gcontroller));
|
||||
|
||||
ControllerData cd = {};
|
||||
cd.controller = gcontroller;
|
||||
cd.player_id = player_id;
|
||||
cd.joystick_id = joystick_id;
|
||||
cd.haptic_left_right_effect = -1;
|
||||
cd.is_game_controller = true;
|
||||
|
||||
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
|
||||
if (haptic)
|
||||
|
@ -233,7 +244,7 @@ bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify
|
|||
if (it->haptic)
|
||||
SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));
|
||||
|
||||
SDL_GameControllerClose(static_cast<SDL_GameController*>(it->controller));
|
||||
SDL_GameControllerClose(SDL_GameControllerFromInstanceID(joystick_index));
|
||||
m_controllers.erase(it);
|
||||
|
||||
if (notify)
|
||||
|
@ -241,18 +252,264 @@ bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify
|
|||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::OpenJoystick(int index)
|
||||
{
|
||||
SDL_Joystick* joystick = SDL_JoystickOpen(index);
|
||||
if (!joystick)
|
||||
{
|
||||
Log_WarningPrintf("Failed to open joystick %d", index);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int joystick_id = SDL_JoystickInstanceID(joystick);
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
int player_id = SDL_JoystickGetDevicePlayerIndex(index);
|
||||
#else
|
||||
int player_id = -1;
|
||||
#endif
|
||||
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
|
||||
{
|
||||
const int free_player_id = GetFreePlayerId();
|
||||
Log_WarningPrintf(
|
||||
"Controller %d (joystick %d) returned player ID %d, which is invalid or in use. Using ID %d instead.", index,
|
||||
joystick_id, player_id, free_player_id);
|
||||
player_id = free_player_id;
|
||||
}
|
||||
|
||||
const char* name = SDL_JoystickName(joystick);
|
||||
|
||||
Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", index, joystick_id, player_id, name);
|
||||
|
||||
ControllerData cd = {};
|
||||
cd.player_id = player_id;
|
||||
cd.joystick_id = joystick_id;
|
||||
cd.haptic_left_right_effect = -1;
|
||||
cd.is_game_controller = false;
|
||||
|
||||
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
|
||||
if (haptic)
|
||||
{
|
||||
SDL_HapticEffect ef = {};
|
||||
ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
|
||||
ef.leftright.length = 1000;
|
||||
|
||||
int ef_id = SDL_HapticNewEffect(haptic, &ef);
|
||||
if (ef_id >= 0)
|
||||
{
|
||||
cd.haptic = haptic;
|
||||
cd.haptic_left_right_effect = ef_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("Failed to create haptic left/right effect: %s", SDL_GetError());
|
||||
if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
|
||||
{
|
||||
cd.haptic = haptic;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
|
||||
SDL_HapticClose(haptic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cd.haptic)
|
||||
Log_InfoPrintf("Rumble is supported on '%s'", name);
|
||||
else
|
||||
Log_WarningPrintf("Rumble is not supported on '%s'", name);
|
||||
|
||||
m_controllers.push_back(std::move(cd));
|
||||
OnControllerConnected(player_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* event)
|
||||
{
|
||||
const float value = static_cast<float>(event->value) / (event->value < 0 ? 32768.0f : 32767.0f);
|
||||
Log_DebugPrintf("controller %d axis %d %d %f", event->which, event->axis, event->value, value);
|
||||
|
||||
auto it = GetControllerDataForJoystickId(event->which);
|
||||
if (it == m_controllers.end() || it->is_game_controller)
|
||||
return false;
|
||||
|
||||
if (DoEventHook(Hook::Type::Axis, it->player_id, event->axis, value, true))
|
||||
return true;
|
||||
|
||||
bool processed = false;
|
||||
|
||||
const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
cb(value);
|
||||
processed = true;
|
||||
}
|
||||
|
||||
if (value > 0.0f)
|
||||
{
|
||||
const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Positive];
|
||||
if (cb)
|
||||
{
|
||||
// Expand 0..1 - -1..1
|
||||
cb(value * 2.0f - 1.0f);
|
||||
processed = true;
|
||||
}
|
||||
}
|
||||
else if (value < 0.0f)
|
||||
{
|
||||
const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Negative];
|
||||
if (cb)
|
||||
{
|
||||
// Expand 0..-1 - -1..1
|
||||
cb(value * -2.0f - 1.0f);
|
||||
processed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (processed)
|
||||
return true;
|
||||
|
||||
// set the other direction to false so large movements don't leave the opposite on
|
||||
const bool outside_deadzone = (std::abs(value) >= it->deadzone);
|
||||
const bool positive = (value >= 0.0f);
|
||||
const ButtonCallback& other_button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(!positive)];
|
||||
const ButtonCallback& button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(positive)];
|
||||
if (button_cb)
|
||||
{
|
||||
button_cb(outside_deadzone);
|
||||
if (other_button_cb)
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else if (other_button_cb)
|
||||
{
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* event)
|
||||
{
|
||||
Log_DebugPrintf("controller %d button %d %s", event->which, event->button,
|
||||
event->state == SDL_PRESSED ? "pressed" : "released");
|
||||
|
||||
auto it = GetControllerDataForJoystickId(event->which);
|
||||
if (it == m_controllers.end() || it->is_game_controller)
|
||||
return false;
|
||||
|
||||
const bool pressed = (event->state == SDL_PRESSED);
|
||||
if (DoEventHook(Hook::Type::Button, it->player_id, event->button, pressed ? 1.0f : 0.0f))
|
||||
return true;
|
||||
|
||||
const ButtonCallback& cb = it->button_mapping[event->button];
|
||||
if (cb)
|
||||
{
|
||||
cb(pressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
const AxisCallback& axis_cb = it->button_axis_mapping[event->button];
|
||||
if (axis_cb)
|
||||
{
|
||||
axis_cb(pressed ? 1.0f : -1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::HandleJoystickHatEvent(const SDL_JoyHatEvent* event)
|
||||
{
|
||||
Log_DebugPrintf("controller %d hat %d %d", event->which, event->hat, event->value);
|
||||
|
||||
auto it = GetControllerDataForJoystickId(event->which);
|
||||
if (it == m_controllers.end() || it->is_game_controller)
|
||||
return false;
|
||||
|
||||
auto HatEventHook = [hat = event->hat, value = event->value, player_id = it->player_id, this](int hat_position) {
|
||||
if ((value & hat_position) == 0)
|
||||
return false;
|
||||
|
||||
std::string_view position_str;
|
||||
switch (value)
|
||||
{
|
||||
case SDL_HAT_UP:
|
||||
position_str = "Up";
|
||||
break;
|
||||
case SDL_HAT_RIGHT:
|
||||
position_str = "Right";
|
||||
break;
|
||||
case SDL_HAT_DOWN:
|
||||
position_str = "Down";
|
||||
break;
|
||||
case SDL_HAT_LEFT:
|
||||
position_str = "Left";
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return DoEventHook(Hook::Type::Hat, player_id, hat, position_str);
|
||||
};
|
||||
|
||||
if (event->value == SDL_HAT_CENTERED)
|
||||
{
|
||||
if (HatEventHook(SDL_HAT_CENTERED))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// event->value can be a bitmask of multiple direction, so probe them all
|
||||
if (HatEventHook(SDL_HAT_UP) || HatEventHook(SDL_HAT_RIGHT) || HatEventHook(SDL_HAT_DOWN) ||
|
||||
HatEventHook(SDL_HAT_LEFT))
|
||||
return true;
|
||||
}
|
||||
|
||||
bool processed = false;
|
||||
|
||||
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][0]; cb)
|
||||
{
|
||||
cb(event->value & SDL_HAT_UP);
|
||||
processed = true;
|
||||
}
|
||||
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][1]; cb)
|
||||
{
|
||||
cb(event->value & SDL_HAT_RIGHT);
|
||||
processed = true;
|
||||
}
|
||||
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][2]; cb)
|
||||
{
|
||||
cb(event->value & SDL_HAT_DOWN);
|
||||
processed = true;
|
||||
}
|
||||
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][3]; cb)
|
||||
{
|
||||
cb(event->value & SDL_HAT_LEFT);
|
||||
processed = true;
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
void SDLControllerInterface::ClearBindings()
|
||||
{
|
||||
for (auto& it : m_controllers)
|
||||
{
|
||||
for (AxisCallback& ac : it.axis_mapping)
|
||||
ac = {};
|
||||
for (ButtonCallback& bc : it.button_mapping)
|
||||
bc = {};
|
||||
it.axis_mapping.fill({});
|
||||
it.button_mapping.fill({});
|
||||
it.axis_button_mapping.fill({});
|
||||
it.button_axis_mapping.fill({});
|
||||
it.hat_button_mapping.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback)
|
||||
bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
|
@ -261,7 +518,7 @@ bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_n
|
|||
if (axis_number < 0 || axis_number >= MAX_NUM_AXISES)
|
||||
return false;
|
||||
|
||||
it->axis_mapping[axis_number] = std::move(callback);
|
||||
it->axis_mapping[axis_number][axis_side] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -292,6 +549,33 @@ bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, in
|
|||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
||||
std::string_view hat_position, ButtonCallback callback)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
size_t index;
|
||||
if (hat_position == "Up")
|
||||
index = 0;
|
||||
else if (hat_position == "Right")
|
||||
index = 1;
|
||||
else if (hat_position == "Down")
|
||||
index = 2;
|
||||
else if (hat_position == "Left")
|
||||
index = 3;
|
||||
else
|
||||
return false;
|
||||
|
||||
// We need 4 entries per hat_number
|
||||
if (it->hat_button_mapping.size() < hat_number + 1)
|
||||
it->hat_button_mapping.resize(hat_number + 1);
|
||||
|
||||
it->hat_button_mapping[hat_number][index] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
|
@ -317,10 +601,18 @@ bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev)
|
|||
if (DoEventHook(Hook::Type::Axis, it->player_id, ev->caxis.axis, value))
|
||||
return true;
|
||||
|
||||
const AxisCallback& cb = it->axis_mapping[ev->caxis.axis];
|
||||
const AxisCallback& cb = it->axis_mapping[ev->caxis.axis][AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
// Extend triggers from a 0 - 1 range to a -1 - 1 range for consistency with other inputs
|
||||
if (ev->caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT || ev->caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
|
||||
{
|
||||
cb((value * 2.0f) - 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
cb(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -367,11 +659,11 @@ bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_Event* ev)
|
|||
return true;
|
||||
}
|
||||
|
||||
// Assume a half-axis, i.e. in 0..1 range
|
||||
const AxisCallback& axis_cb = it->button_axis_mapping[ev->cbutton.button];
|
||||
if (axis_cb)
|
||||
{
|
||||
axis_cb(pressed ? 1.0f : 0.0f);
|
||||
axis_cb(pressed ? 1.0f : -1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -25,10 +25,12 @@ public:
|
|||
void ClearBindings() override;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override;
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
||||
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
||||
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
|
||||
|
||||
// Changing rumble strength.
|
||||
|
@ -45,23 +47,25 @@ public:
|
|||
private:
|
||||
struct ControllerData
|
||||
{
|
||||
void* controller;
|
||||
void* haptic;
|
||||
int haptic_left_right_effect;
|
||||
int joystick_id;
|
||||
int player_id;
|
||||
bool is_game_controller;
|
||||
|
||||
float deadzone = 0.25f;
|
||||
|
||||
std::array<AxisCallback, MAX_NUM_AXISES> axis_mapping;
|
||||
// TODO: Turn to vectors to support arbitrary amounts of buttons and axes (for Joysticks)
|
||||
// Preferably implement a simple "flat map", an ordered view over a vector
|
||||
std::array<std::array<AxisCallback, 3>, MAX_NUM_AXISES> axis_mapping;
|
||||
std::array<ButtonCallback, MAX_NUM_BUTTONS> button_mapping;
|
||||
std::array<std::array<ButtonCallback, 2>, MAX_NUM_AXISES> axis_button_mapping;
|
||||
std::array<AxisCallback, MAX_NUM_BUTTONS> button_axis_mapping;
|
||||
std::vector<std::array<ButtonCallback, 4>> hat_button_mapping;
|
||||
};
|
||||
|
||||
using ControllerDataVector = std::vector<ControllerData>;
|
||||
|
||||
ControllerDataVector::iterator GetControllerDataForController(void* controller);
|
||||
ControllerDataVector::iterator GetControllerDataForJoystickId(int id);
|
||||
ControllerDataVector::iterator GetControllerDataForPlayerId(int id);
|
||||
int GetFreePlayerId() const;
|
||||
|
@ -71,6 +75,11 @@ private:
|
|||
bool HandleControllerAxisEvent(const SDL_Event* event);
|
||||
bool HandleControllerButtonEvent(const SDL_Event* event);
|
||||
|
||||
bool OpenJoystick(int index);
|
||||
bool HandleJoystickAxisEvent(const struct SDL_JoyAxisEvent* event);
|
||||
bool HandleJoystickButtonEvent(const struct SDL_JoyButtonEvent* event);
|
||||
bool HandleJoystickHatEvent(const struct SDL_JoyHatEvent* event);
|
||||
|
||||
ControllerDataVector m_controllers;
|
||||
|
||||
std::mutex m_event_intercept_mutex;
|
||||
|
|
|
@ -158,16 +158,11 @@ void XInputControllerInterface::CheckForStateChanges(u32 index, const XINPUT_STA
|
|||
|
||||
void XInputControllerInterface::ClearBindings()
|
||||
{
|
||||
for (auto& it : m_controllers)
|
||||
{
|
||||
for (AxisCallback& ac : it.axis_mapping)
|
||||
ac = {};
|
||||
for (ButtonCallback& bc : it.button_mapping)
|
||||
bc = {};
|
||||
}
|
||||
m_controllers.fill({});
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback)
|
||||
bool XInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected)
|
||||
return false;
|
||||
|
@ -175,7 +170,7 @@ bool XInputControllerInterface::BindControllerAxis(int controller_index, int axi
|
|||
if (axis_number < 0 || axis_number >= NUM_AXISES)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_mapping[axis_number] = std::move(callback);
|
||||
m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -204,6 +199,13 @@ bool XInputControllerInterface::BindControllerAxisToButton(int controller_index,
|
|||
return true;
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
||||
std::string_view hat_position, ButtonCallback callback)
|
||||
{
|
||||
// Hats don't exist in XInput
|
||||
return false;
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
|
||||
AxisCallback callback)
|
||||
{
|
||||
|
@ -226,7 +228,7 @@ bool XInputControllerInterface::HandleAxisEvent(u32 index, Axis axis, s32 value)
|
|||
if (DoEventHook(Hook::Type::Axis, index, static_cast<u32>(axis), f_value))
|
||||
return true;
|
||||
|
||||
const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)];
|
||||
const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
cb(f_value);
|
||||
|
|
|
@ -22,10 +22,12 @@ public:
|
|||
void ClearBindings() override;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override;
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
||||
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
||||
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
|
||||
|
||||
// Changing rumble strength.
|
||||
|
@ -61,7 +63,7 @@ private:
|
|||
|
||||
float deadzone = 0.25f;
|
||||
|
||||
std::array<AxisCallback, MAX_NUM_AXISES> axis_mapping;
|
||||
std::array<std::array<AxisCallback, 3>, MAX_NUM_AXISES> axis_mapping;
|
||||
std::array<ButtonCallback, MAX_NUM_BUTTONS> button_mapping;
|
||||
std::array<std::array<ButtonCallback, 2>, MAX_NUM_AXISES> axis_button_mapping;
|
||||
std::array<AxisCallback, MAX_NUM_BUTTONS> button_axis_mapping;
|
||||
|
|
Loading…
Reference in a new issue