The playtest report (sound plays, no gauge drop, no cloud) traced to the
Reservoir's InjectCoolant chain being dead in three places:
- The handler was never REGISTERED: the binary's Reservoir handler table
@0x50e680 has one entry {4, "InjectCoolant", @4aee70}; added
Reservoir::GetMessageHandlers + the press/release handler (press starts
the flush gated on coolantLevel@0x12C > 0 -- the old body misread +0x12C
as currentTemperature; release stops it; novice lockout via FUN_004ac9c8).
- Reservoir::InjectCoolant (@4aefa4, 1019 bytes) was an empty stub -- the
drain the coolant gauge reads. Reconstructed in full: work list =
condenser/weapon/heatable chains (+0x7cc/+0x7bc/+0x7ac, roster-walk
emulation with the binary's duplicate-visit weighting; HeatSink-filtered
[T2 guarded]) + the linked master sink; per sink with flowScale != 0 move
squirtMass x flowScale x dt (clamped to the tank / sink capacity) and
credit pendingHeat with the negative carried-heat delta capped at
sinkMass x reservoir startingTemperature -- the set%-biased flush of the
manual (p24), riding the existing heat model.
- Two latent ctor decode bugs surfaced and fixed: the master gate @4af408
(and @4aeb40 HeatWatcher, swept) reads the OWNER MECH's simulationFlags
(*(param_2+0x28)), not the resource's subsystemFlags (the misread left
the CoolantSimulation Performance unregistered); and the capacity scale
FILDs the bank's INTEGER HeatSinkCount ((float10)*(int*)(link+0x1d0)) --
the float reinterpret gave ~1e-44 -> a permanently empty tank.
Capacity = 0.05 x heatSinkCount x streamed CoolantCapacity (BLH: 6.0).
Visual: the binary's mode-1 coolant-effect renderable (FUN_00456a68, built
for classID 0xBC0 on "ReservoirState", part_014.c:5439; tick @part_007.c:
8780) starts psfx 19 = FLUSH.PFX ("Coolant flush", BTDPL.INI) when the
state changes to 1 -- BTSpawnFlushCloud (mech4.cpp) spawns it on the same
alarm edge as an attached emitter at torso height.
Input: new CONTROLS.MAP action "Flush" on 'H' (HELD; press+release both
dispatch, the held-button payload). Diags: BT_FLUSH_LOG, BT_FLUSH_TEST.
Verified live (FOGDAY): [flush] Reservoir level 6 -> 0.13 -> 0 over ~0.6 s
held (= the manual's 3-4 punches to empty), the coolant vertBar source
Reservoir/CoolantMass drains, and the bluish condensation cloud rises from
the mech (scratchpad/flush_cloud.png vs flush_before.png).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1048 lines
30 KiB
C++
1048 lines
30 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};
|
|
|
|
// The launcher's drive-input global (btl4main.cpp) -- read only for the
|
|
// forced-harness override in the stand-down check below.
|
|
struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle;
|
|
int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; };
|
|
extern BTDriveInput gBTDrive;
|
|
|
|
// FILE scope (C++ linkage!): a block-scope extern inside the extern "C"
|
|
// BTInputSuppressKey inherited C linkage -> _BTRIODevicePresent went
|
|
// UNRESOLVED, /FORCE bound the call to garbage (Sensor::DefaultData), and
|
|
// ANY typed key crashed the glass build (found via the backtick report).
|
|
extern int BTRIODevicePresent(void);
|
|
|
|
//=============================================================================
|
|
// Binding model
|
|
//=============================================================================
|
|
|
|
enum BTAxisID
|
|
{
|
|
BTAxisThrottle = 0,
|
|
BTAxisLeftPedal,
|
|
BTAxisRightPedal,
|
|
BTAxisPedals, // signed convenience alias: + = right pedal
|
|
BTAxisJoystickX, // the pod stick X = torso twist (Standard mode)
|
|
BTAxisJoystickY, // the pod stick Y = torso ELEVATION (pitch aim)
|
|
BTAxisCount
|
|
};
|
|
|
|
enum BTActionID
|
|
{
|
|
BTActNone = 0,
|
|
BTActViewToggle,
|
|
BTActLookBehind,
|
|
BTActAllStop,
|
|
BTActModeCycle,
|
|
BTActValve,
|
|
BTActFlush, // Gitea #7: HELD = coolant flush (InjectCoolant)
|
|
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}, {"Flush", BTActFlush}, {"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 R axis JoystickY deflect 1\n"
|
|
"key F axis JoystickY 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 H action Flush\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 LeftStickY axis JoystickY 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*/)
|
|
{
|
|
//
|
|
// STAND-DOWN (glass-cockpit merge, 2026-07-18): when an OPERATIONAL
|
|
// cockpit device owns the input path (serial RIO on the pod, PadRIO on
|
|
// a BT_GLASS build with L4CONTROLS=PAD), the binding engine yields the
|
|
// keyboard/pad completely -- otherwise both systems would read the
|
|
// same keys and double-emit buttons. Mirrors the mech4/mechmppr
|
|
// bridge convention (BTRIODevicePresent). BT_KEY_BRIDGE=1 forces the
|
|
// engine on for debugging, matching the bridges' force-on.
|
|
//
|
|
{
|
|
static const char *s_kbEnv = getenv("BT_KEY_BRIDGE");
|
|
int engine_owns = s_kbEnv ? (*s_kbEnv == '0')
|
|
: BTRIODevicePresent();
|
|
if (engine_owns && !gBTDrive.forced)
|
|
{
|
|
static int announced = 0;
|
|
if (!announced)
|
|
{
|
|
announced = 1;
|
|
DEBUG_STREAM << "[input] binding engine standing down: a "
|
|
"cockpit device owns the input path\n" << std::flush;
|
|
}
|
|
memset(&gBTInput, 0, sizeof(gBTInput));
|
|
return;
|
|
}
|
|
}
|
|
|
|
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 BTActFlush: next.flush = 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];
|
|
// stick Y = torso ELEVATION (pitch aim) -- every 1995 control mode routes
|
|
// it to Torso::SetAnalogElevationAxis (mechmppr InterpretControls)
|
|
float elev = axisDeflect[BTAxisJoystickY];
|
|
if (elev > 1.0f) elev = 1.0f;
|
|
if (elev < -1.0f) elev = -1.0f;
|
|
next.elevTarget = elev;
|
|
next.elevActive = axisActive[BTAxisJoystickY];
|
|
next.elevAbsolute = axisAbsolute[BTAxisJoystickY];
|
|
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)
|
|
{
|
|
//
|
|
// Mirror of the poll's stand-down: with a cockpit device owning the
|
|
// input path the binding engine claims NOTHING -- the authentic 1995
|
|
// keyboard feed flows untouched (a claimed-but-unprocessed key would
|
|
// otherwise go dead).
|
|
//
|
|
{
|
|
static const char *s_kbEnv = getenv("BT_KEY_BRIDGE");
|
|
int engine_owns = s_kbEnv ? (*s_kbEnv == '0')
|
|
: BTRIODevicePresent();
|
|
if (engine_owns && !gBTDrive.forced)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
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];
|
|
}
|