Flight sticks, HOTAS and pedals, with a setup wizard
Ported from BT411, which needed the same thing for its glass cockpit. PadRIO reads XInput, which covers Xbox-class pads and nothing else. A flight stick, a HOTAS throttle, a twist grip, rudder pedals or a wheel arrive through DirectInput instead, and until now the game could not see any of them - the only generic-joystick path left was the 1995 single- device DIJoystick behind L4CONTROLS=DIJOYSTICK, which is untouched here. L4JOY is the reader: up to four devices as normalized state blocks, hot- plug re-enumeration on the same ~3 s cadence PadRIO uses to look for a pad, and a device lost mid-race zeroed rather than left holding whatever was pressed when it went. XInput-class devices are excluded by VID/PID against the RawInput paths carrying the "IG_" marker - without that an Xbox pad arrives through both APIs and every button counts twice. bindings.txt gains four rows in the grammar it already had, using its own vocabulary (deadzone/rate) rather than BT411's: joydev <slot> [product-name substring] joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>] joybutton <n> button <addr> [toggle] joyhat <n> <up|down|left|right> button <addr> Slots resolve to a live device every poll, by name substring or ordinal, so unplugging and replugging does not rewrite anyone's file. Two things the pod's shape forced that BT411 solved differently: Pedals - a signed composite axis that decomposes into the pod's two pedals, positive right and negative left. The pod has a pedal each side; a twist grip or rudder bar is one signed control, and pressing one or the other but never both is exactly what it wants to say. It is a channel name like any other, so a pad stick can drive the turn too. A joyaxis on Throttle with no rate is a real lever and OWNS the channel - full travel maps onto the 0..1 the pod runs on, instead of nudging the accumulator that a spring-centred pad stick has to use. RP412JOYCONFIG=1 (joyconfig.bat) runs the capture wizard before the console screen: it asks the player to move each control, and derives the sign convention from the DIRECTION of the move. That is the point of it - a stick that reads positive pushed right and one that reads negative are equally common, and no amount of documentation gets a player to work out which they own. It writes only its own section, between marker lines, so hand-edited keyboard and pad rows survive re-running it. The wizard also prints every axis at rest before it starts. A driver that refuses the +-32767 range we ask for reports its own, and an axis then sits hard over instead of near zero; seeing "X +1.00" on an untouched stick is the difference between a five-minute fix and a bug report that says it configured itself. Each capture reports the move it saw for the same reason. Verified on the Logitech Extreme 3D on this machine. Enumeration finds it and excludes the Xbox pad, which still arrives separately through XInput. Every row shape parses - 7 axes, 2 buttons, 4 hat directions - and three deliberately malformed rows (a bad axis name, button 99, a "sideways" hat) are each rejected by line number rather than silently dropped. The wizard lists the device with its axes at rest reading X +0.00 Y -0.01 RZ -0.04 SL0 +1.00, waits on the first prompt without self-triggering, and with a hand on the stick captures X to steering, Y to pitch, RZ to the pedals and SL0 to the throttle, inverting the ones that read backwards. Running the captures through to a written file needs a hand on the stick, so that part is the machine's to confirm, not this build's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+197
-26
@@ -2,6 +2,7 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4padbindings.h"
|
||||
#include "l4joy.h" // joyButtonCount / joyHatCount, the parse limits
|
||||
|
||||
#include <XInput.h>
|
||||
#include <stdio.h>
|
||||
@@ -79,6 +80,20 @@ namespace
|
||||
{ "Throttle", BindAxisThrottle },
|
||||
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
|
||||
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
|
||||
{ "Pedals", BindAxisPedals },
|
||||
};
|
||||
|
||||
// DirectInput's axis order, which is what the joy* rows name
|
||||
const NameValue kJoyAxisNames[] =
|
||||
{
|
||||
{ "X", BindJoyAxisX }, { "Y", BindJoyAxisY }, { "Z", BindJoyAxisZ },
|
||||
{ "RX", BindJoyAxisRX }, { "RY", BindJoyAxisRY }, { "RZ", BindJoyAxisRZ },
|
||||
{ "SL0", BindJoyAxisSL0 }, { "SL1", BindJoyAxisSL1 },
|
||||
};
|
||||
|
||||
const NameValue kJoyHatNames[] =
|
||||
{
|
||||
{ "up", 0 }, { "right", 1 }, { "down", 2 }, { "left", 3 },
|
||||
};
|
||||
|
||||
Logical NameEquals(const char *a, const char *b)
|
||||
@@ -185,10 +200,83 @@ namespace
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// One line of the profile grammar
|
||||
// Shared tail of the two axis-source rows: [invert] [deadzone <d>]
|
||||
// [rate <n>], in any order.
|
||||
//---------------------------------------------------------------
|
||||
Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile)
|
||||
Logical ParseAxisOptions(
|
||||
char *tokens[], int token_count, int first,
|
||||
Logical *invert, Scalar *deadzone, Scalar *rate)
|
||||
{
|
||||
for (int i = first; i < token_count; ++i)
|
||||
{
|
||||
if (NameEquals(tokens[i], "invert"))
|
||||
{
|
||||
*invert = True;
|
||||
}
|
||||
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
|
||||
{
|
||||
if (!ParseNumber(tokens[++i], deadzone))
|
||||
{
|
||||
return False;
|
||||
}
|
||||
}
|
||||
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
|
||||
{
|
||||
if (!ParseNumber(tokens[++i], rate))
|
||||
{
|
||||
return False;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return False;
|
||||
}
|
||||
}
|
||||
return True;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// One line of the profile grammar. joy_slot carries the joydev
|
||||
// state forward from line to line - joy rows attach to the slot
|
||||
// most recently declared.
|
||||
//---------------------------------------------------------------
|
||||
Logical ParseLine(
|
||||
char *tokens[], int token_count, PadBindingProfile *profile,
|
||||
int *joy_slot)
|
||||
{
|
||||
//
|
||||
// joydev is the one row that can be two tokens long ("joydev 1"),
|
||||
// so it is answered before the four-token floor below.
|
||||
//
|
||||
if (NameEquals(tokens[0], "joydev") && token_count >= 2)
|
||||
{
|
||||
int slot = -1;
|
||||
if (sscanf(tokens[1], "%d", &slot) != 1 ||
|
||||
slot < 0 || slot >= BindJoyDeviceSlots)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
*joy_slot = slot;
|
||||
//
|
||||
// The rest of the line is a product-name substring, rejoined
|
||||
// with single spaces ("Saitek Pro Flight" is four tokens).
|
||||
//
|
||||
profile->joyDeviceMatch[slot][0] = '\0';
|
||||
for (int i = 2; i < token_count; ++i)
|
||||
{
|
||||
if (i > 2)
|
||||
{
|
||||
strncat(profile->joyDeviceMatch[slot], " ",
|
||||
sizeof(profile->joyDeviceMatch[slot]) -
|
||||
strlen(profile->joyDeviceMatch[slot]) - 1);
|
||||
}
|
||||
strncat(profile->joyDeviceMatch[slot], tokens[i],
|
||||
sizeof(profile->joyDeviceMatch[slot]) -
|
||||
strlen(profile->joyDeviceMatch[slot]) - 1);
|
||||
}
|
||||
return True;
|
||||
}
|
||||
|
||||
if (token_count < 4)
|
||||
{
|
||||
return False;
|
||||
@@ -271,31 +359,75 @@ namespace
|
||||
memset(binding, 0, sizeof(*binding));
|
||||
binding->source = source;
|
||||
binding->axis = axis;
|
||||
for (int i = 4; i < token_count; ++i)
|
||||
return ParseAxisOptions(tokens, token_count, 4,
|
||||
&binding->invert, &binding->deadzone, &binding->rate);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Generic joystick rows, all attaching to the current joydev slot
|
||||
//---------------------------------------------------------------
|
||||
if (NameEquals(tokens[0], "joyaxis") && NameEquals(tokens[2], "axis"))
|
||||
{
|
||||
int source = LookupTable(kJoyAxisNames,
|
||||
sizeof(kJoyAxisNames) / sizeof(kJoyAxisNames[0]), tokens[1]);
|
||||
int axis = LookupTable(kRioAxisNames,
|
||||
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
|
||||
if (source < 0 || axis < 0 ||
|
||||
profile->joyAxisCount >= PadBindingProfile::maxJoyAxes)
|
||||
{
|
||||
if (NameEquals(tokens[i], "invert"))
|
||||
{
|
||||
binding->invert = True;
|
||||
}
|
||||
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
|
||||
{
|
||||
if (!ParseNumber(tokens[++i], &binding->deadzone))
|
||||
{
|
||||
return False;
|
||||
}
|
||||
}
|
||||
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
|
||||
{
|
||||
if (!ParseNumber(tokens[++i], &binding->rate))
|
||||
{
|
||||
return False;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return False;
|
||||
}
|
||||
return False;
|
||||
}
|
||||
PadJoyAxisBinding *binding = &profile->joyAxes[profile->joyAxisCount++];
|
||||
memset(binding, 0, sizeof(*binding));
|
||||
binding->device = *joy_slot;
|
||||
binding->source = source;
|
||||
binding->axis = axis;
|
||||
return ParseAxisOptions(tokens, token_count, 4,
|
||||
&binding->invert, &binding->deadzone, &binding->rate);
|
||||
}
|
||||
|
||||
if (NameEquals(tokens[0], "joybutton") && NameEquals(tokens[2], "button"))
|
||||
{
|
||||
int button = -1;
|
||||
int address;
|
||||
if (sscanf(tokens[1], "%d", &button) != 1 ||
|
||||
button < 0 || button >= joyButtonCount ||
|
||||
!ParseAddress(tokens[3], &address) ||
|
||||
profile->joyButtonCount >= PadBindingProfile::maxJoyButtons)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
|
||||
PadJoyButtonBinding *binding =
|
||||
&profile->joyButtons[profile->joyButtonCount++];
|
||||
memset(binding, 0, sizeof(*binding));
|
||||
binding->device = *joy_slot;
|
||||
binding->button = button;
|
||||
binding->address = address;
|
||||
binding->toggle = toggle;
|
||||
return True;
|
||||
}
|
||||
|
||||
if (NameEquals(tokens[0], "joyhat") && token_count >= 5 &&
|
||||
NameEquals(tokens[3], "button"))
|
||||
{
|
||||
int hat = -1;
|
||||
int direction = LookupTable(kJoyHatNames,
|
||||
sizeof(kJoyHatNames) / sizeof(kJoyHatNames[0]), tokens[2]);
|
||||
int address;
|
||||
if (sscanf(tokens[1], "%d", &hat) != 1 ||
|
||||
hat < 0 || hat >= joyHatCount || direction < 0 ||
|
||||
!ParseAddress(tokens[4], &address) ||
|
||||
profile->joyHatCount >= PadBindingProfile::maxJoyHats)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
PadJoyHatBinding *binding = &profile->joyHats[profile->joyHatCount++];
|
||||
memset(binding, 0, sizeof(*binding));
|
||||
binding->device = *joy_slot;
|
||||
binding->hat = hat;
|
||||
binding->direction = direction;
|
||||
binding->address = address;
|
||||
return True;
|
||||
}
|
||||
|
||||
@@ -320,10 +452,17 @@ namespace
|
||||
"# key <name> axis <axis> rate <n-per-second>\n"
|
||||
"# pad <button> button <addr> [toggle]\n"
|
||||
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
||||
"# joydev <slot> [product-name substring]\n"
|
||||
"# joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
||||
"# joybutton <n> button <addr> [toggle]\n"
|
||||
"# joyhat <n> <up|down|left|right> button <addr>\n"
|
||||
"#\n"
|
||||
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
|
||||
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
|
||||
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
|
||||
"# | Pedals - a signed axis that works the pedal PAIR, positive\n"
|
||||
"# for the right pedal and negative for the left, so one rudder\n"
|
||||
"# bar or twist grip drives both.\n"
|
||||
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
|
||||
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
|
||||
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
|
||||
@@ -336,6 +475,31 @@ namespace
|
||||
"# back on release; 'rate' walks the axis by <n> per second and the\n"
|
||||
"# position sticks (the throttle). Every lamp button is also clickable\n"
|
||||
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
|
||||
"#\n"
|
||||
"# ---- Flight sticks, HOTAS throttles and rudder pedals --------------\n"
|
||||
"#\n"
|
||||
"# EASIEST: run joyconfig.bat once. It asks you to move each control,\n"
|
||||
"# works out which device and axis you moved and which way round it\n"
|
||||
"# reads, and writes the joy* rows below a marker line at the end of\n"
|
||||
"# this file. Everything you have written yourself is kept. Xbox-class\n"
|
||||
"# pads need none of this - they are the pad* rows above.\n"
|
||||
"#\n"
|
||||
"# By hand: joydev picks the device for the rows that follow it - a\n"
|
||||
"# name substring binds that product, a bare slot number binds the Nth\n"
|
||||
"# stick Windows lists. <src> for joyaxis is the DirectInput axis name,\n"
|
||||
"# X Y Z RX RY RZ SL0 SL1: a twist grip is usually RZ and a HOTAS\n"
|
||||
"# throttle usually Z or SL0. A joyaxis on Throttle with no 'rate' is\n"
|
||||
"# treated as a real lever and OWNS the channel - its full travel is\n"
|
||||
"# the full throttle range, rather than nudging the position the way a\n"
|
||||
"# spring-centred pad stick has to.\n"
|
||||
"#\n"
|
||||
"# joydev 0 T.16000M\n"
|
||||
"# joyaxis X axis JoystickX invert deadzone 0.08\n"
|
||||
"# joyaxis Y axis JoystickY invert deadzone 0.08\n"
|
||||
"# joyaxis RZ axis Pedals deadzone 0.08\n"
|
||||
"# joyaxis SL0 axis Throttle deadzone 0\n"
|
||||
"# joybutton 0 button 0x40\n"
|
||||
"# joyhat 0 up button 0x42\n"
|
||||
"\n"
|
||||
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
||||
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
||||
@@ -499,6 +663,7 @@ void
|
||||
char line[256];
|
||||
int line_number = 0;
|
||||
int error_count = 0;
|
||||
int joy_slot = 0; // joy rows before any joydev belong to slot 0
|
||||
const char *cursor = source;
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
@@ -523,7 +688,7 @@ void
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!ParseLine(tokens, token_count, profile))
|
||||
if (!ParseLine(tokens, token_count, profile, &joy_slot))
|
||||
{
|
||||
++error_count;
|
||||
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
|
||||
@@ -542,4 +707,10 @@ void
|
||||
<< profile->padAxisCount << " pad axes"
|
||||
<< (error_count ? " (with rejected lines)" : "")
|
||||
<< "\n" << std::flush;
|
||||
if (profile->joyAxisCount || profile->joyButtonCount || profile->joyHatCount)
|
||||
{
|
||||
DEBUG_STREAM << "PadBindings: joystick - " << profile->joyAxisCount
|
||||
<< " axes, " << profile->joyButtonCount << " buttons, "
|
||||
<< profile->joyHatCount << " hat directions\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user