Generic joystick / HOTAS / pedals support: DirectInput 8 layer + capture wizard
Tester ask: configure non-Xbox controllers. New L4JOY layer (DI8): enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling with range normalization, reacquire-on-loss, 3s hot-plug re-enum. XInput devices are excluded via the documented RawInput IG_ VID/PID check (live-verified skipping a real XBOX360 pad -- no double-feed). bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/ deadzone; hats as 4-direction buttons with diagonal chords). PadRIO runs them through the existing channel/event machinery -- per-binding edge arrays release automatically on detach (the #24 rule), and a direct throttle axis maps full lever travel onto the 0..1 channel while the gait detent still snaps a lever parked in the walk/run dead band. BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move each control when prompted (stick/twist/throttle/fire), invert derived from the move direction, hat look cluster auto-added, output written between markers in bindings.txt with the rest of the file preserved. Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched. Verified: grammar accept/reject per line, XInput-exclusion live, 30s in-mission no-device polling clean. Awaiting a tester with real stick hardware for axis verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
86e387b6c7
commit
c2ee7299b2
@@ -101,6 +101,26 @@ static const NamedValue channelNames[] =
|
||||
{ "Turn", PadBindingProfile::ChannelTurn }, // issue #25 composite
|
||||
};
|
||||
|
||||
//
|
||||
// Generic-joystick axis names (the DIJOYSTATE2 layout -- L4JOY.h order).
|
||||
//
|
||||
static const NamedValue joyAxisNames[] =
|
||||
{
|
||||
{ "X", PadBindingProfile::JoyAxisX },
|
||||
{ "Y", PadBindingProfile::JoyAxisY },
|
||||
{ "Z", PadBindingProfile::JoyAxisZ },
|
||||
{ "RX", PadBindingProfile::JoyAxisRX },
|
||||
{ "RY", PadBindingProfile::JoyAxisRY },
|
||||
{ "RZ", PadBindingProfile::JoyAxisRZ },
|
||||
{ "SL0", PadBindingProfile::JoyAxisSL0 },
|
||||
{ "SL1", PadBindingProfile::JoyAxisSL1 },
|
||||
};
|
||||
|
||||
static const NamedValue hatDirectionNames[] =
|
||||
{
|
||||
{ "up", 0 }, { "right", 1 }, { "down", 2 }, { "left", 3 },
|
||||
};
|
||||
|
||||
static int
|
||||
LookupName(const NamedValue *table, int count, const char *name)
|
||||
{
|
||||
@@ -252,7 +272,28 @@ static const char *defaultProfileText =
|
||||
"pad DPAD_UP button 0x42\n"
|
||||
"pad DPAD_DOWN button 0x41\n"
|
||||
"pad DPAD_LEFT button 0x44\n"
|
||||
"pad DPAD_RIGHT button 0x43\n";
|
||||
"pad DPAD_RIGHT button 0x43\n"
|
||||
"\n"
|
||||
"# --- generic joysticks (flight sticks / HOTAS / pedals; DirectInput) ---\n"
|
||||
"# EASIEST: run the game once with BT_JOYCONFIG=1 (joyconfig.bat) -- an\n"
|
||||
"# interactive wizard detects your controls and writes this section for you.\n"
|
||||
"# Manual grammar (rows attach to the last joydev slot; slot 0 if none):\n"
|
||||
"# joydev <slot 0-3> [product-name substring]\n"
|
||||
"# joyaxis <X|Y|Z|RX|RY|RZ|SL0|SL1> axis <channel> [invert] [slew <r>] [deadzone <f>]\n"
|
||||
"# joybutton <0-31> button <addr>\n"
|
||||
"# joyhat <0-3> <up|down|left|right> button <addr>\n"
|
||||
"# Typical flight stick (twist = RZ, throttle wheel = SL0):\n"
|
||||
"# joydev 0\n"
|
||||
"# joyaxis X axis JoystickX\n"
|
||||
"# joyaxis Y axis JoystickY invert\n"
|
||||
"# joyaxis RZ axis Turn\n"
|
||||
"# joyaxis SL0 axis Throttle invert deadzone 0\n"
|
||||
"# joybutton 0 button 0x40\n"
|
||||
"# joybutton 1 button 0x46\n"
|
||||
"# joyhat 0 up button 0x42\n"
|
||||
"# joyhat 0 down button 0x41\n"
|
||||
"# joyhat 0 left button 0x44\n"
|
||||
"# joyhat 0 right button 0x43\n";
|
||||
|
||||
//###########################################################################
|
||||
// PadBindingProfile
|
||||
@@ -261,8 +302,13 @@ static const char *defaultProfileText =
|
||||
PadBindingProfile::PadBindingProfile():
|
||||
keyBindingCount(0),
|
||||
padButtonBindingCount(0),
|
||||
padAxisBindingCount(0)
|
||||
padAxisBindingCount(0),
|
||||
joyAxisBindingCount(0),
|
||||
joyButtonBindingCount(0),
|
||||
joyHatBindingCount(0),
|
||||
parseJoyDevice(0)
|
||||
{
|
||||
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
|
||||
}
|
||||
|
||||
PadBindingProfile::~PadBindingProfile()
|
||||
@@ -368,7 +414,97 @@ int
|
||||
}
|
||||
|
||||
//
|
||||
// key/pad rows: parse the action clause.
|
||||
// joydev <slot> [name substring...] -- select the device slot for the
|
||||
// joy rows that follow; optional product-name match (joined verbatim).
|
||||
//
|
||||
if (_stricmp(tokens[0], "joydev") == 0)
|
||||
{
|
||||
int slot = (int)strtol(tokens[1], NULL, 0);
|
||||
if (slot < 0 || slot >= JoyDeviceSlots)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
parseJoyDevice = slot;
|
||||
joyDeviceMatch[slot][0] = '\0';
|
||||
for (int t = 2; t < tokenCount; ++t)
|
||||
{
|
||||
if (t > 2)
|
||||
{
|
||||
strncat(joyDeviceMatch[slot], " ",
|
||||
sizeof(joyDeviceMatch[slot]) - strlen(joyDeviceMatch[slot]) - 1);
|
||||
}
|
||||
strncat(joyDeviceMatch[slot], tokens[t],
|
||||
sizeof(joyDeviceMatch[slot]) - strlen(joyDeviceMatch[slot]) - 1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// joyaxis <AXIS> axis <channel> [invert] [slew <rate>] [deadzone <f>]
|
||||
//
|
||||
if (_stricmp(tokens[0], "joyaxis") == 0)
|
||||
{
|
||||
if (tokenCount < 4 || _stricmp(tokens[2], "axis") != 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
int axis = LookupName(joyAxisNames,
|
||||
sizeof(joyAxisNames)/sizeof(joyAxisNames[0]), tokens[1]);
|
||||
int channel = LookupName(channelNames,
|
||||
sizeof(channelNames)/sizeof(channelNames[0]), tokens[3]);
|
||||
if (axis < 0 || channel < 0 ||
|
||||
joyAxisBindingCount >= (int)(sizeof(joyAxisBindings)/sizeof(joyAxisBindings[0])))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
JoyAxisBinding &b = joyAxisBindings[joyAxisBindingCount];
|
||||
b.device = parseJoyDevice;
|
||||
b.axis = axis;
|
||||
b.channel = channel;
|
||||
b.invert = 0;
|
||||
b.slew = 0;
|
||||
b.slewRate = 0.0f;
|
||||
b.deadzone = 0.08f; // stick default; levers set deadzone 0
|
||||
for (int t = 4; t < tokenCount; ++t)
|
||||
{
|
||||
if (_stricmp(tokens[t], "invert") == 0)
|
||||
{
|
||||
b.invert = 1;
|
||||
}
|
||||
else if (_stricmp(tokens[t], "slew") == 0 && t+1 < tokenCount)
|
||||
{
|
||||
b.slew = 1;
|
||||
b.slewRate = (float)atof(tokens[++t]);
|
||||
}
|
||||
else if (_stricmp(tokens[t], "deadzone") == 0 && t+1 < tokenCount)
|
||||
{
|
||||
b.deadzone = (float)atof(tokens[++t]);
|
||||
if (b.deadzone < 0.0f) b.deadzone = 0.0f;
|
||||
if (b.deadzone > 0.9f) b.deadzone = 0.9f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
++joyAxisBindingCount;
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// joyhat's action clause starts one token later (joyhat <n> <dir> ...).
|
||||
//
|
||||
if (_stricmp(tokens[0], "joyhat") == 0)
|
||||
{
|
||||
actionAt = 3;
|
||||
if (tokenCount < 5)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// key/pad/joybutton/joyhat rows: parse the action clause.
|
||||
//
|
||||
if (_stricmp(tokens[actionAt], "button") == 0)
|
||||
{
|
||||
@@ -486,6 +622,45 @@ int
|
||||
++padButtonBindingCount;
|
||||
return 0;
|
||||
}
|
||||
else if (_stricmp(tokens[0], "joybutton") == 0)
|
||||
{
|
||||
if (action.kind != ActionButton && action.kind != ActionKeypad)
|
||||
{
|
||||
return -1; // joystick buttons cannot drive axes; use joyaxis
|
||||
}
|
||||
int button = (int)strtol(tokens[1], NULL, 0);
|
||||
if (button < 0 || button >= 32 ||
|
||||
joyButtonBindingCount >= (int)(sizeof(joyButtonBindings)/sizeof(joyButtonBindings[0])))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
joyButtonBindings[joyButtonBindingCount].device = parseJoyDevice;
|
||||
joyButtonBindings[joyButtonBindingCount].button = button;
|
||||
joyButtonBindings[joyButtonBindingCount].action = action;
|
||||
++joyButtonBindingCount;
|
||||
return 0;
|
||||
}
|
||||
else if (_stricmp(tokens[0], "joyhat") == 0)
|
||||
{
|
||||
if (action.kind != ActionButton)
|
||||
{
|
||||
return -1; // hat directions map to RIO buttons only
|
||||
}
|
||||
int hat = (int)strtol(tokens[1], NULL, 0);
|
||||
int direction = LookupName(hatDirectionNames,
|
||||
sizeof(hatDirectionNames)/sizeof(hatDirectionNames[0]), tokens[2]);
|
||||
if (hat < 0 || hat >= 4 || direction < 0 ||
|
||||
joyHatBindingCount >= (int)(sizeof(joyHatBindings)/sizeof(joyHatBindings[0])))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
joyHatBindings[joyHatBindingCount].device = parseJoyDevice;
|
||||
joyHatBindings[joyHatBindingCount].hat = hat;
|
||||
joyHatBindings[joyHatBindingCount].direction = direction;
|
||||
joyHatBindings[joyHatBindingCount].action = action;
|
||||
++joyHatBindingCount;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -495,6 +670,11 @@ void
|
||||
keyBindingCount = 0;
|
||||
padButtonBindingCount = 0;
|
||||
padAxisBindingCount = 0;
|
||||
joyAxisBindingCount = 0;
|
||||
joyButtonBindingCount = 0;
|
||||
joyHatBindingCount = 0;
|
||||
parseJoyDevice = 0;
|
||||
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
|
||||
|
||||
//
|
||||
// Parse the built-in default text line by line (one code path for
|
||||
@@ -540,6 +720,11 @@ void
|
||||
keyBindingCount = 0;
|
||||
padButtonBindingCount = 0;
|
||||
padAxisBindingCount = 0;
|
||||
joyAxisBindingCount = 0;
|
||||
joyButtonBindingCount = 0;
|
||||
joyHatBindingCount = 0;
|
||||
parseJoyDevice = 0;
|
||||
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
|
||||
|
||||
char line[256];
|
||||
int line_number = 0;
|
||||
@@ -576,7 +761,10 @@ void
|
||||
}
|
||||
DEBUG_STREAM << "[padrio] bindings loaded: " << keyBindingCount
|
||||
<< " keys, " << padButtonBindingCount << " pad buttons, "
|
||||
<< padAxisBindingCount << " pad axes"
|
||||
<< padAxisBindingCount << " pad axes, "
|
||||
<< joyAxisBindingCount << " joy axes, "
|
||||
<< joyButtonBindingCount << " joy buttons, "
|
||||
<< joyHatBindingCount << " joy hats"
|
||||
<< (rejected ? " (rejected lines logged above)" : "")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user