ddr: add support for analog axis-only pads and handle left+right / up+down input (#659)

## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Some crappy DDR soft mats (either USB-based or console pads with crappy
adapters that lack a dedicated "DDR pad mode") use X and Y analog axis
for arrows, which means they cannot cleanly report Up+Down or Left+Right
input.

According to some ancient Stepmania code, a lot of these report a middle
value (value between center and right, for example, for left+right).
Borrow this hack from Stepmania and implement it in spice as analog
input for DDR.

Also, for rawinput/xinput automatic bind scenario, update the logic so
that face buttons are always detected before analog axis / dpads for
consistency. This would help with binding any DDR pads that
simultaneously input face buttons / dpad / analog axis, so that we can
prefer face buttons when detected.

## Testing
I don't have any soft mats that have this, but it was tested with an
xbox controller.
This commit is contained in:
bicarus
2026-04-27 20:17:54 -07:00
committed by GitHub
parent 37218e7fe0
commit 6bc1357d6a
10 changed files with 351 additions and 128 deletions
+17 -5
View File
@@ -2,6 +2,7 @@
#include "mdxf_poll.h"
#include "avs/game.h"
#include "games/ddr/ddr.h"
#include "games/ddr/io.h"
#include "launcher/launcher.h"
#include "rawinput/rawinput.h"
@@ -357,14 +358,17 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
// decide on button map
const size_t *button_map = nullptr;
int player = 0;
switch (node) {
case 17:
case 25:
button_map = &buttons_p1[0];
player = 1;
break;
case 18:
case 26:
button_map = &buttons_p2[0];
player = 2;
break;
}
@@ -374,19 +378,27 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
if (source == EXTERNAL_POLL) {
// get buttons
auto &buttons = games::ddr::get_buttons();
// get analogs
bool analog_left = false;
bool analog_right = false;
games::ddr::get_analog_x_axis(player, analog_left, analog_right);
bool analog_up = false;
bool analog_down = false;
games::ddr::get_analog_y_axis(player, analog_up, analog_down);
uint8_t up_down = 0;
uint8_t left_right = 0;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) {
up_down |= 0xF0;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1])) || analog_down) {
up_down |= 0x0F;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2])) || analog_left) {
left_right |= 0xF0;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3])) || analog_right) {
left_right |= 0x0F;
}
current_state = (uint16_t(up_down) << 8) | left_right;
+46
View File
@@ -291,4 +291,50 @@ namespace games::ddr {
// dispose device hook
devicehook_dispose();
}
static void get_analog_xy_axis(Analog &analog, bool &less, bool &more) {
if (!analog.isSet()) {
return;
}
const auto value = GameAPI::Analogs::getState(RI_MGR, analog);
// stepmania linux source:
// https://github.com/stepmania/stepmania/blob/d55acb1ba26f1c5b5e3048d6d6c0bd116625216f/src/arch/InputHandler/InputHandler_Linux_Event.cpp#L410
// for example, value cap with range [0, 255]:
// 127 = 0.498, neutral
// 128 = 0.502, both
// apparently some adapters report values like above; this obviously makes a lot of
// assumptions about bit width of the HID value cap and how the adapter reports
// neutral/both values, but this is good enough for parity with stepmania and its many forks
if (0.5001f < value && value < 0.75f) {
less |= true;
more |= true;
} else if (value <= 0.25f) {
less |= true;
} else if (value >= 0.75f) {
more |= true;
}
}
void get_analog_x_axis(int player, bool &left, bool &right) {
if (player != 1 && player != 2) {
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
return;
}
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_LEFT_RIGHT : Analogs::P2_LEFT_RIGHT);
get_analog_xy_axis(analog, left, right);
}
void get_analog_y_axis(int player, bool &up, bool &down) {
if (player != 1 && player != 2) {
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
return;
}
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_UP_DOWN : Analogs::P2_UP_DOWN);
get_analog_xy_axis(analog, up, down);
}
}
+3
View File
@@ -23,4 +23,7 @@ namespace games::ddr {
private:
void register_codecs();
};
void get_analog_x_axis(int player, bool &left, bool &right);
void get_analog_y_axis(int player, bool &up, bool &down);
}
+43
View File
@@ -35,6 +35,49 @@ std::vector<Button> &games::ddr::get_buttons() {
return analogs;
}
std::string games::ddr::get_buttons_help() {
// keep to max 100 characters wide
return
"For DDR pad arrows, double check that simultaneous Left+Right and Up+Down can be detected.\n"
"You should boot the game, enter test menu, and use Foot Panel Check.\n\n"
"When mapping arrows, try the following in order:\n\n"
" 1. If your controller uses face buttons (A/B/X/Y), bind them here.\n"
" 2. If your controller supports XInput, use that.\n"
" 3. If your controller uses analog axis for arrows, try Analogs tab.\n"
" 4. Otherwise, you will need to use remapping software or get a better adapter."
;
}
std::vector<Analog> &games::ddr::get_analogs() {
static std::vector<Analog> analogs;
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Dance Dance Revolution");
GameAPI::Analogs::sortAnalogs(
&analogs,
"P1 Left-Right (Axis Fix)",
"P1 Up-Down (Axis Fix)",
"P2 Left-Right (Axis Fix)",
"P2 Up-Down (Axis Fix)"
);
}
return analogs;
}
std::string games::ddr::get_analogs_help() {
// keep to max 100 characters wide
return
"Only use this if your DDR pad outputs analog axis for arrows.\n\n"
"If the pad uses face buttons (A/B/X/Y), use Buttons tab instead.\n\n"
"Spice will treat values <=25% as Left, >=75% as Right, ~=50% as neutral,\n"
"and value between 50% and 75% as both arrows.\n\n"
"This is the classic Stepmania \"Axis Fix\" which may or may not work with\n"
"your dance pad or your adapter."
;
}
std::vector<Light> &games::ddr::get_lights() {
static std::vector<Light> lights;
+13
View File
@@ -33,6 +33,16 @@ namespace games::ddr {
};
}
// all analogs in correct order
namespace Analogs {
enum {
P1_LEFT_RIGHT,
P1_UP_DOWN,
P2_LEFT_RIGHT,
P2_UP_DOWN
};
}
// all lights in correct order
namespace Lights {
enum {
@@ -177,5 +187,8 @@ namespace games::ddr {
// getters
std::vector<Button> &get_buttons();
std::string get_buttons_help();
std::string get_analogs_help();
std::vector<Analog> &get_analogs();
std::vector<Light> &get_lights();
}
+49 -1
View File
@@ -498,32 +498,80 @@ int games::ddr::DDRP3IOHandle::device_io(
// shift table
static size_t shift_table[] = {
30, 28, 29, 8, 9, 10, 11, 12, 24, 25, 14, 15, 16, 17, 18, 19, 20, 26, 27, 22, 23
30, 28, 29, // service, test, coin
8, // p1 start
9, 10, 11, 12, // p1 panel
24, 25, 14, 15, // p1 menu
16, // p2 start
17, 18, 19, 20, // p2 panel
26, 27, 22, 23 // p2 menu
};
static size_t button_table[] = {
Buttons::SERVICE,
Buttons::TEST,
Buttons::COIN_MECH,
Buttons::P1_START,
Buttons::P1_PANEL_UP,
Buttons::P1_PANEL_DOWN,
Buttons::P1_PANEL_LEFT,
Buttons::P1_PANEL_RIGHT,
Buttons::P1_MENU_UP,
Buttons::P1_MENU_DOWN,
Buttons::P1_MENU_LEFT,
Buttons::P1_MENU_RIGHT,
Buttons::P2_START,
Buttons::P2_PANEL_UP,
Buttons::P2_PANEL_DOWN,
Buttons::P2_PANEL_LEFT,
Buttons::P2_PANEL_RIGHT,
Buttons::P2_MENU_UP,
Buttons::P2_MENU_DOWN,
Buttons::P2_MENU_LEFT,
Buttons::P2_MENU_RIGHT,
};
// get analogs
struct {
bool up;
bool down;
bool left;
bool right;
} analog_values[2] = {};
games::ddr::get_analog_x_axis(1, analog_values[0].left, analog_values[0].right);
games::ddr::get_analog_y_axis(1, analog_values[0].up, analog_values[0].down);
games::ddr::get_analog_x_axis(2, analog_values[1].left, analog_values[1].right);
games::ddr::get_analog_y_axis(2, analog_values[1].up, analog_values[1].down);
if (analog_values[0].up) {
controls |= 1 << 9;
}
if (analog_values[0].down) {
controls |= 1 << 10;
}
if (analog_values[0].left) {
controls |= 1 << 11;
}
if (analog_values[0].right) {
controls |= 1 << 12;
}
if (analog_values[1].up) {
controls |= 1 << 17;
}
if (analog_values[1].down) {
controls |= 1 << 18;
}
if (analog_values[1].left) {
controls |= 1 << 19;
}
if (analog_values[1].right) {
controls |= 1 << 20;
}
// update states
auto &buttons = get_buttons();
size_t count = 0;
+3
View File
@@ -92,6 +92,9 @@ namespace games {
games.push_back(ddr);
buttons.insert({ ddr, ddr::get_buttons() });
lights.insert({ ddr, ddr::get_lights() });
buttons_help.insert({ ddr, ddr::get_buttons_help() });
analogs_help.insert({ ddr, ddr::get_analogs_help() });
analogs.insert({ ddr, ddr::get_analogs() });
file_hints[ddr].push_back({"ddr.dll"});
file_hints[ddr].push_back({"mdxja_945.dll"});
file_hints[ddr].push_back({"arkmdxp3.dll"});
+99 -91
View File
@@ -1206,6 +1206,16 @@ namespace overlay::windows {
case rawinput::HID: {
auto hid = device->hidInfo;
struct temp_button {
std::string device_name;
unsigned short vkey;
ButtonAnalogType analog_type;
};
// use this to prioritize button binding over value types
// (for controllers that output both)
std::optional<temp_button> button_to_bind;
// ignore touchscreen and digitizer button inputs
// digitizer has funky stuff like "Touch Valid" "Data Valid" always held high
if (!rawinput::touch::is_touchscreen(device) &&
@@ -1219,18 +1229,11 @@ namespace overlay::windows {
// check if button is down
if (button_states[i]) {
// bind key
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(button_index + i));
button->setAnalogType(BAT_NONE);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
button_to_bind = {
device->name,
static_cast<unsigned short>(button_index + i),
BAT_NONE
};
break;
}
}
@@ -1239,95 +1242,100 @@ namespace overlay::windows {
}
// value caps
auto value_states = &hid->value_states;
auto bind_value_states = &hid->bind_value_states;
auto value_names = &hid->value_caps_names;
for (size_t i = 0; i < value_states->size(); i++) {
auto &state = value_states->at(i);
auto &bind_state = bind_value_states->at(i);
auto &value_name = value_names->at(i);
if (!button_to_bind.has_value()) {
auto value_states = &hid->value_states;
auto bind_value_states = &hid->bind_value_states;
auto value_names = &hid->value_caps_names;
for (size_t i = 0; i < value_states->size(); i++) {
auto &state = value_states->at(i);
auto &bind_state = bind_value_states->at(i);
auto &value_name = value_names->at(i);
// check for valid axis names
if (value_name == "X" ||
value_name == "Y" ||
value_name == "Rx" ||
value_name == "Ry" ||
value_name == "Z")
{
// check if axis is in activation area
float normalized = (state - 0.5f) * 2.f;
float diff = std::fabs(state - bind_state);
if (std::fabs(normalized) > 0.9f && diff > 0.1f) {
auto bat = normalized > 0 ? BAT_POSITIVE : BAT_NEGATIVE;
// check for valid axis names
if (value_name == "X" ||
value_name == "Y" ||
value_name == "Rx" ||
value_name == "Ry" ||
value_name == "Z")
{
// check if axis is in activation area
float normalized = (state - 0.5f) * 2.f;
float diff = std::fabs(state - bind_state);
if (std::fabs(normalized) > 0.9f && diff > 0.1f) {
auto bat = normalized > 0 ? BAT_POSITIVE : BAT_NEGATIVE;
// bind value
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(bat);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
// bind value
button_to_bind = {
device->name,
static_cast<unsigned short>(i),
bat
};
// usually, turntables are X, knobs are X and Y
// gamepad triggers are Z, and Rx/Ry are right thumb stick
// one day we will label all I/O modules and flag which one are button-as-analog
// so that we don't have to do string comparions like below
if ((value_name == "X" || value_name == "Y") &&
(button->getName().find("Press") == std::string::npos) && // museca
(button->getName().find("Slowdown") == std::string::npos) && // bishibashi
(button->getName().find("TT+") != std::string::npos ||
button->getName().find("TT-") != std::string::npos || // iidx
button->getName().find("Knob") != std::string::npos || // gitadora guitar
button->getName().find("Disk") != std::string::npos || // museca, bishibashi
button->getName().find("VOL-") != std::string::npos)) { // sdvx
this->analog_as_button_warning_show_next_frame =
std::make_pair(button->getName(), alt_index);
// usually, turntables are X, knobs are X and Y
// gamepad triggers are Z, and Rx/Ry are right thumb stick
// one day we will label all I/O modules and flag which one are button-as-analog
// so that we don't have to do string comparions like below
if ((value_name == "X" || value_name == "Y") &&
(button->getName().find("Press") == std::string::npos) && // museca
(button->getName().find("Slowdown") == std::string::npos) && // bishibashi
(button->getName().find("TT+") != std::string::npos ||
button->getName().find("TT-") != std::string::npos || // iidx
button->getName().find("Knob") != std::string::npos || // gitadora guitar
button->getName().find("Disk") != std::string::npos || // museca, bishibashi
button->getName().find("P1 Panel") != std::string::npos || // ddr
button->getName().find("P2 Panel") != std::string::npos || // ddr
button->getName().find("VOL-") != std::string::npos)) { // sdvx
this->analog_as_button_warning_show_next_frame =
std::make_pair(button->getName(), alt_index);
}
break;
} else if (diff > 0.3f) {
bind_state = state;
}
break;
} else if (diff > 0.3f) {
bind_state = state;
}
}
// hat switch
if (value_name == "Hat switch") {
// hat switch
if (value_name == "Hat switch") {
// get hat switch values
ButtonAnalogType buffer[3], buffer_bind[3];
Button::getHatSwitchValues(state, buffer);
Button::getHatSwitchValues(bind_state, buffer_bind);
// get hat switch values
ButtonAnalogType buffer[3], buffer_bind[3];
Button::getHatSwitchValues(state, buffer);
Button::getHatSwitchValues(bind_state, buffer_bind);
// check the first entry only
if (buffer[0] != BAT_NONE && buffer[0] != buffer_bind[0]) {
// check the first entry only
if (buffer[0] != BAT_NONE && buffer[0] != buffer_bind[0]) {
// bind value
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(buffer[0]);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
// bind value
button_to_bind = {
device->name,
static_cast<unsigned short>(i),
buffer[0]
};
break;
}
}
}
}
if (button_to_bind.has_value()) {
const auto &b = button_to_bind.value();
button->setDeviceIdentifier(b.device_name);
button->setVKey(b.vkey);
button->setAnalogType(b.analog_type);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setBatThreshold(0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
alt_index - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
}
break;
}
case rawinput::MIDI: {
@@ -2184,9 +2192,9 @@ namespace overlay::windows {
ImGui::Separator();
if (ImGui::BeginTable("AnalogsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(220));
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(240));
ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(100));
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(80));
// check if empty
if (!analogs || analogs->empty()) {
+75 -30
View File
@@ -153,7 +153,7 @@ XInputSetState(
float XInputManager::get_analog_state(uint8_t player, XInputAnalogEnum analog) {
return 0.5f;
}
bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button) {
bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button, XINPUT_GAMEPAD_STATE_NORMALIZED *state_in) {
return false;
}
bool XInputManager::get_any_button_pressed(XINPUT_NEW_BUTTON &button) {
@@ -289,59 +289,66 @@ XInputSetState(
}
}
bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button) {
XINPUT_GAMEPAD_STATE_NORMALIZED state;
bool XInputManager::is_button_pressed(uint8_t player, XInputButtonEnum button, XINPUT_GAMEPAD_STATE_NORMALIZED *state_in) {
XINPUT_GAMEPAD_STATE_NORMALIZED state_on_stack;
XINPUT_GAMEPAD_STATE_NORMALIZED *state;
get_state(player, state);
if (state_in) {
state = state_in;
} else {
state = &state_on_stack;
get_state(player, *state);
}
switch (button) {
case XInputButtonEnum::DPAD_UP:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_UP) != 0;
return (state->wButtons & XINPUT_GAMEPAD_DPAD_UP) != 0;
case XInputButtonEnum::DPAD_DOWN:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) != 0;
return (state->wButtons & XINPUT_GAMEPAD_DPAD_DOWN) != 0;
case XInputButtonEnum::DPAD_LEFT:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_LEFT) != 0;
return (state->wButtons & XINPUT_GAMEPAD_DPAD_LEFT) != 0;
case XInputButtonEnum::DPAD_RIGHT:
return (state.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) != 0;
return (state->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) != 0;
case XInputButtonEnum::START:
return (state.wButtons & XINPUT_GAMEPAD_START) != 0;
return (state->wButtons & XINPUT_GAMEPAD_START) != 0;
case XInputButtonEnum::BACK:
return (state.wButtons & XINPUT_GAMEPAD_BACK) != 0;
return (state->wButtons & XINPUT_GAMEPAD_BACK) != 0;
case XInputButtonEnum::LEFT_STICK:
return (state.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) != 0;
return (state->wButtons & XINPUT_GAMEPAD_LEFT_THUMB) != 0;
case XInputButtonEnum::RIGHT_STICK:
return (state.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) != 0;
return (state->wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) != 0;
case XInputButtonEnum::LEFT_SHOULDER:
return (state.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) != 0;
return (state->wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) != 0;
case XInputButtonEnum::RIGHT_SHOULDER:
return (state.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0;
return (state->wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0;
case XInputButtonEnum::BUTTON_A:
return (state.wButtons & XINPUT_GAMEPAD_A) != 0;
return (state->wButtons & XINPUT_GAMEPAD_A) != 0;
case XInputButtonEnum::BUTTON_B:
return (state.wButtons & XINPUT_GAMEPAD_B) != 0;
return (state->wButtons & XINPUT_GAMEPAD_B) != 0;
case XInputButtonEnum::BUTTON_X:
return (state.wButtons & XINPUT_GAMEPAD_X) != 0;
return (state->wButtons & XINPUT_GAMEPAD_X) != 0;
case XInputButtonEnum::BUTTON_Y:
return (state.wButtons & XINPUT_GAMEPAD_Y) != 0;
return (state->wButtons & XINPUT_GAMEPAD_Y) != 0;
case XInputButtonEnum::LEFT_TRIGGER:
return state.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
return state->bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
case XInputButtonEnum::RIGHT_TRIGGER:
return state.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
return state->bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
case XInputButtonEnum::LEFT_STICK_UP:
return state.sThumbLY > 0.6f;
return state->sThumbLY > 0.6f;
case XInputButtonEnum::LEFT_STICK_DOWN:
return state.sThumbLY < 0.4f;
return state->sThumbLY < 0.4f;
case XInputButtonEnum::LEFT_STICK_LEFT:
return state.sThumbLX < 0.4f;
return state->sThumbLX < 0.4f;
case XInputButtonEnum::LEFT_STICK_RIGHT:
return state.sThumbLX > 0.6f;
return state->sThumbLX > 0.6f;
case XInputButtonEnum::RIGHT_STICK_UP:
return state.sThumbRY > 0.6f;
return state->sThumbRY > 0.6f;
case XInputButtonEnum::RIGHT_STICK_DOWN:
return state.sThumbRY < 0.4f;
return state->sThumbRY < 0.4f;
case XInputButtonEnum::RIGHT_STICK_LEFT:
return state.sThumbRX < 0.4f;
return state->sThumbRX < 0.4f;
case XInputButtonEnum::RIGHT_STICK_RIGHT:
return state.sThumbRX > 0.6f;
return state->sThumbRX > 0.6f;
default:
break;
}
@@ -374,11 +381,49 @@ XInputSetState(
}
bool XInputManager::get_any_button_pressed(XINPUT_NEW_BUTTON &button) {
constexpr std::array<XInputButtonEnum, static_cast<size_t>(XInputButtonEnum::COUNT)> button_priority = {
// the ordering here is important; we want to check buttons first, then dpad, then analog
// this is to help with cases like DDR pads that output multiple at the same time for arrows
// actual buttons
XInputButtonEnum::BUTTON_A,
XInputButtonEnum::BUTTON_B,
XInputButtonEnum::BUTTON_X,
XInputButtonEnum::BUTTON_Y,
XInputButtonEnum::START,
XInputButtonEnum::BACK,
XInputButtonEnum::LEFT_STICK,
XInputButtonEnum::RIGHT_STICK,
XInputButtonEnum::LEFT_SHOULDER,
XInputButtonEnum::RIGHT_SHOULDER,
// dpad
XInputButtonEnum::DPAD_UP,
XInputButtonEnum::DPAD_DOWN,
XInputButtonEnum::DPAD_LEFT,
XInputButtonEnum::DPAD_RIGHT,
// analog values that can be used as buttons
XInputButtonEnum::LEFT_TRIGGER,
XInputButtonEnum::RIGHT_TRIGGER,
XInputButtonEnum::LEFT_STICK_UP,
XInputButtonEnum::LEFT_STICK_DOWN,
XInputButtonEnum::LEFT_STICK_LEFT,
XInputButtonEnum::LEFT_STICK_RIGHT,
XInputButtonEnum::RIGHT_STICK_UP,
XInputButtonEnum::RIGHT_STICK_DOWN,
XInputButtonEnum::RIGHT_STICK_LEFT,
XInputButtonEnum::RIGHT_STICK_RIGHT,
};
for (uint8_t player = 0; player < XUSER_MAX_COUNT; player++) {
XINPUT_GAMEPAD_STATE_NORMALIZED state;
get_state(player, state);
for (uint16_t b = 0; b < static_cast<uint16_t>(XInputButtonEnum::COUNT); b++) {
if (is_button_pressed(player, static_cast<XInputButtonEnum>(b))) {
if (is_button_pressed(player, button_priority[b], &state)) {
button.player = player;
button.button = static_cast<XInputButtonEnum>(b);
button.button = button_priority[b];
return true;
}
}
+3 -1
View File
@@ -31,6 +31,8 @@ namespace xinput {
float sThumbRY;
};
// the order of this enum is used for vkey mapping (saved in config file)
// therefore it can never be changed
enum class XInputButtonEnum : uint16_t {
// actual buttons
DPAD_UP,
@@ -102,7 +104,7 @@ namespace xinput {
~XInputManager();
void stop();
std::vector<uint8_t> get_available_players();
bool is_button_pressed(uint8_t player, XInputButtonEnum button);
bool is_button_pressed(uint8_t player, XInputButtonEnum button, XINPUT_GAMEPAD_STATE_NORMALIZED *state_in=nullptr);
float get_analog_state(uint8_t player, XInputAnalogEnum analog);
bool get_any_button_pressed(XINPUT_NEW_BUTTON &button);
void set_output_state(uint8_t player, XInputOutputEnum output, float value);