Ported from BT411, which needed the same thing for its glass cockpit. PadRIO reads XInput, which covers Xbox-class pads and nothing else. A flight stick, a HOTAS throttle, a twist grip, rudder pedals or a wheel arrive through DirectInput instead, and until now the game could not see any of them - the only generic-joystick path left was the 1995 single- device DIJoystick behind L4CONTROLS=DIJOYSTICK, which is untouched here. L4JOY is the reader: up to four devices as normalized state blocks, hot- plug re-enumeration on the same ~3 s cadence PadRIO uses to look for a pad, and a device lost mid-race zeroed rather than left holding whatever was pressed when it went. XInput-class devices are excluded by VID/PID against the RawInput paths carrying the "IG_" marker - without that an Xbox pad arrives through both APIs and every button counts twice. bindings.txt gains four rows in the grammar it already had, using its own vocabulary (deadzone/rate) rather than BT411's: joydev <slot> [product-name substring] joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>] joybutton <n> button <addr> [toggle] joyhat <n> <up|down|left|right> button <addr> Slots resolve to a live device every poll, by name substring or ordinal, so unplugging and replugging does not rewrite anyone's file. Two things the pod's shape forced that BT411 solved differently: Pedals - a signed composite axis that decomposes into the pod's two pedals, positive right and negative left. The pod has a pedal each side; a twist grip or rudder bar is one signed control, and pressing one or the other but never both is exactly what it wants to say. It is a channel name like any other, so a pad stick can drive the turn too. A joyaxis on Throttle with no rate is a real lever and OWNS the channel - full travel maps onto the 0..1 the pod runs on, instead of nudging the accumulator that a spring-centred pad stick has to use. RP412JOYCONFIG=1 (joyconfig.bat) runs the capture wizard before the console screen: it asks the player to move each control, and derives the sign convention from the DIRECTION of the move. That is the point of it - a stick that reads positive pushed right and one that reads negative are equally common, and no amount of documentation gets a player to work out which they own. It writes only its own section, between marker lines, so hand-edited keyboard and pad rows survive re-running it. The wizard also prints every axis at rest before it starts. A driver that refuses the +-32767 range we ask for reports its own, and an axis then sits hard over instead of near zero; seeing "X +1.00" on an untouched stick is the difference between a five-minute fix and a bug report that says it configured itself. Each capture reports the move it saw for the same reason. Verified on the Logitech Extreme 3D on this machine. Enumeration finds it and excludes the Xbox pad, which still arrives separately through XInput. Every row shape parses - 7 axes, 2 buttons, 4 hat directions - and three deliberately malformed rows (a bad axis name, button 99, a "sideways" hat) are each rejected by line number rather than silently dropped. The wizard lists the device with its axes at rest reading X +0.00 Y -0.01 RZ -0.04 SL0 +1.00, waits on the first prompt without self-triggering, and with a hand on the stick captures X to steering, Y to pitch, RZ to the pedals and SL0 to the throttle, inverting the ones that read backwards. Running the captures through to a written file needs a hand on the stick, so that part is the machine's to confirm, not this build's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
760 lines
20 KiB
C++
760 lines
20 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;
|
|
}
|
|
|
|
//
|
|
// 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 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;
|
|
|
|
//
|
|
// 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;
|
|
|
|
//---------------------------------------------------------------
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
//---------------------------------------------------------------
|
|
// 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;
|
|
}
|
|
if (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 (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 = 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);
|
|
}
|
|
}
|