Files
RP412/MUNGA_L4/L4PADRIO.cpp
T
CydandClaude Fable 5 1058de326d Cockpit buttons on the split displays, lamp-lit and clickable
Each MFDSplitView window now carries its display's physical button bank,
placed as in the pod (addresses per vRIO CockpitLayout): a 4x2 red
cluster around each MFD glass (anchors 0x2F/0x27/0x37 upper, 0x0F/0x07
lower, addresses descending row-major) and 6 amber buttons down each
side of the map - Secondary 0x10-0x15 left, Screen 0x18-0x1D right; the
remaining column addresses are Tesla relays, per the pod wiring, so they
get no buttons.

Buttons light from the lamp state the game commands: PadRIO grows a
static active-instance hook (SetScreenButton/GetLampState); mouse
press/release feeds PadRIO's desired-state sampling alongside pad and
keyboard, and paint decodes the lamp byte (state1/state2 brightness,
solid/slow/med/fast flash animated by tick). With real serial hardware
(no PadRIO) the buttons draw dark and inert.

Verified: map flank buttons light per the game's preset lamps, aligned
with the labels the glass draws at its edges; MFD clusters render 4+4.
Roadmap: queued the vRIO Dynamic Lighting RGB-keyboard lamp mirror as a
polish-pass item. dist repacked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:22:38 -05:00

416 lines
10 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4padrio.h"
#include <XInput.h>
#pragma comment(lib, "xinput9_1_0.lib")
//########################################################################
// Binding tables (vRIO default profile, condensed)
//########################################################################
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)
{
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(lampState, 0, sizeof(lampState));
memset(screenButton, 0, sizeof(screenButton));
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));
}
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 across pad + keyboard, then
// diff against what we last reported.
//---------------------------------------------------------------
unsigned char desired[buttonUnits];
memset(desired, 0, sizeof(desired));
if (pad_live)
{
for (int i = 0; i < (int)(sizeof(padButtonMap)/sizeof(padButtonMap[0])); ++i)
{
if (pad.Gamepad.wButtons & padButtonMap[i].padMask)
{
desired[padButtonMap[i].rioUnit] = 1;
}
}
}
for (int i = 0; i < (int)(sizeof(keyboardButtonMap)/sizeof(keyboardButtonMap[0])); ++i)
{
if (KeyDown(keyboardButtonMap[i].virtualKey))
{
desired[keyboardButtonMap[i].rioUnit] = 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);
}
}
//---------------------------------------------------------------
// Axes
//---------------------------------------------------------------
Scalar x = (Scalar) 0, y = (Scalar) 0;
Scalar left_pedal = (Scalar) 0, right_pedal = (Scalar) 0;
Scalar throttle_rate = (Scalar) 0;
if (pad_live)
{
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)
{
left_pedal += (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f;
}
if (pad.Gamepad.bRightTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
{
right_pedal += (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f;
}
}
// 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
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;
//---------------------------------------------------------------
// 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);
}
}