Files
BT411/game/reconstructed/btinput.cpp
T
arcattackandClaude Fable 5 980c9cd7e5 Input remap: CONTROLS.MAP binding engine + XInput gamepad support
The pod's controls reach the game through the RIO serial board; on a
desktop that hardware is a keyboard shim of hardcoded GetAsyncKeyState
reads.  New binding engine (game/reconstructed/btinput.cpp) maps PC keys
and an XInput pad onto the authentic channels through a user-editable
file (content/CONTROLS.MAP, community-suggested grammar, corrected):

  key/pad -> button <addr>   real buttonGroup emissions, mirroring the
                             RIO convention exactly (press a+1 w/ mode
                             mask saved, release -a-1 w/ saved mask;
                             L4CTRL.cpp:2470-2520) -- aux/preset banks,
                             hotbox, panic, reverse thrust all reachable
  key/pad -> axis            Throttle rate (lever), pedals/JoystickX
                             deflect (turn/twist) into the existing
                             virtual-controls integrators (feel, gait
                             detent, spring-centering all unchanged)
  keypad pilot|external <n>  keyboardGroup key values ('0'-'F')
  pckey <char>               any authentic 1995 typed hotkey
  action <name>              port dev controls (view, all-stop, ...)

Keys claimed by a binding are SUPPRESSED from the legacy WM_CHAR/KEYUP
feed -- ends the historic double-dispatch ('w' drove AND selected pilot
0; F5's key-up value 0x74 aliased to the 't' hotkey; letter key-ups fed
the developer fake-event dispatcher).  Unbound keys keep their authentic
meaning.  The dual-use 'V' is split: V = view toggle, B = look behind.

Default profile = WASD classic (compiled-in twin; delete the file to
restore).  CONTROLS_NUMPAD.MAP ships the corrected community layout
(keypads are NOT buttons 0x50-0x6F -- that space doesn't exist; no
clickable cockpit exists so everything needs a binding; keyboard fire
buttons added; 0x36/0x37 are hotbox not 'config'; missiles moved off
Ctrl).  XInput loads dynamically (1_4 -> 9_1_0), disconnected-pad
probing rate-limited; pad sticks write the axes as absolute positions
(the spring is physical), triggers/buttons per the file.

Verified live: bindings load (43), W and NumPad8 drive (speedDemand 0 ->
61.5), X all-stop (spd -> 0), aux-bank emission (D1 -> [input] 0x2f
PRESS/release under the numpad profile), suppression both directions
(posted 'r' reaches [keych], posted 'w' swallowed), full-keyspace
WM_CHAR+WM_KEYUP fuzz x3 survived with sim advancing, XInput graceful
with no pad, 2-node relay session un-regressed (full ladder + UDP).
Pad-in-hand testing still needs a physical controller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:09:36 -05:00

979 lines
27 KiB
C++

