Files
BT412/engine/MUNGA_L4/L4PADBINDINGS.cpp
T
CydandClaude Fable 5 fe97746a51 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>
2026-07-14 08:16:33 -05:00

543 lines
17 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4padbindings.h"
#include <XInput.h>
#include <stdio.h>
//########################################################################
// Name tables: .NET Keys names (what vRIO uses) to virtual keys, pad
// names to XInput masks. Matching is case-insensitive.
//########################################################################
namespace
{
struct NameValue
{
const char *name;
int value;
};
const NameValue kKeyNames[] =
{
// letters and digits are generated in LookupKey
{ "F1", VK_F1 }, { "F2", VK_F2 }, { "F3", VK_F3 }, { "F4", VK_F4 },
{ "F5", VK_F5 }, { "F6", VK_F6 }, { "F7", VK_F7 }, { "F8", VK_F8 },
{ "F9", VK_F9 }, { "F10", VK_F10 }, { "F11", VK_F11 }, { "F12", VK_F12 },
{ "NumPad0", VK_NUMPAD0 }, { "NumPad1", VK_NUMPAD1 },
{ "NumPad2", VK_NUMPAD2 }, { "NumPad3", VK_NUMPAD3 },
{ "NumPad4", VK_NUMPAD4 }, { "NumPad5", VK_NUMPAD5 },
{ "NumPad6", VK_NUMPAD6 }, { "NumPad7", VK_NUMPAD7 },
{ "NumPad8", VK_NUMPAD8 }, { "NumPad9", VK_NUMPAD9 },
{ "Divide", VK_DIVIDE }, { "Multiply", VK_MULTIPLY },
{ "Subtract", VK_SUBTRACT }, { "Add", VK_ADD },
{ "Decimal", VK_DECIMAL },
{ "Up", VK_UP }, { "Down", VK_DOWN },
{ "Left", VK_LEFT }, { "Right", VK_RIGHT },
{ "Space", VK_SPACE }, { "Return", VK_RETURN }, { "Enter", VK_RETURN },
{ "Escape", VK_ESCAPE }, { "Tab", VK_TAB }, { "Back", VK_BACK },
{ "Home", VK_HOME }, { "End", VK_END },
{ "PageUp", VK_PRIOR }, { "PageDown", VK_NEXT },
{ "Prior", VK_PRIOR }, { "Next", VK_NEXT },
{ "Insert", VK_INSERT }, { "Delete", VK_DELETE },
{ "ShiftKey", VK_SHIFT }, { "Shift", VK_SHIFT },
{ "ControlKey", VK_CONTROL }, { "Ctrl", VK_CONTROL }, { "Control", VK_CONTROL },
{ "Menu", VK_MENU }, { "Alt", VK_MENU },
{ "OemMinus", VK_OEM_MINUS }, { "Oemplus", VK_OEM_PLUS },
{ "Oemcomma", VK_OEM_COMMA }, { "OemPeriod", VK_OEM_PERIOD },
{ "OemOpenBrackets", VK_OEM_4 }, { "OemCloseBrackets", VK_OEM_6 },
{ "OemSemicolon", VK_OEM_1 }, { "OemQuotes", VK_OEM_7 },
{ "OemQuestion", VK_OEM_2 }, { "OemPipe", VK_OEM_5 },
{ "Oemtilde", VK_OEM_3 },
};
const NameValue kPadButtonNames[] =
{
{ "A", XINPUT_GAMEPAD_A }, { "B", XINPUT_GAMEPAD_B },
{ "X", XINPUT_GAMEPAD_X }, { "Y", XINPUT_GAMEPAD_Y },
{ "DPadUp", XINPUT_GAMEPAD_DPAD_UP },
{ "DPadDown", XINPUT_GAMEPAD_DPAD_DOWN },
{ "DPadLeft", XINPUT_GAMEPAD_DPAD_LEFT },
{ "DPadRight", XINPUT_GAMEPAD_DPAD_RIGHT },
{ "Start", XINPUT_GAMEPAD_START }, { "Back", XINPUT_GAMEPAD_BACK },
{ "LeftShoulder", XINPUT_GAMEPAD_LEFT_SHOULDER },
{ "RightShoulder", XINPUT_GAMEPAD_RIGHT_SHOULDER },
{ "LeftThumb", XINPUT_GAMEPAD_LEFT_THUMB },
{ "RightThumb", XINPUT_GAMEPAD_RIGHT_THUMB },
};
const NameValue kPadAxisNames[] =
{
{ "LeftStickX", BindPadLeftStickX }, { "LeftStickY", BindPadLeftStickY },
{ "RightStickX", BindPadRightStickX }, { "RightStickY", BindPadRightStickY },
{ "LeftTrigger", BindPadLeftTrigger }, { "RightTrigger", BindPadRightTrigger },
};
const NameValue kRioAxisNames[] =
{
{ "Throttle", BindAxisThrottle },
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
};
Logical NameEquals(const char *a, const char *b)
{
return _stricmp(a, b) == 0;
}
int LookupTable(const NameValue *table, int count, const char *name)
{
for (int i = 0; i < count; ++i)
{
if (NameEquals(table[i].name, name))
{
return table[i].value;
}
}
return -1;
}
int LookupKey(const char *name)
{
// single letter A-Z (VK == uppercase ASCII)
if (name[1] == '\0' &&
((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z')))
{
return toupper(name[0]);
}
// digit row: D0-D9 (VK == '0'-'9')
if ((name[0] == 'D' || name[0] == 'd') &&
name[1] >= '0' && name[1] <= '9' && name[2] == '\0')
{
return name[1];
}
return LookupTable(kKeyNames, sizeof(kKeyNames) / sizeof(kKeyNames[0]), name);
}
Logical ParseAddress(const char *text, int *address)
{
int value = -1;
if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X'))
{
if (sscanf(text + 2, "%x", &value) != 1)
{
return False;
}
}
else if (sscanf(text, "%d", &value) != 1)
{
return False;
}
if ((value >= 0x00 && value <= 0x47) || (value >= 0x50 && value <= 0x6F))
{
*address = value;
return True;
}
return False;
}
Logical ParseNumber(const char *text, Scalar *value)
{
float parsed;
if (sscanf(text, "%f", &parsed) != 1)
{
return False;
}
*value = (Scalar) parsed;
return True;
}
//---------------------------------------------------------------
// Line tokenizer: whitespace separated, '#' starts a comment
//---------------------------------------------------------------
int Tokenize(char *line, char *tokens[], int max_tokens)
{
char *hash = strchr(line, '#');
if (hash != NULL)
{
*hash = '\0';
}
int count = 0;
char *cursor = line;
while (count < max_tokens)
{
while (*cursor == ' ' || *cursor == '\t' || *cursor == '\r' || *cursor == '\n')
{
++cursor;
}
if (*cursor == '\0')
{
break;
}
tokens[count++] = cursor;
while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' &&
*cursor != '\r' && *cursor != '\n')
{
++cursor;
}
if (*cursor != '\0')
{
*cursor++ = '\0';
}
}
return count;
}
//---------------------------------------------------------------
// One line of the profile grammar
//---------------------------------------------------------------
Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile)
{
if (token_count < 4)
{
return False;
}
if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "button"))
{
int key = LookupKey(tokens[1]);
int address;
if (key < 0 || !ParseAddress(tokens[3], &address) ||
profile->keyButtonCount >= PadBindingProfile::maxKeyButtons)
{
return False;
}
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
PadKeyButtonBinding *binding = &profile->keyButtons[profile->keyButtonCount++];
memset(binding, 0, sizeof(*binding));
binding->virtualKey = key;
binding->address = address;
binding->toggle = toggle;
return True;
}
if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "axis"))
{
if (token_count < 6)
{
return False;
}
int key = LookupKey(tokens[1]);
int axis = LookupTable(kRioAxisNames,
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
int mode = NameEquals(tokens[4], "deflect") ? BindKeyDeflect
: NameEquals(tokens[4], "rate") ? BindKeyRate : -1;
Scalar value;
if (key < 0 || axis < 0 || mode < 0 || !ParseNumber(tokens[5], &value) ||
profile->keyAxisCount >= PadBindingProfile::maxKeyAxes)
{
return False;
}
PadKeyAxisBinding *binding = &profile->keyAxes[profile->keyAxisCount++];
binding->virtualKey = key;
binding->axis = axis;
binding->mode = mode;
binding->value = value;
return True;
}
if (NameEquals(tokens[0], "pad") && NameEquals(tokens[2], "button"))
{
int mask = LookupTable(kPadButtonNames,
sizeof(kPadButtonNames) / sizeof(kPadButtonNames[0]), tokens[1]);
int address;
if (mask < 0 || !ParseAddress(tokens[3], &address) ||
profile->padButtonCount >= PadBindingProfile::maxPadButtons)
{
return False;
}
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
PadPadButtonBinding *binding = &profile->padButtons[profile->padButtonCount++];
memset(binding, 0, sizeof(*binding));
binding->padMask = (unsigned short) mask;
binding->address = address;
binding->toggle = toggle;
return True;
}
if (NameEquals(tokens[0], "padaxis") && NameEquals(tokens[2], "axis"))
{
int source = LookupTable(kPadAxisNames,
sizeof(kPadAxisNames) / sizeof(kPadAxisNames[0]), tokens[1]);
int axis = LookupTable(kRioAxisNames,
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
if (source < 0 || axis < 0 ||
profile->padAxisCount >= PadBindingProfile::maxPadAxes)
{
return False;
}
PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++];
memset(binding, 0, sizeof(*binding));
binding->source = source;
binding->axis = axis;
for (int i = 4; i < token_count; ++i)
{
if (NameEquals(tokens[i], "invert"))
{
binding->invert = True;
}
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
{
if (!ParseNumber(tokens[++i], &binding->deadzone))
{
return False;
}
}
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
{
if (!ParseNumber(tokens[++i], &binding->rate))
{
return False;
}
}
else
{
return False;
}
}
return True;
}
return False;
}
//---------------------------------------------------------------
// The default profile: vRIO's board-complete layout, adapted for
// the desktop game - the driving keys (WASD/QE/PgUp/PgDn) stay
// axes, so six bank buttons lose their keyboard keys (they are
// still clickable on the on-screen cockpit, and rebindable here).
//---------------------------------------------------------------
const char kDefaultProfile[] =
"# BT412 input bindings - keyboard and Xbox (XInput) controller.\n"
"#\n"
"# One binding per line, '#' starts a comment, keywords are case-\n"
"# insensitive. Loaded at game start; delete this file to restore\n"
"# the defaults.\n"
"#\n"
"# key <name> button <addr> [toggle]\n"
"# key <name> axis <axis> deflect <n>\n"
"# key <name> axis <axis> rate <n-per-second>\n"
"# pad <button> button <addr> [toggle]\n"
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
"#\n"
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
"# <button> A B X Y DPadUp DPadDown DPadLeft DPadRight Start Back\n"
"# LeftShoulder RightShoulder LeftThumb RightThumb\n"
"# <src> LeftStickX LeftStickY RightStickX RightStickY\n"
"# LeftTrigger RightTrigger\n"
"#\n"
"# 'deflect' holds the axis there while the key is down and springs\n"
"# back on release; 'rate' walks the axis by <n> per second and the\n"
"# position sticks (the throttle). Every lamp button is also clickable\n"
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
"\n"
"# ---- Driving: number pad + modifiers ------------------------------\n"
"# The whole letter board stays free for the MFD banks; driving lives\n"
"# on the numpad with the throttle lever on the modifier keys.\n"
"key NumPad8 axis JoystickY deflect -1 # stick forward\n"
"key NumPad2 axis JoystickY deflect 1 # stick back\n"
"key NumPad4 axis JoystickX deflect 1 # stick left\n"
"key NumPad6 axis JoystickX deflect -1 # stick right\n"
"key NumPad7 axis LeftPedal deflect 1\n"
"key NumPad9 axis RightPedal deflect 1\n"
"key NumPad0 button 0x40 # Main trigger (Space works too)\n"
"key Shift axis Throttle rate 0.75 # throttle up\n"
"key Ctrl axis Throttle rate -0.75 # throttle down\n"
"key Alt button 0x3F # Throttle-head button\n"
"\n"
"# ---- Driving: Xbox controller (signs match the pod convention) ----\n"
"padaxis LeftStickX axis JoystickX invert deadzone 0.24\n"
"padaxis LeftStickY axis JoystickY invert deadzone 0.24\n"
"padaxis RightStickY axis Throttle deadzone 0.24 rate 0.75\n"
"padaxis LeftTrigger axis LeftPedal deadzone 0.12\n"
"padaxis RightTrigger axis RightPedal deadzone 0.12\n"
"\n"
"# ---- Joystick column: hat on the arrows, Main on Space ------------\n"
"key Space button 0x40 # Main trigger\n"
"key Up button 0x42 # Hat Up\n"
"key Down button 0x41 # Hat Back\n"
"key Left button 0x44 # Hat Left\n"
"key Right button 0x43 # Hat Right\n"
"\n"
"# ---- Xbox controller: buttons -------------------------------------\n"
"pad A button 0x40 # Main\n"
"pad B button 0x46 # Middle\n"
"pad X button 0x45 # Pinky\n"
"pad Y button 0x47 # Upper\n"
"pad DPadUp button 0x42 # Hat Up\n"
"pad DPadDown button 0x41 # Hat Back\n"
"pad DPadLeft button 0x44 # Hat Left\n"
"pad DPadRight button 0x43 # Hat Right\n"
"pad LeftShoulder button 0x3D # Panic\n"
"pad RightShoulder button 0x3F # Throttle-head button\n"
"pad Start button 0x37 # config 1\n"
"pad Back button 0x36 # config 2\n"
"\n"
"# ---- Upper MFD bank: the number row is the top MFD row and the\n"
"# ---- QWERTY row is the row under it, left to right across the\n"
"# ---- Left / Middle / Right MFDs.\n"
"key D1 button 0x2F\n"
"key D2 button 0x2E\n"
"key D3 button 0x2D\n"
"key D4 button 0x2C\n"
"key D5 button 0x27\n"
"key D6 button 0x26\n"
"key D7 button 0x25\n"
"key D8 button 0x24\n"
"key D9 button 0x37\n"
"key D0 button 0x36\n"
"key OemMinus button 0x35\n"
"key Oemplus button 0x34\n"
"key Q button 0x2B\n"
"key W button 0x2A\n"
"key E button 0x29\n"
"key R button 0x28\n"
"key T button 0x23\n"
"key Y button 0x22\n"
"key U button 0x21\n"
"key I button 0x20\n"
"key O button 0x33\n"
"key P button 0x32\n"
"key OemOpenBrackets button 0x31\n"
"key OemCloseBrackets button 0x30\n"
"\n"
"# ---- Lower MFD bank: home row and the row below it, two 4-key\n"
"# ---- blocks split by an unbound gap key (G / B), mirroring the\n"
"# ---- keypad gap between the Lower Left and Lower Right MFDs.\n"
"key A button 0x0F\n"
"key S button 0x0E\n"
"key D button 0x0D\n"
"key F button 0x0C\n"
"key H button 0x07\n"
"key J button 0x06\n"
"key K button 0x05\n"
"key L button 0x04\n"
"key Z button 0x0B\n"
"key X button 0x0A\n"
"key C button 0x09\n"
"key V button 0x08\n"
"key N button 0x03\n"
"key M button 0x02\n"
"key Oemcomma button 0x01\n"
"key OemPeriod button 0x00\n"
"\n"
"# ---- Secondary / Screen columns on the function keys, top to\n"
"# ---- bottom (0x16/0x17 and 0x1E/0x1F intentionally unmapped).\n"
"key F1 button 0x10\n"
"key F2 button 0x11\n"
"key F3 button 0x12\n"
"key F4 button 0x13\n"
"key F5 button 0x14\n"
"key F6 button 0x15\n"
"key F7 button 0x18\n"
"key F8 button 0x19\n"
"key F9 button 0x1A\n"
"key F10 button 0x1B\n"
"key F11 button 0x1C\n"
"key F12 button 0x1D\n"
"\n"
"# The pilot keypad (0x50-0x5F) and external keypad (0x60-0x6F) are\n"
"# unbound - the game never reads them - but remain bindable here\n"
"# (they arrive as arcade RIO key events).\n";
const char kBindingsFileName[] = "bindings.txt";
}
//########################################################################
// Loader
//########################################################################
void
PadBindings_Load(PadBindingProfile *profile)
{
memset(profile, 0, sizeof(*profile));
//
// First run: write the default profile so the player has a
// self-documenting file to edit
//
FILE *file = fopen(kBindingsFileName, "rb");
if (file == NULL)
{
FILE *out = fopen(kBindingsFileName, "wb");
if (out != NULL)
{
fwrite(kDefaultProfile, 1, strlen(kDefaultProfile), out);
fclose(out);
DEBUG_STREAM << "PadBindings: wrote default " << kBindingsFileName
<< "\n" << std::flush;
}
file = fopen(kBindingsFileName, "rb");
}
//
// Parse: file if we have one, the built-in default if not
//
char *text = NULL;
long size = 0;
if (file != NULL)
{
fseek(file, 0, SEEK_END);
size = ftell(file);
fseek(file, 0, SEEK_SET);
text = new char[size + 1];
fread(text, 1, size, file);
text[size] = '\0';
fclose(file);
}
const char *source = (text != NULL) ? text : kDefaultProfile;
char line[256];
int line_number = 0;
int error_count = 0;
const char *cursor = source;
while (*cursor != '\0')
{
int length = 0;
while (cursor[length] != '\0' && cursor[length] != '\n' &&
length < (int) sizeof(line) - 1)
{
line[length] = cursor[length];
++length;
}
line[length] = '\0';
cursor += length;
if (*cursor == '\n')
{
++cursor;
}
++line_number;
char *tokens[12];
int token_count = Tokenize(line, tokens, 12);
if (token_count == 0)
{
continue;
}
if (!ParseLine(tokens, token_count, profile))
{
++error_count;
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
<< line_number << " rejected\n" << std::flush;
}
}
if (text != NULL)
{
delete[] text;
}
DEBUG_STREAM << "PadBindings: " << profile->keyButtonCount << " key buttons, "
<< profile->keyAxisCount << " key axes, "
<< profile->padButtonCount << " pad buttons, "
<< profile->padAxisCount << " pad axes"
<< (error_count ? " (with rejected lines)" : "")
<< "\n" << std::flush;
}