Rebindable input: vRIO bindings profile ported, full board coverage

PadRIO now loads bindings.txt (vRIO profile grammar: key/pad/padaxis
lines with toggle, deflect/rate, invert/deadzone options) written
self-documenting with the full default layout on first run. The
default is vRIO board-complete map - number and letter rows are the
MFD banks as printed on the panel, F-keys the secondary/screen
columns, numpad the pilot keypad (0x50-0x5F delivered as arcade RIO
KeyEvents, a new PadRIO capability), Space/arrows the joystick
column - with the desktop driving keys carved out: WASD stick, Q/E
pedals, PgUp/PgDn throttle, B reverse (vRIO gap key; R returns to
its bank). Pad bindings unchanged in spirit, plus Panic on LB and
config on Start/Back; axis signs are encoded in the profile now, so
L4PADFLIP flips on top of it.

Default profile parses with zero rejected lines (68 key buttons, 8
key axes, 12 pad buttons, 5 pad axes); single-player cycle and the
key-bomb tests stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-13 10:18:36 -05:00
co-authored by Claude Fable 5
parent fbd23ff1ca
commit b0def79de2
6 changed files with 844 additions and 105 deletions
+151 -88
View File
@@ -7,54 +7,12 @@
#pragma comment(lib, "xinput9_1_0.lib")
//########################################################################
// Binding tables (vRIO default profile, condensed)
// Input helpers; the binding tables live in bindings.txt now
// (l4padbindings.cpp writes and parses the vRIO-format profile)
//########################################################################
namespace
{
struct KeyBinding
{
int virtualKey;
int rioUnit;
};
const KeyBinding keyboardButtonMap[] =
{
{ VK_SPACE, 0x40 }, // ButtonJoystickTrigger
{ 'R', 0x3F }, // ButtonThrottle1 (reverse thrust)
{ VK_UP, 0x42 }, // ButtonJoystickHatUp
{ VK_DOWN, 0x41 }, // ButtonJoystickHatDown
{ VK_RIGHT, 0x43 }, // ButtonJoystickHatRight
{ VK_LEFT, 0x44 }, // ButtonJoystickHatLeft
{ VK_F1, 0x37 }, // ButtonAuxUpperRight1 (config)
{ VK_F2, 0x36 }, // ButtonAuxUpperRight2 (config)
};
struct PadBinding
{
unsigned short padMask;
int rioUnit;
};
const PadBinding padButtonMap[] =
{
{ XINPUT_GAMEPAD_A, 0x40 }, // trigger
{ XINPUT_GAMEPAD_B, 0x3F }, // reverse thrust
{ XINPUT_GAMEPAD_X, 0x45 }, // pinky
{ XINPUT_GAMEPAD_Y, 0x46 }, // thumb low
{ XINPUT_GAMEPAD_LEFT_SHOULDER, 0x46 }, // thumb low
{ XINPUT_GAMEPAD_RIGHT_SHOULDER, 0x47 }, // thumb high
{ XINPUT_GAMEPAD_DPAD_UP, 0x42 },
{ XINPUT_GAMEPAD_DPAD_DOWN, 0x41 },
{ XINPUT_GAMEPAD_DPAD_RIGHT, 0x43 },
{ XINPUT_GAMEPAD_DPAD_LEFT, 0x44 },
{ XINPUT_GAMEPAD_START, 0x37 }, // config 1
{ XINPUT_GAMEPAD_BACK, 0x36 }, // config 2
};
// Full throttle sweep takes ~1.3 s at full stick deflection.
const Scalar throttleRatePerSecond = 0.75f;
Scalar StickValue(int raw, int dead_zone)
{
if (raw > -dead_zone && raw < dead_zone)
@@ -125,9 +83,12 @@ PadRIO::PadRIO()
sentJoystickX = sentJoystickY = (Scalar) 0;
memset(buttonDown, 0, sizeof(buttonDown));
memset(keypadDown, 0, sizeof(keypadDown));
memset(lampState, 0, sizeof(lampState));
memset(screenButton, 0, sizeof(screenButton));
PadBindings_Load(&profile);
invertX = False;
invertY = False;
const char *flip = getenv("L4PADFLIP");
@@ -208,6 +169,17 @@ void
JoystickY = (Scalar) 0;
analogRequested = True;
memset(lampState, 0, sizeof(lampState));
memset(keypadDown, 0, sizeof(keypadDown));
for (int i = 0; i < profile.keyButtonCount; ++i)
{
profile.keyButtons[i].latched = False;
profile.keyButtons[i].wasDown = False;
}
for (int i = 0; i < profile.padButtonCount; ++i)
{
profile.padButtons[i].latched = False;
profile.padButtons[i].wasDown = False;
}
}
void
@@ -295,27 +267,60 @@ void
}
//---------------------------------------------------------------
// Buttons: build the desired state across pad + keyboard, then
// diff against what we last reported.
// Buttons: build the desired state from the binding profile
// (keyboard + pad, with toggle latches), merge the on-screen
// cockpit buttons, then diff against what we last reported.
// Keypad addresses (0x50-0x6F) collect separately - they become
// arcade KeyEvents, not button events.
//---------------------------------------------------------------
unsigned char desired[buttonUnits];
unsigned char keypadDesired[keypadUnits];
memset(desired, 0, sizeof(desired));
memset(keypadDesired, 0, sizeof(keypadDesired));
if (pad_live)
for (int i = 0; i < profile.keyButtonCount; ++i)
{
for (int i = 0; i < (int)(sizeof(padButtonMap)/sizeof(padButtonMap[0])); ++i)
PadKeyButtonBinding *binding = &profile.keyButtons[i];
Logical down = KeyDown(binding->virtualKey);
if (binding->toggle && down && !binding->wasDown)
{
if (pad.Gamepad.wButtons & padButtonMap[i].padMask)
binding->latched = !binding->latched;
}
binding->wasDown = down;
if (binding->toggle ? binding->latched : down)
{
if (binding->address < buttonUnits)
{
desired[padButtonMap[i].rioUnit] = 1;
desired[binding->address] = 1;
}
else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits)
{
keypadDesired[binding->address - 0x50] = 1;
}
}
}
for (int i = 0; i < (int)(sizeof(keyboardButtonMap)/sizeof(keyboardButtonMap[0])); ++i)
for (int i = 0; i < profile.padButtonCount; ++i)
{
if (KeyDown(keyboardButtonMap[i].virtualKey))
PadPadButtonBinding *binding = &profile.padButtons[i];
Logical down = pad_live &&
(pad.Gamepad.wButtons & binding->padMask) != 0;
if (binding->toggle && down && !binding->wasDown)
{
desired[keyboardButtonMap[i].rioUnit] = 1;
binding->latched = !binding->latched;
}
binding->wasDown = down;
if (binding->toggle ? binding->latched : down)
{
if (binding->address < buttonUnits)
{
desired[binding->address] = 1;
}
else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits)
{
keypadDesired[binding->address - 0x50] = 1;
}
}
}
for (int i = 0; i < buttonUnits; ++i)
@@ -340,53 +345,111 @@ void
}
//---------------------------------------------------------------
// Axes
// Keypads: presses become the arcade RIO KeyEvents. Unit 0 is the
// pilot's internal keypad (0x50-0x5F), unit 1 the external
// operator keypad (0x60-0x6F); the key is the hex digit 0-15.
//---------------------------------------------------------------
Scalar x = (Scalar) 0, y = (Scalar) 0;
Scalar left_pedal = (Scalar) 0, right_pedal = (Scalar) 0;
Scalar throttle_rate = (Scalar) 0;
if (pad_live)
for (int pad_key = 0; pad_key < keypadUnits; ++pad_key)
{
x += StickValue(pad.Gamepad.sThumbLX, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
y += StickValue(pad.Gamepad.sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
throttle_rate += StickValue(pad.Gamepad.sThumbRY, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE);
if (pad.Gamepad.bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
if (keypadDesired[pad_key] != keypadDown[pad_key])
{
left_pedal += (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f;
}
if (pad.Gamepad.bRightTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
{
right_pedal += (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f;
keypadDown[pad_key] = keypadDesired[pad_key];
if (keypadDesired[pad_key])
{
RIOEvent an_event;
an_event.Type = KeyEvent;
an_event.Data.Keyboard.Unit = (pad_key >= 0x10) ? 1 : 0;
an_event.Data.Keyboard.Key = pad_key & 0x0F;
QueueEvent(an_event);
}
}
}
// keyboard: WASD stick deflect, Q/E pedals, PgUp/PgDn throttle
if (KeyDown('A')) x -= 1.0f;
if (KeyDown('D')) x += 1.0f;
if (KeyDown('W')) y += 1.0f;
if (KeyDown('S')) y -= 1.0f;
if (KeyDown('Q')) left_pedal += 1.0f;
if (KeyDown('E')) right_pedal += 1.0f;
if (KeyDown(VK_PRIOR)) throttle_rate += 1.0f; // PgUp
if (KeyDown(VK_NEXT)) throttle_rate -= 1.0f; // PgDn
//---------------------------------------------------------------
// Axes, from the profile. 'deflect' sources sum into a springy
// position; 'rate' sources integrate the throttle (the pod's only
// sticky axis) by value per second.
//---------------------------------------------------------------
Scalar deflect[BindAxisCount];
Scalar rate[BindAxisCount];
memset(deflect, 0, sizeof(deflect));
memset(rate, 0, sizeof(rate));
for (int i = 0; i < profile.keyAxisCount; ++i)
{
const PadKeyAxisBinding *binding = &profile.keyAxes[i];
if (KeyDown(binding->virtualKey))
{
if (binding->mode == BindKeyRate)
{
rate[binding->axis] += binding->value;
}
else
{
deflect[binding->axis] += binding->value;
}
}
}
if (pad_live)
{
for (int i = 0; i < profile.padAxisCount; ++i)
{
const PadPadAxisBinding *binding = &profile.padAxes[i];
Scalar value = (Scalar) 0;
switch (binding->source)
{
case BindPadLeftStickX:
value = StickValue(pad.Gamepad.sThumbLX, (int)(binding->deadzone * 32767.0f));
break;
case BindPadLeftStickY:
value = StickValue(pad.Gamepad.sThumbLY, (int)(binding->deadzone * 32767.0f));
break;
case BindPadRightStickX:
value = StickValue(pad.Gamepad.sThumbRX, (int)(binding->deadzone * 32767.0f));
break;
case BindPadRightStickY:
value = StickValue(pad.Gamepad.sThumbRY, (int)(binding->deadzone * 32767.0f));
break;
case BindPadLeftTrigger:
value = (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f;
if (value <= binding->deadzone) value = (Scalar) 0;
break;
case BindPadRightTrigger:
value = (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f;
if (value <= binding->deadzone) value = (Scalar) 0;
break;
}
if (binding->invert)
{
value = -value;
}
if (binding->rate > 0.0f)
{
rate[binding->axis] += value * binding->rate;
}
else
{
deflect[binding->axis] += value;
}
}
}
throttleAccum = Clamp01(throttleAccum + rate[BindAxisThrottle] * delta_t);
Scalar x = deflect[BindAxisJoystickX];
Scalar y = deflect[BindAxisJoystickY];
if (x > 1.0f) x = 1.0f;
if (x < -1.0f) x = -1.0f;
if (y > 1.0f) y = 1.0f;
if (y < -1.0f) y = -1.0f;
throttleAccum = Clamp01(throttleAccum + throttle_rate * throttleRatePerSecond * delta_t);
Throttle = throttleAccum;
LeftPedal = Clamp01(left_pedal);
RightPedal = Clamp01(right_pedal);
// The pod's stick convention is opposite the XInput axes on both X and
// Y (confirmed in playtest), so the default is negated; L4PADFLIP
// flips back per axis.
JoystickX = invertX ? x : -x;
JoystickY = invertY ? y : -y;
Throttle = Clamp01(throttleAccum + deflect[BindAxisThrottle]);
LeftPedal = Clamp01(deflect[BindAxisLeftPedal]);
RightPedal = Clamp01(deflect[BindAxisRightPedal]);
// The profile encodes the pod's stick sign convention; L4PADFLIP
// flips on top of it per axis.
JoystickX = invertX ? -x : x;
JoystickY = invertY ? -y : y;
//---------------------------------------------------------------
// Emit an analog event when asked to, or when anything moved