//===========================================================================//
// File: btinput.cpp //
// Project: BattleTech Brick: BattleTech LBE Application //
// Contents: The desktop input binding engine (CONTROLS.MAP + XInput). //
//---------------------------------------------------------------------------//
// PORT ADDITION (2026-07-18, input-remap task): the pod reads its stick / //
// throttle / pedals / lamp buttons / keypads through the RIO serial board //
// (L4RIO -> LBE4ControlsManager). On a desktop none of that hardware //
// exists; this engine maps PC keys and an XInput gamepad onto the same //
// channels through a user-editable text file. See btinput.hpp for the //
// architecture and content/CONTROLS.MAP for the grammar + active profile. //
// //
// AUTHENTIC EMISSION CONVENTIONS (LBE4ControlsManager::ProcessRIOEvent, //
// L4CTRL.cpp:2470-2520 -- mirrored exactly): //
// button press: buttonGroup[a].Update(value = a+1, currentModeMask) //
// button release: buttonGroup[a].Update(value = -a-1, mask saved at press)//
// keypad key: keyboardGroup[unit].ForceUpdate('0'-'9'/'A'-'F', mask) //
// //
// The four pod JOYSTICK fire buttons (0x40 trigger / 0x45 pinky / 0x46 //
// thumb-low / 0x47 thumb-high) do NOT go through buttonGroup here: the //
// bring-up fire dispatcher (mech4.cpp weapon groups) consumes them as //
// levels, and double-driving both channels would double-fire once the //
// streamed ChooseButton mappings are exercised. They publish as //
// gBTInput.fire* levels instead. //
//===========================================================================//
#include <bt.hpp>
#include <btl4app.hpp> // application (the BTL4Application global)
#include <l4ctrl.hpp> // LBE4ControlsManager
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "btinput.hpp"
BTInputState gBTInput = {0};
//=============================================================================
// Binding model
//=============================================================================
enum BTAxisID
{
BTAxisThrottle = 0,
BTAxisLeftPedal,
BTAxisRightPedal,
BTAxisPedals, // signed convenience alias: + = right pedal
BTAxisJoystickX, // the pod stick X = torso twist (Standard mode)
BTAxisJoystickY, // parsed + tracked; no consumer yet
BTAxisCount
};
enum BTActionID
{
BTActNone = 0,
BTActViewToggle,
BTActLookBehind,
BTActAllStop,
BTActModeCycle,
BTActValve,
BTActConfigHold,
BTActGenerator1,
BTActGenerator2,
BTActGenerator3,
BTActGenerator4,
BTActReconnect,
BTActCount
};
enum BTTargetKind
{
BTTargetButton = 0, // RIO buttonGroup address (or fire-level for 0x40..0x47)
BTTargetAxisDeflect, // hold the axis at <scale> while down, spring back
BTTargetAxisRate, // walk the axis at <scale>/second while down
BTTargetKeypad, // keyboardGroup[unit] key value
BTTargetPCKey, // keyboardGroup[KeyboardPC] character (authentic hotkeys)
BTTargetAction // port-side dev control
};
enum BTSourceKind
{
BTSourceKey = 0, // PC key (GetAsyncKeyState VK)
BTSourcePadButton, // XInput digital button (or trigger past 50%)
BTSourcePadAxis // XInput analog axis
};
struct BTBinding
{
int sourceKind;
int sourceCode; // VK / XINPUT button bit index / pad-axis id
int targetKind;
int targetCode; // button addr / axis id / keypad unit / char / action
int keypadValue; // BTTargetKeypad only
float scale; // deflect value or rate-per-second
float deadzone; // pad axes
int invert; // pad axes
int toggle; // buttons: press latches on/off
// runtime
int wasDown; // raw source state (toggle edge detection)
int lastEffective; // last emitted/latched state (edge -> emission)
int latched;
int pressMask; // mode mask captured at press (release uses it)
};
#define BTINPUT_MAX_BINDINGS 192
static BTBinding sBindings[BTINPUT_MAX_BINDINGS];
static int sBindingCount = 0;
static int sLoaded = 0;
// characters/VKs claimed by key bindings (legacy keyboard-feed suppression)
static unsigned char sSuppressChar[256]; // WM_CHAR values
static unsigned char sSuppressKeyUp[256]; // WM_KEYUP/-SYSKEYUP values (VK aliases)
static int sInputLog = -1;
#define BTINPUT_LOG(x) \
do { if (sInputLog < 0) sInputLog = getenv("BT_INPUT_LOG") ? 1 : 0; \
if (sInputLog) { DEBUG_STREAM << "[input] " << x << "\n" << std::flush; } } while (0)
//=============================================================================
// Name tables
//=============================================================================
struct BTName { const char *name; int code; };
static const BTName sKeyNames[] =
{
{"Space", ' '}, {"Enter", 0x0D}, {"Tab", 0x09}, {"Escape", 0x1B},
{"Shift", 0x10}, {"Ctrl", 0x11}, {"Alt", 0x12},
{"LShift", 0xA0}, {"RShift", 0xA1}, {"LCtrl", 0xA2}, {"RCtrl", 0xA3},
{"Up", 0x26}, {"Down", 0x28}, {"Left", 0x25}, {"Right", 0x27},
{"PageUp", 0x21}, {"PageDown", 0x22}, {"End", 0x23}, {"Home", 0x24},
{"Insert", 0x2D}, {"Delete", 0x2E}, {"Back", 0x08},
{"NumPad0", 0x60}, {"NumPad1", 0x61}, {"NumPad2", 0x62},
{"NumPad3", 0x63}, {"NumPad4", 0x64}, {"NumPad5", 0x65},
{"NumPad6", 0x66}, {"NumPad7", 0x67}, {"NumPad8", 0x68},
{"NumPad9", 0x69}, {"Multiply", 0x6A}, {"Add", 0x6B},
{"Subtract", 0x6D}, {"Decimal", 0x6E}, {"Divide", 0x6F},
{"OemMinus", 0xBD}, {"Oemplus", 0xBB}, {"Oemcomma", 0xBC},
{"OemPeriod", 0xBE}, {"OemQuestion", 0xBF}, {"OemTilde", 0xC0},
{"OemOpenBrackets", 0xDB}, {"OemPipe", 0xDC}, {"OemCloseBrackets", 0xDD},
{"OemSemicolon", 0xBA}, {"OemQuotes", 0xDE},
{0, 0}
};
// XInput wButtons bit positions (XINPUT_GAMEPAD_* values are single bits)
static const BTName sPadButtonNames[] =
{
{"DPadUp", 0x0001}, {"DPadDown", 0x0002},
{"DPadLeft", 0x0004}, {"DPadRight", 0x0008},
{"Start", 0x0010}, {"Back", 0x0020},
{"LeftThumb", 0x0040}, {"RightThumb", 0x0080},
{"LeftShoulder", 0x0100}, {"RightShoulder", 0x0200},
{"A", 0x1000}, {"B", 0x2000}, {"X", 0x4000}, {"Y", 0x8000},
// analog triggers usable as digital buttons (>= 50% = pressed);
// out-of-band codes above the 16-bit button mask
{"LeftTrigger", 0x10000}, {"RightTrigger", 0x20000},
{0, 0}
};
enum { BTPadLX = 0, BTPadLY, BTPadRX, BTPadRY, BTPadLT, BTPadRT, BTPadAxisCount };
static const BTName sPadAxisNames[] =
{
{"LeftStickX", BTPadLX}, {"LeftStickY", BTPadLY},
{"RightStickX", BTPadRX}, {"RightStickY", BTPadRY},
{"LeftTrigger", BTPadLT}, {"RightTrigger", BTPadRT},
{0, 0}
};
static const BTName sAxisNames[] =
{
{"Throttle", BTAxisThrottle},
{"LeftPedal", BTAxisLeftPedal}, {"RightPedal", BTAxisRightPedal},
{"Pedals", BTAxisPedals},
{"JoystickX", BTAxisJoystickX}, {"JoystickY", BTAxisJoystickY},
{0, 0}
};
static const BTName sActionNames[] =
{
{"ViewToggle", BTActViewToggle}, {"LookBehind", BTActLookBehind},
{"AllStop", BTActAllStop}, {"ModeCycle", BTActModeCycle},
{"Valve", BTActValve}, {"ConfigHold", BTActConfigHold},
{"Generator1", BTActGenerator1}, {"Generator2", BTActGenerator2},
{"Generator3", BTActGenerator3}, {"Generator4", BTActGenerator4},
{"Reconnect", BTActReconnect},
{0, 0}
};
static int
LookupName(const BTName *table, const char *token)
{
for (int i = 0; table[i].name != 0; ++i)
{
if (_stricmp(table[i].name, token) == 0)
{
return table[i].code;
}
}
return -1;
}
static int
ParseKeyName(const char *token)
{
// single letter / digit-row Dn / function keys / named specials
if (token[1] == '\0' && isalpha((unsigned char)token[0]))
{
return toupper((unsigned char)token[0]); // VK == 'A'..'Z'
}
if ((token[0] == 'D' || token[0] == 'd')
&& isdigit((unsigned char)token[1]) && token[2] == '\0')
{
return '0' + (token[1] - '0'); // VK == '0'..'9'
}
if ((token[0] == 'F' || token[0] == 'f') && isdigit((unsigned char)token[1]))
{
int n = atoi(token + 1);
if (n >= 1 && n <= 12)
{
return 0x70 + n - 1; // VK_F1..VK_F12
}
}
return LookupName(sKeyNames, token);
}
//=============================================================================
// The compiled-in default profile: WASD CLASSIC -- byte-identical to
// content/CONTROLS.MAP as shipped. Deleting the file restores exactly this.
//=============================================================================
static const char *sDefaultProfile =
"key W axis Throttle rate 0.7\n"
"key S axis Throttle rate -0.7\n"
"key Up axis Throttle rate 0.7\n"
"key Down axis Throttle rate -0.7\n"
"key A axis LeftPedal deflect 1\n"
"key D axis RightPedal deflect 1\n"
"key Left axis LeftPedal deflect 1\n"
"key Right axis RightPedal deflect 1\n"
"key Q axis JoystickX deflect -1\n"
"key E axis JoystickX deflect 1\n"
"key X action AllStop\n"
"key D1 button 0x40\n"
"key Space button 0x40\n"
"key D2 button 0x46\n"
"key D3 button 0x47\n"
"key Ctrl button 0x47\n"
"key D4 button 0x45\n"
"key G action ConfigHold\n"
"key C action Valve\n"
"key M action ModeCycle\n"
"key V action ViewToggle\n"
"key B action LookBehind\n"
"key F5 action Generator1\n"
"key F6 action Generator2\n"
"key F7 action Generator3\n"
"key F8 action Generator4\n"
"key F9 action Reconnect\n"
"padaxis LeftStickX axis Pedals deadzone 0.24\n"
"padaxis RightStickX axis JoystickX deadzone 0.24\n"
"padaxis RightStickY axis Throttle rate 0.75 deadzone 0.24\n"
"pad RightTrigger button 0x40\n"
"pad LeftTrigger button 0x47\n"
"pad RightShoulder button 0x46\n"
"pad LeftShoulder button 0x45\n"
"pad DPadUp button 0x42\n"
"pad DPadDown button 0x41\n"
"pad DPadLeft button 0x44\n"
"pad DPadRight button 0x43\n"
"pad B action AllStop\n"
"pad Y action ViewToggle\n"
"pad X action LookBehind\n"
"pad Start action ModeCycle\n"
"pad Back action Valve\n";
//=============================================================================
// Parser
//=============================================================================
static void
RegisterKeySuppression(int vk)
{
// WM_KEYUP/-SYSKEYUP deliver the raw VK as the key value (the engine
// keyboard feed, L4CTRL.cpp:1506) -- suppress it; this is what aliased
// F5's key-up (0x74) to the 't' pilot-select hotkey, and letter key-ups
// (uppercase) to developer fake events.
if (vk >= 0 && vk < 256)
{
sSuppressKeyUp[vk] = 1;
}
// WM_CHAR delivers typed characters: suppress both cases of a letter,
// the digit itself, space, and the base punctuation of the Oem keys.
if (vk >= 'A' && vk <= 'Z')
{
sSuppressChar[tolower(vk)] = 1;
sSuppressChar[vk] = 1;
}
else if (vk >= '0' && vk <= '9')
{
sSuppressChar[vk] = 1;
}
else if (vk == ' ')
{
sSuppressChar[' '] = 1;
}
else
{
static const struct { int vk; char ch; } oem[] =
{
{0xBD, '-'}, {0xBB, '='}, {0xBC, ','}, {0xBE, '.'},
{0xBF, '/'}, {0xC0, '`'}, {0xDB, '['}, {0xDC, '\\'},
{0xDD, ']'}, {0xBA, ';'}, {0xDE, '\''}, {0x0D, '\r'},
{0x09, '\t'}, {0x08, '\b'},
};
for (int i = 0; i < (int)(sizeof(oem) / sizeof(oem[0])); ++i)
{
if (oem[i].vk == vk)
{
sSuppressChar[(unsigned char)oem[i].ch] = 1;
}
}
}
}
static int
ParseLine(char *line, int line_number)
{
// strip comments + tokenize
char *hash = strchr(line, '#');
if (hash != 0)
{
*hash = '\0';
}
char *tokens[8];
int token_count = 0;
for (char *t = strtok(line, " \t\r\n");
t != 0 && token_count < 8;
t = strtok(0, " \t\r\n"))
{
tokens[token_count++] = t;
}
if (token_count == 0)
{
return 1; // blank / comment
}
if (token_count < 3 || sBindingCount >= BTINPUT_MAX_BINDINGS)
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": malformed (ignored)");
return 0;
}
BTBinding b;
memset(&b, 0, sizeof(b));
// ---- source ----
int arg = 2; // first token after <verb> <source>
if (_stricmp(tokens[0], "key") == 0)
{
b.sourceKind = BTSourceKey;
b.sourceCode = ParseKeyName(tokens[1]);
}
else if (_stricmp(tokens[0], "pad") == 0)
{
b.sourceKind = BTSourcePadButton;
b.sourceCode = LookupName(sPadButtonNames, tokens[1]);
}
else if (_stricmp(tokens[0], "padaxis") == 0)
{
b.sourceKind = BTSourcePadAxis;
b.sourceCode = LookupName(sPadAxisNames, tokens[1]);
}
else
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown verb '"
<< tokens[0] << "'");
return 0;
}
if (b.sourceCode < 0)
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown source '"
<< tokens[1] << "'");
return 0;
}
// ---- target ----
if (_stricmp(tokens[arg], "button") == 0 && arg + 1 < token_count)
{
b.targetKind = BTTargetButton;
b.targetCode = (int)strtol(tokens[arg + 1], 0, 0);
if (b.targetCode < 0 || b.targetCode > 0x47)
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number
<< ": button address out of range (0x00-0x47)");
return 0;
}
arg += 2;
if (arg < token_count && _stricmp(tokens[arg], "toggle") == 0)
{
b.toggle = 1;
++arg;
}
}
else if (_stricmp(tokens[arg], "axis") == 0 && arg + 1 < token_count)
{
int axis = LookupName(sAxisNames, tokens[arg + 1]);
if (axis < 0)
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown axis '"
<< tokens[arg + 1] << "'");
return 0;
}
b.targetCode = axis;
arg += 2;
b.targetKind = BTTargetAxisDeflect;
b.scale = 1.0f;
b.deadzone = 0.0f;
while (arg < token_count)
{
if (_stricmp(tokens[arg], "deflect") == 0 && arg + 1 < token_count)
{
b.targetKind = BTTargetAxisDeflect;
b.scale = (float)atof(tokens[arg + 1]);
arg += 2;
}
else if (_stricmp(tokens[arg], "rate") == 0 && arg + 1 < token_count)
{
b.targetKind = BTTargetAxisRate;
b.scale = (float)atof(tokens[arg + 1]);
arg += 2;
}
else if (_stricmp(tokens[arg], "deadzone") == 0 && arg + 1 < token_count)
{
b.deadzone = (float)atof(tokens[arg + 1]);
arg += 2;
}
else if (_stricmp(tokens[arg], "invert") == 0)
{
b.invert = 1;
++arg;
}
else
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number
<< ": unknown axis option '" << tokens[arg] << "'");
return 0;
}
}
}
else if (_stricmp(tokens[arg], "keypad") == 0 && arg + 2 < token_count)
{
b.targetKind = BTTargetKeypad;
if (_stricmp(tokens[arg + 1], "pilot") == 0)
{
b.targetCode = LBE4ControlsManager::KeyboardPilot;
}
else if (_stricmp(tokens[arg + 1], "external") == 0)
{
b.targetCode = LBE4ControlsManager::KeyboardExternal;
}
else
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number
<< ": keypad unit must be pilot|external");
return 0;
}
// the RIO converts keypad units 0-15 to '0'-'9','A'-'F'
// (L4CTRL.cpp:2526); accept the same range
int v = (int)strtol(tokens[arg + 2], 0, 0);
if (v < 0 || v > 15)
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number
<< ": keypad value must be 0-15");
return 0;
}
b.keypadValue = (v <= 9) ? ('0' + v) : ('A' + v - 10);
arg += 3;
}
else if (_stricmp(tokens[arg], "pckey") == 0 && arg + 1 < token_count)
{
// send an authentic PC-keyboard hotkey character (the 1995
// KeypressMessageHandler dispatcher: '+'/'-' zoom, presets, ...)
b.targetKind = BTTargetPCKey;
b.targetCode = (tokens[arg + 1][1] == '\0')
? (unsigned char)tokens[arg + 1][0]
: (int)strtol(tokens[arg + 1], 0, 0);
arg += 2;
}
else if (_stricmp(tokens[arg], "action") == 0 && arg + 1 < token_count)
{
b.targetKind = BTTargetAction;
b.targetCode = LookupName(sActionNames, tokens[arg + 1]);
if (b.targetCode < 0)
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown action '"
<< tokens[arg + 1] << "'");
return 0;
}
arg += 2;
}
else
{
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown target '"
<< tokens[arg] << "'");
return 0;
}
if (b.sourceKind == BTSourceKey)
{
RegisterKeySuppression(b.sourceCode);
}
sBindings[sBindingCount++] = b;
return 1;
}
static void
LoadBindings()
{
sLoaded = 1;
sBindingCount = 0;
memset(sSuppressChar, 0, sizeof(sSuppressChar));
memset(sSuppressKeyUp, 0, sizeof(sSuppressKeyUp));
char *text = 0;
FILE *f = fopen("CONTROLS.MAP", "rb"); // cwd == content/
if (f != 0)
{
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
if (len > 0 && len < 256 * 1024)
{
text = (char *)malloc((size_t)len + 1);
if (text != 0)
{
fread(text, 1, (size_t)len, f);
text[len] = '\0';
}
}
fclose(f);
}
const char *source = text != 0 ? text : sDefaultProfile;
char *work = _strdup(source);
int line_number = 0;
// strtok is used inside ParseLine, so split lines manually first
for (char *line = work, *next = 0; line != 0; line = next)
{
next = strchr(line, '\n');
if (next != 0)
{
*next++ = '\0';
}
++line_number;
ParseLine(line, line_number);
}
free(work);
BTINPUT_LOG("loaded " << sBindingCount << " binding(s) from "
<< (text != 0 ? "CONTROLS.MAP" : "built-in WASD-classic defaults"));
if (text != 0)
{
free(text);
}
}
//=============================================================================
// Device polling
//=============================================================================
// -- keyboard (dynamic, same pattern as the old shim: WndProc key messages
// are consumed by the engine keyboard reader, so poll async state) --
typedef short (__stdcall *BTAsyncFn)(int);
typedef void *(__stdcall *BTFgFn)(void);
typedef unsigned long (__stdcall *BTWtpFn)(void *, unsigned long *);
typedef unsigned long (__stdcall *BTPidFn)(void);
static BTAsyncFn sAsync = 0;
static BTFgFn sFg = 0;
static BTWtpFn sWtp = 0;
static BTPidFn sPid = 0;
// -- XInput (dynamic: xinput1_4.dll on Win8+, xinput9_1_0.dll everywhere) --
struct BTXInputGamepad
{
unsigned short wButtons;
unsigned char bLeftTrigger;
unsigned char bRightTrigger;
short sThumbLX;
short sThumbLY;
short sThumbRX;
short sThumbRY;
};
struct BTXInputStateRaw
{
unsigned long dwPacketNumber;
BTXInputGamepad Gamepad;
};
typedef unsigned long (__stdcall *BTXInputGetStateFn)(unsigned long, BTXInputStateRaw *);
static BTXInputGetStateFn sXInputGetState = 0;
static int sXInputTried = 0;
static int sPadConnected = 0;
static unsigned long sPadRecheckAt = 0; // GetTickCount throttle
static void
InitDevices()
{
HMODULE u = GetModuleHandleA("user32.dll");
HMODULE k = GetModuleHandleA("kernel32.dll");
if (u != 0 && k != 0)
{
sAsync = (BTAsyncFn)GetProcAddress(u, "GetAsyncKeyState");
sFg = (BTFgFn)GetProcAddress(u, "GetForegroundWindow");
sWtp = (BTWtpFn)GetProcAddress(u, "GetWindowThreadProcessId");
sPid = (BTPidFn)GetProcAddress(k, "GetCurrentProcessId");
}
if (!sXInputTried)
{
sXInputTried = 1;
static const char *dlls[] =
{ "xinput1_4.dll", "xinput1_3.dll", "xinput9_1_0.dll" };
for (int i = 0; i < 3 && sXInputGetState == 0; ++i)
{
HMODULE x = LoadLibraryA(dlls[i]);
if (x != 0)
{
sXInputGetState =
(BTXInputGetStateFn)GetProcAddress(x, "XInputGetState");
if (sXInputGetState != 0)
{
BTINPUT_LOG("XInput via " << dlls[i]);
}
}
}
}
}
static float
PadAxisValue(const BTXInputGamepad &pad, int axis)
{
switch (axis)
{
case BTPadLX: return pad.sThumbLX / 32767.0f;
case BTPadLY: return pad.sThumbLY / 32767.0f;
case BTPadRX: return pad.sThumbRX / 32767.0f;
case BTPadRY: return pad.sThumbRY / 32767.0f;
case BTPadLT: return pad.bLeftTrigger / 255.0f;
case BTPadRT: return pad.bRightTrigger / 255.0f;
}
return 0.0f;
}
//=============================================================================
// Emission (the authentic RIO event mirror)
//=============================================================================
static void
EmitButton(BTBinding &b, int down)
{
LBE4ControlsManager *controls =
(LBE4ControlsManager *)application->GetControlsManager();
if (controls == 0)
{
return;
}
int addr = b.targetCode;
// the four pod joystick fire buttons feed the bring-up fire channels
// as LEVELS (see file banner); handled in the poll aggregation.
if (addr >= 0x40 && addr <= 0x47 && addr != 0x41 && addr != 0x42
&& addr != 0x43 && addr != 0x44)
{
return;
}
if (down)
{
ControlsButton value = (ControlsButton)(addr + 1);
b.pressMask = application->GetModeManager()->GetModeMask();
controls->buttonGroup[addr].Update(&value, b.pressMask);
BTINPUT_LOG("button 0x" << std::hex << addr << std::dec << " PRESS");
}
else
{
ControlsButton value = (ControlsButton)(-addr - 1);
controls->buttonGroup[addr].Update(&value, b.pressMask);
BTINPUT_LOG("button 0x" << std::hex << addr << std::dec << " release");
}
}
static void
EmitKeypad(BTBinding &b)
{
LBE4ControlsManager *controls =
(LBE4ControlsManager *)application->GetControlsManager();
if (controls == 0)
{
return;
}
ControlsKey key = (ControlsKey)b.keypadValue;
controls->keyboardGroup[b.targetCode].ForceUpdate(
&key, application->GetModeManager()->GetModeMask());
BTINPUT_LOG("keypad unit " << b.targetCode << " key '"
<< (char)b.keypadValue << "'");
}
static void
EmitPCKey(BTBinding &b)
{
LBE4ControlsManager *controls =
(LBE4ControlsManager *)application->GetControlsManager();
if (controls == 0)
{
return;
}
ControlsKey key = (ControlsKey)b.targetCode;
controls->keyboardGroup[LBE4ControlsManager::KeyboardPC].ForceUpdate(
&key, application->GetModeManager()->GetModeMask());
BTINPUT_LOG("pckey 0x" << std::hex << b.targetCode << std::dec);
}
//=============================================================================
// The per-frame poll
//=============================================================================
void
BTInputPoll(float /*dt*/)
{
if (!sLoaded)
{
LoadBindings();
InitDevices();
}
// foreground-focus guard (BT_KEY_NOFOCUS=1 for automation harnesses)
int focused = 0;
if (sFg != 0 && sWtp != 0 && sPid != 0)
{
void *fg = sFg();
unsigned long fg_pid = 0;
if (fg != 0)
{
sWtp(fg, &fg_pid);
}
focused = (fg_pid == sPid());
}
static int sNoFocus = -1;
if (sNoFocus < 0)
{
const char *nf = getenv("BT_KEY_NOFOCUS");
sNoFocus = (nf != 0 && *nf == '1') ? 1 : 0;
}
if (sNoFocus)
{
focused = 1;
}
// pad state (rate-limit reconnect probing: XInputGetState on an EMPTY
// slot is expensive, ~ms -- probe disconnected pads at 1 Hz only)
BTXInputGamepad pad;
memset(&pad, 0, sizeof(pad));
if (sXInputGetState != 0)
{
unsigned long now = GetTickCount();
if (sPadConnected || now >= sPadRecheckAt)
{
BTXInputStateRaw raw;
memset(&raw, 0, sizeof(raw));
if (sXInputGetState(0, &raw) == 0) // ERROR_SUCCESS
{
if (!sPadConnected)
{
BTINPUT_LOG("gamepad CONNECTED");
}
sPadConnected = 1;
pad = raw.Gamepad;
}
else
{
if (sPadConnected)
{
BTINPUT_LOG("gamepad disconnected");
}
sPadConnected = 0;
sPadRecheckAt = now + 1000;
}
}
}
// ---- aggregate the frame ----
BTInputState next;
memset(&next, 0, sizeof(next));
float axisDeflect[BTAxisCount] = {0};
int axisActive[BTAxisCount] = {0};
int axisAbsolute[BTAxisCount] = {0};
float axisRate[BTAxisCount] = {0};
for (int i = 0; i < sBindingCount; ++i)
{
BTBinding &b = sBindings[i];
// -- source value --
int down = 0;
float analog = 0.0f;
if (b.sourceKind == BTSourceKey)
{
down = (focused && sAsync != 0
&& (sAsync(b.sourceCode) & 0x8000)) ? 1 : 0;
analog = (float)down;
}
else if (b.sourceKind == BTSourcePadButton)
{
if (!sPadConnected)
{
down = 0;
}
else if (b.sourceCode == 0x10000)
{
down = pad.bLeftTrigger >= 128;
}
else if (b.sourceCode == 0x20000)
{
down = pad.bRightTrigger >= 128;
}
else
{
down = (pad.wButtons & b.sourceCode) ? 1 : 0;
}
analog = (float)down;
}
else // BTSourcePadAxis
{
analog = sPadConnected ? PadAxisValue(pad, b.sourceCode) : 0.0f;
if (b.invert)
{
analog = -analog;
}
if (analog > -b.deadzone && analog < b.deadzone)
{
analog = 0.0f;
}
else if (b.deadzone > 0.0f && b.deadzone < 1.0f)
{
// rescale so the deadzone edge is 0 (no jump)
float sign = analog < 0.0f ? -1.0f : 1.0f;
analog = sign * ((analog * sign - b.deadzone)
/ (1.0f - b.deadzone));
}
down = analog != 0.0f;
}
// toggle latch: a PRESS edge flips the held state
if (b.toggle && b.sourceKind != BTSourcePadAxis)
{
if (down && !b.wasDown)
{
b.latched = !b.latched;
}
b.wasDown = down;
down = b.latched;
analog = (float)down;
}
// -- apply to target --
switch (b.targetKind)
{
case BTTargetButton:
{
int addr = b.targetCode;
if (down != b.lastEffective)
{
EmitButton(b, down);
}
b.lastEffective = down;
// fire-button levels (0x40/45/46/47) aggregate every frame
if (down)
{
if (addr == 0x40) next.fireTrigger = 1;
if (addr == 0x45) next.firePinky = 1;
if (addr == 0x46) next.fireThumbLow = 1;
if (addr == 0x47) next.fireThumbHigh = 1;
}
break;
}
case BTTargetAxisDeflect:
if (down)
{
axisDeflect[b.targetCode] += analog * b.scale;
axisActive[b.targetCode] = 1;
if (b.sourceKind == BTSourcePadAxis)
{
axisAbsolute[b.targetCode] = 1;
}
}
break;
case BTTargetAxisRate:
if (down)
{
axisRate[b.targetCode] += analog * b.scale;
}
break;
case BTTargetKeypad:
if (down && !b.lastEffective)
{
EmitKeypad(b);
}
b.lastEffective = down;
break;
case BTTargetPCKey:
if (down && !b.lastEffective)
{
EmitPCKey(b);
}
b.lastEffective = down;
break;
case BTTargetAction:
if (down)
{
switch (b.targetCode)
{
case BTActViewToggle: next.viewToggle = 1; break;
case BTActLookBehind: next.lookBehind = 1; break;
case BTActAllStop: next.allStop = 1; break;
case BTActModeCycle: next.modeCycle = 1; break;
case BTActValve: next.valve = 1; break;
case BTActConfigHold: next.configHold = 1; break;
case BTActGenerator1: next.genSel = 4; break;
case BTActGenerator2: next.genSel = 5; break;
case BTActGenerator3: next.genSel = 6; break;
case BTActGenerator4: next.genSel = 7; break;
case BTActReconnect: next.genSel = 8; break;
}
}
break;
}
}
// ---- fold the axis aliases + publish ----
// Pedals (signed alias): + = right pedal (turn right)
float turn = (axisDeflect[BTAxisRightPedal] - axisDeflect[BTAxisLeftPedal])
+ axisDeflect[BTAxisPedals];
int turn_active = axisActive[BTAxisRightPedal] || axisActive[BTAxisLeftPedal]
|| axisActive[BTAxisPedals];
int turn_abs = axisAbsolute[BTAxisPedals]
|| axisAbsolute[BTAxisLeftPedal] || axisAbsolute[BTAxisRightPedal];
if (turn > 1.0f) turn = 1.0f;
if (turn < -1.0f) turn = -1.0f;
float twist = axisDeflect[BTAxisJoystickX];
if (twist > 1.0f) twist = 1.0f;
if (twist < -1.0f) twist = -1.0f;
next.turnTarget = turn;
next.turnActive = turn_active;
next.turnAbsolute = turn_abs;
next.twistTarget = twist;
next.twistActive = axisActive[BTAxisJoystickX];
next.twistAbsolute = axisAbsolute[BTAxisJoystickX];
next.leverRate = axisRate[BTAxisThrottle];
gBTInput = next;
}
//=============================================================================
// Legacy keyboard-feed suppression (called from L4CTRL.cpp:1506 block)
//=============================================================================
extern "C" int
BTInputSuppressKey(unsigned int key_value, int is_char)
{
if (!sLoaded)
{
LoadBindings(); // the feed can run before the first poll
InitDevices();
}
if (key_value > 255)
{
return 0; // ALT_BIT-tagged values etc. -- never claimed
}
return is_char ? sSuppressChar[key_value] : sSuppressKeyUp[key_value];
}