Files
RP412/MUNGA_L4/L4PADRIO.cpp
T
CydandClaude Opus 5 827c5b295b The controls answer only while the game is the window in front
Testers taking notes in another window were flying the pod while they
typed. RP412INPUTFOCUS=1 is the new default; 0 restores the old
behaviour.

The pod was the only thing running on its cabinet, so the virtual RIO
reads the key state directly rather than waiting on the message pump.
That is the right call for latency and it is why the pedals feel like
pedals - but a direct read is a read of the WHOLE keyboard, whatever has
focus. On a cabinet that distinction did not exist. On a desktop it is
the difference between writing a bug report and steering into a wall
while you write it.

One choke point does the whole job: PadRIO::PollInputs is where the
keyboard, the XInput pad and the DirectInput stick are all read, so a
single flag covers the three of them. The joystick needs no change of
its own - unfocused the resolve block is skipped, every device slot
stays at -1, and the button, hat and axis loops find no device and read
released on their own. It is opened DISCL_BACKGROUND on purpose, or it
would stop answering the moment a cockpit pane took focus, so declining
to poll it is what makes it go quiet.

Each source reads as RELEASED rather than the poll returning early, and
that is the part worth keeping: bail out instead and whatever was held
at the moment you switched away stays held until you come back, which is
the stuck throttle this is meant to prevent rather than cause. Reading
released lets the diffs already in there turn it into proper release
events.

The throttle accumulator is the deliberate exception. It is the pod's
one sticky axis and it integrates what the controls ask for, so controls
asking for nothing simply stop moving it - you come back to the speed
you left rather than to a dead stop.

Focus is tested per PROCESS, not against one window handle. The cockpit
is a shell full of child panes, the exploded view is six windows of its
own and the plasma glass another; matching a single HWND would drop the
controls the moment somebody clicked an MFD.

Real RIO cockpit hardware is untouched - this is the keyboard, pad and
joystick path only. The volume and bass keys in L4CTRL were already
gated this way, unconditionally, which is where the idiom comes from.

On by default because the alternative is every tester editing a file
before the fix reaches them: an environ.ini written by an older build
does not carry the line, so the built-in default is what they get. The
log says which way it is set, and the option-mention check names it as
one they have not heard of.

Verified against the built exe both ways: a fresh run writes the
documented default and applies 14 settings where it applied 13, and a
file with the line removed reports exactly one unknown option and falls
back to focus-gated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:24:40 -05:00

