Input: PadRIO -- play without the pod (L4CONTROLS=PAD), Workstream A.1
Ported from RP412: RIOBase split out of the serial RIO (L4RIO.h), rioPointer is RIOBase* (L4CTRL.h), PAD token -> new PadRIO() speaking the RIO surface from an XInput pad + keyboard (L4PADRIO/L4PADBINDINGS, vRIO bindings.txt grammar, hot-plug), KeyLight RGB mirror TU (BT412KEYLIGHT, /std:c++17 per-file). BT-side fixes PadRIO forced into the open: - Both keyboard input bridges (mech4.cpp, mechmppr.cpp BT_KEY_BRIDGE) stand down when a RIO device exists -- they overwrote the engine controls push every frame. M/X conveniences stay live. - Mapper attribute chain OFF BY ONE (latent real-pod bug): the DOS chain below MechControlsMapper carried two base attributes, WinTesla carries one, and AttributeIndexSet::Find is positional -- the .CTL stick mapping wrote throttlePosition. Pad slot + binary-locked enum; gotcha ledgered (reconstruction-gotchas #11). Verified: PAD throttle lever ramps + sticks, stick turns with the authentic speed-vs-turn clamp (61.5 -> 22.0 u/s), mech drives; keyboard fallback intact (BT_FORCE_THROTTLE harness). New diags: BT_CTRLMAP_LOG, BT_STICK_LOG. (Phase 2 of docs/BT412-ROADMAP.md) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
#include "mungal4.h"
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4padrio.h"
|
||||
#include "l4keylight.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;
|
||||
}
|
||||
|
||||
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
|
||||
// columns (0x10-0x1F), red = everything else, like the physical
|
||||
// panel. BT412KEYLIGHT=0 opts out.
|
||||
//
|
||||
keyLightActive = False;
|
||||
const char *keylight = getenv("BT412KEYLIGHT");
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user