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>
479 lines
12 KiB
C++
479 lines
12 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4padrio.h"
|
|
|
|
#include <XInput.h>
|
|
#pragma comment(lib, "xinput9_1_0.lib")
|
|
|
|
//########################################################################
|
|
// Input helpers; the binding tables live in bindings.txt now
|
|
// (l4padbindings.cpp writes and parses the vRIO-format profile)
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
Scalar StickValue(int raw, int dead_zone)
|
|
{
|
|
if (raw > -dead_zone && raw < dead_zone)
|
|
{
|
|
return (Scalar) 0;
|
|
}
|
|
Scalar value =
|
|
(raw > 0)
|
|
? (Scalar)(raw - dead_zone) / (Scalar)(32767 - dead_zone)
|
|
: (Scalar)(raw + dead_zone) / (Scalar)(32768 - dead_zone);
|
|
if (value > 1.0f) value = 1.0f;
|
|
if (value < -1.0f) value = -1.0f;
|
|
return value;
|
|
}
|
|
|
|
Scalar Clamp01(Scalar value)
|
|
{
|
|
if (value < 0.0f) return 0.0f;
|
|
if (value > 1.0f) return 1.0f;
|
|
return value;
|
|
}
|
|
|
|
Logical KeyDown(int virtual_key)
|
|
{
|
|
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
|
|
}
|
|
}
|
|
|
|
//########################################################################
|
|
//############################### PadRIO #################################
|
|
//########################################################################
|
|
|
|
PadRIO *PadRIO::activeInstance = NULL;
|
|
|
|
void
|
|
PadRIO::SetScreenButton(int unit, Logical pressed)
|
|
{
|
|
if (activeInstance != NULL && unit >= 0 && unit < buttonUnits)
|
|
{
|
|
activeInstance->screenButton[unit] = pressed ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
int
|
|
PadRIO::GetLampState(int unit)
|
|
{
|
|
if (activeInstance != NULL && unit >= 0 && unit < lampCount)
|
|
{
|
|
return activeInstance->lampState[unit];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
PadRIO::PadRIO()
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
queueHead = 0;
|
|
queueTail = 0;
|
|
lastPollTick = GetTickCount();
|
|
lastPadCheckTick = 0;
|
|
padIndex = -1;
|
|
padReported = False;
|
|
analogRequested = False;
|
|
throttleAccum = (Scalar) 0;
|
|
|
|
sentThrottle = sentLeftPedal = sentRightPedal = (Scalar) 0;
|
|
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");
|
|
if (flip != NULL)
|
|
{
|
|
if (strchr(flip, 'X') || strchr(flip, 'x'))
|
|
{
|
|
invertX = True;
|
|
}
|
|
if (strchr(flip, 'Y') || strchr(flip, 'y'))
|
|
{
|
|
invertY = True;
|
|
}
|
|
}
|
|
|
|
// Report as a v4.2 board, like vRIO does
|
|
MajorRevision = 4;
|
|
MinorRevision = 2;
|
|
|
|
activeInstance = this;
|
|
|
|
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
|
|
}
|
|
|
|
PadRIO::~PadRIO()
|
|
{
|
|
Check_Pointer(this);
|
|
if (activeInstance == this)
|
|
{
|
|
activeInstance = NULL;
|
|
}
|
|
}
|
|
|
|
Logical
|
|
PadRIO::TestInstance() const
|
|
{
|
|
return True;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// The controls manager drains events every frame; sampling lives here so
|
|
// button latency does not depend on the analog request cadence (which is
|
|
// 15 s outside of missions).
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
Logical
|
|
PadRIO::GetNextEvent(RIOEvent *destinationPointer)
|
|
{
|
|
Check_Pointer(this);
|
|
Check_Pointer(destinationPointer);
|
|
|
|
PollInputs();
|
|
|
|
if (queueTail == queueHead)
|
|
{
|
|
return False;
|
|
}
|
|
*destinationPointer = eventQueue[queueTail];
|
|
queueTail = (queueTail + 1) % queueSize;
|
|
return True;
|
|
}
|
|
|
|
void
|
|
PadRIO::RequestAnalogUpdate()
|
|
{
|
|
Check_Pointer(this);
|
|
analogRequested = True;
|
|
}
|
|
|
|
void
|
|
PadRIO::GeneralReset()
|
|
{
|
|
Check_Pointer(this);
|
|
throttleAccum = (Scalar) 0;
|
|
Throttle = (Scalar) 0;
|
|
LeftPedal = (Scalar) 0;
|
|
RightPedal = (Scalar) 0;
|
|
JoystickX = (Scalar) 0;
|
|
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
|
|
PadRIO::ResetThrottle()
|
|
{
|
|
Check_Pointer(this);
|
|
throttleAccum = (Scalar) 0;
|
|
Throttle = (Scalar) 0;
|
|
analogRequested = True;
|
|
}
|
|
|
|
void
|
|
PadRIO::SetLamp(int lampNumber, int state)
|
|
{
|
|
Check_Pointer(this);
|
|
if (lampNumber >= 0 && lampNumber < lampCount)
|
|
{
|
|
lampState[lampNumber] = (unsigned char) state;
|
|
}
|
|
}
|
|
|
|
void
|
|
PadRIO::QueueEvent(const RIOEvent &an_event)
|
|
{
|
|
int next = (queueHead + 1) % queueSize;
|
|
if (next == queueTail)
|
|
{
|
|
// full: drop the oldest event
|
|
queueTail = (queueTail + 1) % queueSize;
|
|
}
|
|
eventQueue[queueHead] = an_event;
|
|
queueHead = next;
|
|
}
|
|
|
|
void
|
|
PadRIO::PollInputs()
|
|
{
|
|
unsigned long now = GetTickCount();
|
|
if (now - lastPollTick < 10)
|
|
{
|
|
return;
|
|
}
|
|
Scalar delta_t = (Scalar)(now - lastPollTick) / 1000.0f;
|
|
if (delta_t > 0.25f)
|
|
{
|
|
delta_t = 0.25f;
|
|
}
|
|
lastPollTick = now;
|
|
|
|
//---------------------------------------------------------------
|
|
// Find / keep the XInput pad. Probing empty slots is slow, so an
|
|
// absent pad is only re-probed every 3 seconds.
|
|
//---------------------------------------------------------------
|
|
XINPUT_STATE pad;
|
|
memset(&pad, 0, sizeof(pad));
|
|
Logical pad_live = False;
|
|
|
|
if (padIndex >= 0)
|
|
{
|
|
pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS);
|
|
if (!pad_live)
|
|
{
|
|
DEBUG_STREAM << "PadRIO: controller " << padIndex << " disconnected\n" << std::flush;
|
|
padIndex = -1;
|
|
}
|
|
}
|
|
if (padIndex < 0 && (now - lastPadCheckTick) >= 3000)
|
|
{
|
|
lastPadCheckTick = now;
|
|
for (DWORD i = 0; i < 4; ++i)
|
|
{
|
|
if (XInputGetState(i, &pad) == ERROR_SUCCESS)
|
|
{
|
|
padIndex = (int) i;
|
|
pad_live = True;
|
|
DEBUG_STREAM << "PadRIO: controller " << padIndex << " connected\n" << std::flush;
|
|
break;
|
|
}
|
|
}
|
|
if (padIndex < 0 && !padReported)
|
|
{
|
|
padReported = True;
|
|
DEBUG_STREAM << "PadRIO: no controller found - keyboard only\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// 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));
|
|
|
|
for (int i = 0; i < profile.keyButtonCount; ++i)
|
|
{
|
|
PadKeyButtonBinding *binding = &profile.keyButtons[i];
|
|
Logical down = KeyDown(binding->virtualKey);
|
|
if (binding->toggle && down && !binding->wasDown)
|
|
{
|
|
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 < profile.padButtonCount; ++i)
|
|
{
|
|
PadPadButtonBinding *binding = &profile.padButtons[i];
|
|
Logical down = pad_live &&
|
|
(pad.Gamepad.wButtons & binding->padMask) != 0;
|
|
if (binding->toggle && down && !binding->wasDown)
|
|
{
|
|
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)
|
|
{
|
|
if (screenButton[i])
|
|
{
|
|
desired[i] = 1;
|
|
}
|
|
}
|
|
|
|
for (int unit = 0; unit < buttonUnits; ++unit)
|
|
{
|
|
if (desired[unit] != buttonDown[unit])
|
|
{
|
|
buttonDown[unit] = desired[unit];
|
|
|
|
RIOEvent an_event;
|
|
an_event.Type = desired[unit] ? ButtonPressedEvent : ButtonReleasedEvent;
|
|
an_event.Data.Unit = unit;
|
|
QueueEvent(an_event);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// 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.
|
|
//---------------------------------------------------------------
|
|
for (int pad_key = 0; pad_key < keypadUnits; ++pad_key)
|
|
{
|
|
if (keypadDesired[pad_key] != keypadDown[pad_key])
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// 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;
|
|
|
|
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
|
|
//---------------------------------------------------------------
|
|
Logical changed =
|
|
(Throttle != sentThrottle) ||
|
|
(LeftPedal != sentLeftPedal) ||
|
|
(RightPedal != sentRightPedal) ||
|
|
(JoystickX != sentJoystickX) ||
|
|
(JoystickY != sentJoystickY);
|
|
|
|
if (analogRequested || changed)
|
|
{
|
|
analogRequested = False;
|
|
sentThrottle = Throttle;
|
|
sentLeftPedal = LeftPedal;
|
|
sentRightPedal = RightPedal;
|
|
sentJoystickX = JoystickX;
|
|
sentJoystickY = JoystickY;
|
|
|
|
RIOEvent an_event;
|
|
an_event.Type = AnalogEvent;
|
|
an_event.Data.Unit = 0;
|
|
QueueEvent(an_event);
|
|
}
|
|
}
|