854 lines
23 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4padrio.h"
#include "l4keylight.h"
#include "l4joy.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;
}
//
// RP412INPUTFOCUS - do the controls answer only while the game is the
// window in front? On unless the file says 0.
//
// The pod was the only thing running on its cabinet, so the virtual
// RIO reads the key state directly rather than waiting on the message
// pump. That is the right call for latency and it is why the pedals
// feel like pedals - but a direct read is a read of the WHOLE
// keyboard, whatever has focus, so a player alt-tabbed into a text
// editor was flying the pod with every note they typed.
//
Logical InputNeedsFocus()
{
static int setting = -1;
if (setting < 0)
{
const char *value = getenv("RP412INPUTFOCUS");
setting = (value != NULL && *value == '0') ? 0 : 1;
}
return setting ? True : False;
}
//
// Whether the foreground window is one of OURS - the process, not one
// particular handle. The cockpit is a shell full of child panes, the
// exploded view is six windows of its own and the plasma glass
// another, so any of them being in front is the game being in front.
// Matching a single HWND would drop the controls the moment somebody
// clicked an MFD.
//
Logical ProcessHasFocus()
{
HWND foreground = GetForegroundWindow();
if (foreground == NULL)
{
return False;
}
DWORD foreground_process = 0;
GetWindowThreadProcessId(foreground, &foreground_process);
return (foreground_process == GetCurrentProcessId()) ? True : False;
}
//
// A generic stick axis is already normalized -1..1, so the deadzone
// is a plain cut about centre with the remainder rescaled - press
// just past the edge and you get just past zero, not a step.
//
Scalar JoyAxisValue(Scalar raw, Scalar deadzone)
{
if (deadzone <= 0.0f)
{
return raw;
}
if (raw > -deadzone && raw < deadzone)
{
return (Scalar) 0;
}
Scalar value = (raw > 0.0f)
? (raw - deadzone) / (1.0f - deadzone)
: (raw + deadzone) / (1.0f - deadzone);
if (value > 1.0f) value = 1.0f;
if (value < -1.0f) value = -1.0f;
return value;
}
//
// A one-way control - a floor pedal, a slider - rests at one END of
// its travel, not in the middle, and DirectInput still reports it as
// a full -1..1 axis. Fold that travel onto 0..1 so the pedal starts
// answering as soon as it moves instead of at half depression, and
// measure the deadzone from the released end, where the slack in a
// tired return spring actually lives.
//
Scalar JoyLeverValue(Scalar raw, Scalar deadzone)
{
Scalar value = (raw + 1.0f) * 0.5f;
if (value <= deadzone)
{
return (Scalar) 0;
}
if (value > 1.0f) value = 1.0f;
return value;
}
//
// A POV hat reports centidegrees clockwise from up, or -1 centered.
// The 45-degree window each way is what makes the diagonals press
// both of their neighbours, which is how a four-way hat is read.
//
Logical JoyHatHeld(int centidegrees, int direction)
{
if (centidegrees < 0)
{
return False;
}
int degrees = (centidegrees / 100) % 360;
switch (direction)
{
case 0: return (degrees >= 315 || degrees <= 45) ? True : False;
case 1: return (degrees >= 45 && degrees <= 135) ? True : False;
case 2: return (degrees >= 135 && degrees <= 225) ? True : False;
case 3: return (degrees >= 225 && degrees <= 315) ? True : False;
}
return False;
}
void KeyLightLog(const char *line)
{
DEBUG_STREAM << line << "\n" << std::flush;
}
}
//########################################################################
//############################### 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);
//
// RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound
// to lamp addresses glow with the panel. Yellow = the Secondary /
// Screen columns (0x10-0x1F), red = everything else, exactly like
// the physical panel and vRIO. RP412KEYLIGHT=0 opts out.
//
keyLightActive = False;
const char *keylight = getenv("RP412KEYLIGHT");
if (keylight == NULL || atoi(keylight) != 0)
{
int light_keys[PadBindingProfile::maxKeyButtons];
int light_addresses[PadBindingProfile::maxKeyButtons];
unsigned char light_yellow[PadBindingProfile::maxKeyButtons];
int light_count = 0;
for (int i = 0; i < profile.keyButtonCount; ++i)
{
int address = profile.keyButtons[i].address;
if (address >= buttonUnits)
{
continue; // keypads have no lamps
}
Logical duplicate = False;
for (int j = 0; j < light_count; ++j)
{
if (light_keys[j] == profile.keyButtons[i].virtualKey)
{
duplicate = True; // first binding wins
break;
}
}
if (duplicate)
{
continue;
}
light_keys[light_count] = profile.keyButtons[i].virtualKey;
light_addresses[light_count] = address;
light_yellow[light_count] =
(address >= 0x10 && address <= 0x1F) ? 1 : 0;
++light_count;
}
if (light_count > 0)
{
KeyLight_SetLogger(&KeyLightLog);
KeyLight_SetMap(light_keys, light_addresses, light_yellow, light_count);
KeyLight_Start();
keyLightActive = True;
}
}
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;
DEBUG_STREAM << "PadRIO: controls "
<< (InputNeedsFocus()
? "answer only while the game window has focus"
: "answer whether the game has focus or not (RP412INPUTFOCUS=0)")
<< "\n" << std::flush;
//
// Only open DirectInput when the profile actually asks for it. A
// player on keyboard and pad should not pay for an enumeration of
// every HID on the machine, and joyconfig.bat is what writes the
// rows that turn this on.
//
if (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 ||
profile.joyHatCount > 0)
{
int found = RPJoyInit();
DEBUG_STREAM << "PadRIO: joystick bindings present, " << found
<< " generic device(s) attached\n" << std::flush;
for (int d = 0; d < found; ++d)
{
const RPJoyDeviceState *state = RPJoyDevice(d);
if (state != NULL)
{
DEBUG_STREAM << "PadRIO: [" << d << "] " << state->name
<< "\n" << std::flush;
}
}
}
}
PadRIO::~PadRIO()
{
Check_Pointer(this);
if (keyLightActive)
{
KeyLight_Stop();
keyLightActive = False;
}
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));
if (keyLightActive)
{
KeyLight_UpdateLamps(lampState, lampCount);
}
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;
}
for (int i = 0; i < profile.joyButtonCount; ++i)
{
profile.joyButtons[i].latched = False;
profile.joyButtons[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;
if (keyLightActive)
{
KeyLight_UpdateLamps(lampState, lampCount);
}
}
}
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;
//---------------------------------------------------------------
// Is the game the window in front? Every source below is gated on
// this - keyboard, pad and stick alike.
//
// Gated rather than skipped, and that is the whole trick: each
// source reads as RELEASED instead of the poll returning early, so
// the diffs further down turn whatever was held at the moment you
// switched away into proper release events. Bail out instead and a
// key held on alt-tab stays down until you come back, which is the
// stuck throttle this is meant to prevent rather than cause.
//
// The throttle accumulator is the deliberate exception. It is the
// pod's one sticky axis and it integrates what the controls ask
// for, so controls asking for nothing simply stop moving it - you
// come back to the speed you left, not to a dead stop.
//---------------------------------------------------------------
Logical input_live =
(!InputNeedsFocus() || ProcessHasFocus()) ? True : False;
//---------------------------------------------------------------
// 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 (input_live && 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 (input_live && 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 = input_live && 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;
}
}
}
//---------------------------------------------------------------
// Generic joysticks. The slots are resolved every poll rather than
// cached, so a stick unplugged mid-race simply stops answering and
// one plugged back in picks up where it left off.
//---------------------------------------------------------------
int joyDevice[BindJoyDeviceSlots];
Logical joyLive = False;
for (int slot = 0; slot < BindJoyDeviceSlots; ++slot)
{
joyDevice[slot] = -1;
}
//
// Unfocused this whole block is skipped, which leaves every joyDevice
// slot at -1 - so the button, hat and axis loops below find no device
// and read released and centred on their own. The stick is opened
// DISCL_BACKGROUND (it has to be, or it stops answering the moment a
// pane takes focus), so not polling it is what makes it go quiet.
//
if (input_live && (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 ||
profile.joyHatCount > 0))
{
RPJoyPoll();
for (int slot = 0; slot < BindJoyDeviceSlots; ++slot)
{
joyDevice[slot] = (profile.joyDeviceMatch[slot][0] != '\0')
? RPJoyFindDevice(profile.joyDeviceMatch[slot])
: ((RPJoyDevice(slot) != NULL) ? slot : -1);
if (joyDevice[slot] >= 0)
{
joyLive = True;
}
}
}
for (int i = 0; i < profile.joyButtonCount; ++i)
{
PadJoyButtonBinding *binding = &profile.joyButtons[i];
const RPJoyDeviceState *state =
(binding->device >= 0 && binding->device < BindJoyDeviceSlots)
? RPJoyDevice(joyDevice[binding->device]) : NULL;
Logical down = (state != NULL) &&
(state->buttons & (1u << binding->button)) != 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 < profile.joyHatCount; ++i)
{
const PadJoyHatBinding *binding = &profile.joyHats[i];
const RPJoyDeviceState *state =
(binding->device >= 0 && binding->device < BindJoyDeviceSlots)
? RPJoyDevice(joyDevice[binding->device]) : NULL;
if (state != NULL &&
JoyHatHeld(state->hat[binding->hat], binding->direction))
{
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 (input_live && 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;
}
}
}
//---------------------------------------------------------------
// Joystick axes. A physical throttle lever is the one source that
// does not add into the pile: it has an absolute position, so its
// full travel IS the channel and it takes ownership rather than
// nudging an accumulator that a spring-centred pad stick has to.
//---------------------------------------------------------------
Logical throttleLever = False;
Scalar throttleLeverValue = (Scalar) 0;
if (joyLive)
{
for (int i = 0; i < profile.joyAxisCount; ++i)
{
const PadJoyAxisBinding *binding = &profile.joyAxes[i];
if (binding->device < 0 || binding->device >= BindJoyDeviceSlots)
{
continue;
}
const RPJoyDeviceState *state = RPJoyDevice(joyDevice[binding->device]);
if (state == NULL)
{
continue;
}
Scalar raw = (Scalar) state->axis[binding->source];
if (binding->invert)
{
raw = -raw;
}
if (binding->axis == BindAxisThrottle && binding->rate == 0.0f)
{
// -1..1 of lever travel onto the 0..1 the pod runs on
throttleLeverValue = (raw + 1.0f) * 0.5f;
throttleLever = True;
continue;
}
Scalar value = binding->lever
? JoyLeverValue(raw, binding->deadzone)
: JoyAxisValue(raw, binding->deadzone);
if (binding->rate > 0.0f)
{
rate[binding->axis] += value * binding->rate;
}
else
{
deflect[binding->axis] += value;
}
}
}
//
// The composite pedal axis becomes the pair the pod actually has.
// One signed source presses one pedal or the other, never both,
// which is what a rudder bar or a twist grip does.
//
Scalar pedals = deflect[BindAxisPedals];
if (pedals > 0.0f)
{
deflect[BindAxisRightPedal] += pedals;
}
else if (pedals < 0.0f)
{
deflect[BindAxisLeftPedal] += -pedals;
}
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 = throttleLever
? Clamp01(throttleLeverValue)
: 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);
}
}