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
+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"});