Files
BT411/engine/MUNGA_L4/L4PADBINDINGS.cpp
T
arcattackandClaude Opus 5 8edce41f88 bindings: one-time migration to the board-2 layout -- the two-population fork is gone
Operator decision: "make this version devour their custom settings and re-output
the correct binding file with the updates -- I'd like to eliminate competing
code paths."  Before this, the preserved-bindings.txt convention meant upgraders
stayed frozen on whatever layout their file was written under, forever: two
player populations, two sets of true documentation, and every future default
improvement reaching only fresh installs.

THE MIGRATION (PadBindingProfile::MigrateBindingsFile, run once at load):
  * A file carrying the new "# bindings-board 2" marker is left alone -- the
    check is the first thing that runs, so this is a no-op forever after.
  * Otherwise: every row matching the UNION OF EVERY DEFAULT SET EVER SHIPPED
    (85 canonical rows, harvested from c2ee729..1cda880 and embedded as a
    table; comments stripped, whitespace collapsed, uppercased) is dropped --
    those rows were never a player's choice, they were just the old board.
  * Rows the player actually authored are CARRIED with their original text and
    comments, and WIN over the new defaults: the losing default row is emitted
    commented out as "# (yours wins) ...", keyed by the control ("KEY T",
    "PAD A", "PADAXIS LX") so key rows only displace key rows.
  * The joystick wizard's marker-delimited section is preserved VERBATIM (the
    same markers L4JOY rewrites between), so HOTAS players migrate without
    re-running the wizard.
  * The previous file is saved as bindings.old.txt first.  Restoring it over
    bindings.txt just re-migrates next boot -- deliberate: the old WORLD is not
    restorable, only the player's own rows persist.  That is the point.

VERIFIED end-to-end with the real exe (three planted scenarios):
  A. old-world file (8 old-default rows + 2 authored rows incl. a rate tweak +
     a wizard section): both authored rows carried with comments, both
     conflicting new defaults stood down with "(yours wins)", wizard section
     byte-preserved, backup written, log line states all counts, and the
     migrated file parses with ZERO malformed lines.
  B. already-migrated file: byte-identical md5 after a full boot, no migration
     line, no backup touched.  (First attempt at this check clobbered its own
     evidence by running case C first -- re-run properly.)
  C. no file: fresh defaults now carry the marker as line 1, no migration.
  The operator's real bindings.txt was stashed before testing and restored
  untouched; it will migrate on their own next launch, as intended.

Docs flipped to the new reality: the README's "your keys DO NOT change"
upgrade promise is replaced by the migration story (customs + stick kept, old
stock keys replaced, bindings.old.txt escape hatch); CONTROLS.md gains the same
note; context/glass-cockpit.md records the mechanism, the 85-row table's
provenance, and why restoring the backup re-migrates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:19:13 -05:00

1185 lines
37 KiB
C++

#include "mungal4.h"
#pragma hdrstop
//###########################################################################
// L4PADBINDINGS -- the PadRIO input-binding profile (BT_GLASS only; this TU
// is only in the build when the gate is on -- see CMakeLists.txt).
// Format + semantics: L4PADBINDINGS.h.
//###########################################################################
#include "l4padbindings.h"
#include "l4ctrl.h"
#include <windows.h>
#include <xinput.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static const char *bindingsFileName = "bindings.txt";
//###########################################################################
// Name tables
//###########################################################################
struct NamedValue
{
const char *name;
int value;
};
//
// Win32 virtual-key names accepted in bindings.txt. Single letters and
// digits are accepted directly ("W", "5"); everything else by name.
//
static const NamedValue keyNames[] =
{
{ "SPACE", VK_SPACE }, { "ENTER", VK_RETURN },
{ "TAB", VK_TAB }, { "BACKSPACE", VK_BACK },
{ "LSHIFT", VK_LSHIFT }, { "RSHIFT", VK_RSHIFT },
{ "LCTRL", VK_LCONTROL }, { "RCTRL", VK_RCONTROL },
{ "LALT", VK_LMENU }, { "RALT", VK_RMENU },
{ "UP", VK_UP }, { "DOWN", VK_DOWN },
{ "LEFT", VK_LEFT }, { "RIGHT", VK_RIGHT },
{ "INSERT", VK_INSERT }, { "DELETE", VK_DELETE },
{ "HOME", VK_HOME }, { "END", VK_END },
{ "PAGEUP", VK_PRIOR }, { "PAGEDOWN", VK_NEXT },
{ "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 },
{ "NUMPADPLUS", VK_ADD }, { "NUMPADMINUS", VK_SUBTRACT },
{ "NUMPADSTAR", VK_MULTIPLY }, { "NUMPADSLASH", VK_DIVIDE },
{ "NUMPADDOT", VK_DECIMAL },
{ "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 },
{ "MINUS", VK_OEM_MINUS }, { "EQUALS", VK_OEM_PLUS },
{ "COMMA", VK_OEM_COMMA }, { "PERIOD", VK_OEM_PERIOD },
{ "SEMICOLON", VK_OEM_1 }, { "SLASH", VK_OEM_2 },
{ "BACKTICK", VK_OEM_3 }, { "LBRACKET", VK_OEM_4 },
{ "BACKSLASH", VK_OEM_5 }, { "RBRACKET", VK_OEM_6 },
{ "QUOTE", VK_OEM_7 },
//
// MOUSE BUTTONS (2026-07-26). Win32 hands these out as virtual keys, and
// the whole binding path already stores VKs and reads them with
// GetAsyncKeyState -- so they need no verb, no parser change and no new
// machinery, just a name. `key MOUSE4 button 0x40` works exactly like any
// other key row, axes included.
//
// ⚠ MOUSELEFT/MOUSERIGHT are how the player presses cockpit buttons (left
// = press, right = latch). The poll is focus-guarded but not click-aware,
// so binding either ALSO fires on every panel click. They are named here
// for completeness; the middle and side buttons are the free ones.
//
{ "MOUSELEFT", VK_LBUTTON }, { "MOUSERIGHT", VK_RBUTTON },
{ "MOUSEMIDDLE", VK_MBUTTON },
{ "MOUSE4", VK_XBUTTON1 }, { "MOUSE5", VK_XBUTTON2 },
{ "MOUSEX1", VK_XBUTTON1 }, { "MOUSEX2", VK_XBUTTON2 },
};
//
// A mouse button rather than a keyboard key? (Used where a binding only
// makes sense for a real key -- the RGB lamp mirror, which paints keycaps.)
//
static int
IsMouseVirtualKey(int virtualKey)
{
return (virtualKey == VK_LBUTTON || virtualKey == VK_RBUTTON ||
virtualKey == VK_MBUTTON || virtualKey == VK_XBUTTON1 ||
virtualKey == VK_XBUTTON2) ? 1 : 0;
}
int
BTPadBindingIsMouseKey(int virtualKey)
{
return IsMouseVirtualKey(virtualKey);
}
static const NamedValue padButtonNames[] =
{
{ "A", XINPUT_GAMEPAD_A },
{ "B", XINPUT_GAMEPAD_B },
{ "X", XINPUT_GAMEPAD_X },
{ "Y", XINPUT_GAMEPAD_Y },
{ "LB", XINPUT_GAMEPAD_LEFT_SHOULDER },
{ "RB", XINPUT_GAMEPAD_RIGHT_SHOULDER },
{ "BACK", XINPUT_GAMEPAD_BACK },
{ "START", XINPUT_GAMEPAD_START },
{ "LS", XINPUT_GAMEPAD_LEFT_THUMB },
{ "RS", XINPUT_GAMEPAD_RIGHT_THUMB },
{ "DPAD_UP", XINPUT_GAMEPAD_DPAD_UP },
{ "DPAD_DOWN", XINPUT_GAMEPAD_DPAD_DOWN },
{ "DPAD_LEFT", XINPUT_GAMEPAD_DPAD_LEFT },
{ "DPAD_RIGHT", XINPUT_GAMEPAD_DPAD_RIGHT },
};
static const NamedValue padAxisNames[] =
{
{ "LX", PadBindingProfile::PadAxisLX },
{ "LY", PadBindingProfile::PadAxisLY },
{ "RX", PadBindingProfile::PadAxisRX },
{ "RY", PadBindingProfile::PadAxisRY },
{ "LT", PadBindingProfile::PadAxisLT },
{ "RT", PadBindingProfile::PadAxisRT },
};
static const NamedValue channelNames[] =
{
{ "Throttle", PadBindingProfile::ChannelThrottle },
{ "JoystickX", PadBindingProfile::ChannelJoystickX },
{ "JoystickY", PadBindingProfile::ChannelJoystickY },
{ "LeftPedal", PadBindingProfile::ChannelLeftPedal },
{ "RightPedal", PadBindingProfile::ChannelRightPedal },
{ "Turn", PadBindingProfile::ChannelTurn }, // issue #25 composite
};
//
// Generic-joystick axis names (the DIJOYSTATE2 layout -- L4JOY.h order).
//
static const NamedValue joyAxisNames[] =
{
{ "X", PadBindingProfile::JoyAxisX },
{ "Y", PadBindingProfile::JoyAxisY },
{ "Z", PadBindingProfile::JoyAxisZ },
{ "RX", PadBindingProfile::JoyAxisRX },
{ "RY", PadBindingProfile::JoyAxisRY },
{ "RZ", PadBindingProfile::JoyAxisRZ },
{ "SL0", PadBindingProfile::JoyAxisSL0 },
{ "SL1", PadBindingProfile::JoyAxisSL1 },
};
static const NamedValue hatDirectionNames[] =
{
{ "up", 0 }, { "right", 1 }, { "down", 2 }, { "left", 3 },
};
static int
LookupName(const NamedValue *table, int count, const char *name)
{
for (int i = 0; i < count; ++i)
{
if (_stricmp(table[i].name, name) == 0)
{
return table[i].value;
}
}
return -1;
}
static int
LookupKeyName(const char *name)
{
//
// Single letter / digit binds itself ('A'..'Z','0'..'9' are the VK
// codes for the un-shifted keys).
//
if (strlen(name) == 1)
{
char c = (char)toupper((unsigned char)name[0]);
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
{
return (int)c;
}
}
return LookupName(keyNames, sizeof(keyNames)/sizeof(keyNames[0]), name);
}
//###########################################################################
// The default profile.
//
// RIO addresses (L4CTRL.h button enum): 0x40 joystick trigger, 0x3F the
// throttle-handle reverse button, 0x41-0x44 the hat (look behind/forward/
// right/left), 0x45-0x47 pinky/thumb-low/thumb-high, 0x10-0x15 + 0x18-0x1D
// the 12 secondary-panel buttons, 0x00-0x0F the lower aux banks, 0x20-0x37
// the upper aux banks. Keypad unit 0 = the pilot's MFD keypad.
//
// THE KEYBOARD IS THE BUTTON BOARD (2026-07-26, ported from RP412).
//
// Every one of the pod's 72 buttons is reachable from the keyboard, laid out
// WHERE IT SITS ON THE PANEL: the number row is the upper MFDs' top row and
// QWERTY the row beneath it, running left to right across Heat / Engineering /
// Comm; the home row and the row below it do the same for the two lower
// weapon MFDs; F1-F12 are the two columns flanking the map. G and B stay
// unbound on purpose -- they are the physical gap between the lower clusters.
// Flight moves to the NUMPAD (which already carried it as an alternative), so
// the letter field is free for the board.
//
// It lands on BT's own addresses better than it has any right to: 1-4 and
// QWER are the Heat display's two rows, which is the entire coolant system
// (Condensers 1-6, the flush and the balance button); F6/F7 are the display
// and control-mode cycles; F9-F12 are Generators A-D.
//
// WHAT THIS COSTS (the deliberate trade). A bound key is REMOVED from the
// authentic 1995 typed-hotkey channel to stop double-dispatch, and this board
// binds nearly the whole field -- so the typed hotkeys go: 5 = MFD1 Quad page,
// z = MFD3 Eng1, t/y/u/i/o = pilot select 3-7, +/- = target zoom. They remain
// reachable on the panel and by editing this file. The built-in convenience
// keys yield the same way: V and J/K/L are board buttons now, so PadRIO's
// view-toggle and preset-cycle polls stand down for them (KeyHasBinding) and
// the view toggle lives on BACKTICK alone.
//
// PREVIOUS DEFAULT (2026-07-20 - 2026-07-26) was the ~20-action CONTROLS.MAP
// mirror: W/S/A/D drive, Q/E twist, R/F elevation, 1-4/Space/Ctrl fire,
// M/N/H/C/G systems. An existing bindings.txt is never overwritten, so a
// player only meets this board by deleting theirs.
//###########################################################################
static const char *defaultProfileText =
"# bindings-board 2 -- KEEP THIS LINE. It marks the keyboard-layout generation;\n"
"# files without it are pre-board layouts and are migrated automatically\n"
"# (your own rows and the joystick wizard section are kept, the obsolete old\n"
"# defaults are replaced, and the previous file is saved as bindings.old.txt).\n"
"#\n"
"# bindings.txt -- PadRIO input bindings (glass cockpit). Auto-written with\n"
"# defaults on first run; edit freely, delete to regenerate. Grammar:\n"
"# key <KEYNAME> button <addr> RIO button (hex 0x.. ok)\n"
"# key <KEYNAME> keypad <unit> <key> MFD keypad unit 0-2, key 0-15\n"
"# key <KEYNAME> axis <channel> deflect <+|-> <rate> spring stick\n"
"# key <KEYNAME> axis <channel> slew <+|-> <rate> throttle lever\n"
"# key <KEYNAME> axis <channel> set <value> jump/detent\n"
"# pad <PADBTN> button <addr>\n"
"# padaxis <PADAXIS> axis <channel> [invert] [slew <rate>]\n"
"# <KEYNAME> may be a MOUSE BUTTON: MOUSEMIDDLE, MOUSE4, MOUSE5 (the two\n"
"# side buttons; MOUSEX1/MOUSEX2 are aliases). MOUSELEFT and MOUSERIGHT\n"
"# also work but are how you press cockpit buttons -- bind them and they\n"
"# fire on every panel click too. Mouse MOVEMENT is not bindable.\n"
"# Channels: Throttle JoystickX JoystickY LeftPedal RightPedal Turn\n"
"# (Turn = signed composite: + drives the right pedal, - the left --\n"
"# made for mapping a gamepad stick to turning)\n"
"# Pad buttons: A B X Y LB RB BACK START LS RS DPAD_* Axes: LX LY RX RY LT RT\n"
"#\n"
"# LAYOUT: THE KEYBOARD IS THE POD'S BUTTON BOARD. The letter and number\n"
"# rows are the MFD button banks laid out where they sit on the panel, and\n"
"# flight lives on the numpad so the board stays free. Every one of the 72\n"
"# pod buttons is also one mouse click away on the cockpit itself\n"
"# (right-click = press-and-hold latch).\n"
"#\n"
"# A key bound here is REMOVED from the authentic 1995 typed-hotkey channel\n"
"# so it cannot double-dispatch -- and this board binds nearly everything,\n"
"# so those hotkeys (5 = MFD1 Quad, z = MFD3 Eng1, t/y/u/i/o = pilot select,\n"
"# +/- = target zoom) are given up by design. Unbind a key here to get its\n"
"# 1995 meaning back. Same rule for the built-ins: BACKTICK = view toggle\n"
"# (V is a board button now), and J/K/L cycle the Mfd1/2/3 preset pages ONLY\n"
"# while unbound.\n"
"\n"
"# --- flight: the numpad ------------------------------------------------\n"
"# 8/2/4/6 stick, 7/9 pedals, 5 all-stop detent, 0 main trigger.\n"
"# SHIFT/CTRL walk the throttle lever and it STICKS where you leave it;\n"
"# ALT is the throttle handle's reverse-thrust button.\n"
"key NUMPAD8 axis JoystickY deflect + 2.5\n"
"key NUMPAD2 axis JoystickY deflect - 2.5\n"
"key NUMPAD4 axis JoystickX deflect - 2.5\n"
"key NUMPAD6 axis JoystickX deflect + 2.5\n"
"key NUMPAD7 axis LeftPedal deflect + 2.5\n"
"key NUMPAD9 axis RightPedal deflect + 2.5\n"
"key NUMPAD5 axis Throttle set 0\n"
"key LSHIFT axis Throttle slew + 0.7\n"
"key RSHIFT axis Throttle slew + 0.7\n"
"key LCTRL axis Throttle slew - 0.7\n"
"key RCTRL axis Throttle slew - 0.7\n"
"key LALT button 0x3F\n"
"key RALT button 0x3F\n"
"\n"
"# PgUp/PgDn are the VOLUME keys (built in; they produce no typed character\n"
"# so they can never collide with a hotkey) -- leave them unbound.\n"
"\n"
"# --- the four mappable fire buttons ------------------------------------\n"
"# The mouse's side buttons are free if you want them on the trigger:\n"
"# key MOUSE4 button 0x40\n"
"# key MOUSE5 button 0x46\n"
"# key MOUSEMIDDLE button 0x41\n"
"key SPACE button 0x40 # main trigger\n"
"key NUMPAD0 button 0x40 # main trigger\n"
"key NUMPAD1 button 0x45 # pinky\n"
"key NUMPAD3 button 0x46 # middle thumb\n"
"key NUMPADDOT button 0x47 # upper thumb\n"
"\n"
"# --- the hat: look around, on the arrows -------------------------------\n"
"key UP button 0x42 # torso CENTER (the shipped .RES name)\n"
"key DOWN button 0x41 # look behind\n"
"key LEFT button 0x44 # look left\n"
"key RIGHT button 0x43 # look right\n"
"\n"
"# --- UPPER MFD banks: number row on top, QWERTY beneath ----------------\n"
"# Heat / coolant (0x28-0x2F) -- 1-4 and QWER ARE the coolant system:\n"
"# 1/2/3 Condenser 1-3 valve, 4 coolant FLUSH (hold), Q/W/E Condenser\n"
"# 4-6 valve, R balance coolant. The valve detents run 1-5-50-CLOSED.\n"
"key 1 button 0x2F\n"
"key 2 button 0x2E\n"
"key 3 button 0x2D\n"
"key 4 button 0x2C\n"
"key Q button 0x2B\n"
"key W button 0x2A\n"
"key E button 0x29\n"
"key R button 0x28\n"
"# Engineering / Mfd2 (0x20-0x27) -- page-gated: Quad page picks an Eng\n"
"# page, Eng page drives generator select A-D / bus mode / coolant.\n"
"key 5 button 0x27\n"
"key 6 button 0x26\n"
"key 7 button 0x25\n"
"key 8 button 0x24\n"
"key T button 0x23\n"
"key Y button 0x22\n"
"key U button 0x21\n"
"key I button 0x20\n"
"# Comm / target hotbox (0x30-0x37) -- pilot select.\n"
"key 9 button 0x37\n"
"key 0 button 0x36\n"
"key MINUS button 0x35\n"
"key EQUALS button 0x34\n"
"key O button 0x33\n"
"key P button 0x32\n"
"key LBRACKET button 0x31\n"
"key RBRACKET button 0x30\n"
"\n"
"# --- LOWER MFD banks: home row on top, the row below beneath -----------\n"
"# G and B are the PANEL GAP between the two lower clusters -- left\n"
"# unbound on purpose, so they keep their 1995 typed meaning.\n"
"# Left Weapons / Mfd1 (0x08-0x0F).\n"
"key A button 0x0F\n"
"key S button 0x0E\n"
"key D button 0x0D\n"
"key F button 0x0C\n"
"key Z button 0x0B\n"
"key X button 0x0A\n"
"key C button 0x09\n"
"key V button 0x08\n"
"# Right Weapons / Mfd3 (0x00-0x07).\n"
"key H button 0x07\n"
"key J button 0x06\n"
"key K button 0x05\n"
"key L button 0x04\n"
"key N button 0x03\n"
"key M button 0x02\n"
"key COMMA button 0x01\n"
"key PERIOD button 0x00\n"
"\n"
"# --- the two columns flanking the map, top to bottom -------------------\n"
"# Left (0x10-0x15): map zoom in/out, thermal IR, CROUCH, searchlight,\n"
"# cycle secondary display. Right (0x18-0x1D): cycle control mode\n"
"# (BAS/MID/ADV), unused, Generator A-D on/off.\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"
"# --- XInput pad ---\n"
"# POD-FAITHFUL (default): stick X = torso twist, triggers = pedals.\n"
"# TWIN-STICK alternative: replace the LX line with the two below --\n"
"# padaxis LX axis Turn\n"
"# padaxis RX axis JoystickX\n"
"padaxis LX axis JoystickX\n"
"padaxis LY axis JoystickY invert\n"
"padaxis LT axis LeftPedal\n"
"padaxis RT axis RightPedal\n"
"padaxis RY axis Throttle slew 0.7\n"
"pad A button 0x40\n"
"pad B button 0x3F\n"
"pad X button 0x47\n"
"pad Y button 0x42\n"
"pad LB button 0x44\n"
"pad RB button 0x43\n"
"pad DPAD_UP button 0x42\n"
"pad DPAD_DOWN button 0x41\n"
"pad DPAD_LEFT button 0x44\n"
"pad DPAD_RIGHT button 0x43\n"
"\n"
"# --- generic joysticks (flight sticks / HOTAS / pedals; DirectInput) ---\n"
"# EASIEST: run the game once with BT_JOYCONFIG=1 (joyconfig.bat) -- an\n"
"# interactive wizard detects your controls and writes this section for you.\n"
"# Manual grammar (rows attach to the last joydev slot; slot 0 if none):\n"
"# joydev <slot 0-3> [product-name substring]\n"
"# joyaxis <X|Y|Z|RX|RY|RZ|SL0|SL1> axis <channel> [invert] [slew <r>] [deadzone <f>]\n"
"# joybutton <0-31> button <addr>\n"
"# joyhat <0-3> <up|down|left|right> button <addr>\n"
"# Typical flight stick (twist = RZ, throttle wheel = SL0):\n"
"# joydev 0\n"
"# joyaxis X axis JoystickX\n"
"# joyaxis Y axis JoystickY invert\n"
"# joyaxis RZ axis Turn\n"
"# joyaxis SL0 axis Throttle invert deadzone 0\n"
"# joybutton 0 button 0x40\n"
"# joybutton 1 button 0x46\n"
"# joyhat 0 up button 0x42\n"
"# joyhat 0 down button 0x41\n"
"# joyhat 0 left button 0x44\n"
"# joyhat 0 right button 0x43\n";
//###########################################################################
// PadBindingProfile
//###########################################################################
PadBindingProfile::PadBindingProfile():
keyBindingCount(0),
padButtonBindingCount(0),
padAxisBindingCount(0),
joyAxisBindingCount(0),
joyButtonBindingCount(0),
joyHatBindingCount(0),
parseJoyDevice(0)
{
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
}
PadBindingProfile::~PadBindingProfile()
{
}
//############################################################################
// ONE-TIME BINDINGS MIGRATION (operator decision 2026-07-26: "eliminate
// competing code paths").
//
// Before this, an upgrader's preserved bindings.txt froze them on whatever
// keyboard layout it was written under -- two player populations, two sets of
// muscle-memory documentation, forever. Now a file without the
// "# bindings-board 2" marker is migrated in place, exactly once:
//
// * every row matching a HISTORICAL DEFAULT (the union of every default set
// ever shipped, canonicalized below) is dropped -- those rows were never a
// player's choice, they were just the old board;
// * every row the player actually authored (anything else) is CARRIED, and
// wins over the new default for that key/element, whose default row is
// emitted commented-out with a note;
// * the joystick wizard's marker-delimited section is preserved verbatim
// (same markers L4JOY.cpp rewrites between);
// * the previous file is saved as bindings.old.txt first.
//
// Restoring bindings.old.txt over bindings.txt just re-runs the migration next
// boot -- deliberate: the old WORLD is not restorable, only the player's own
// rows persist. That is the whole point.
//############################################################################
// Union of every default row ever shipped (c2ee729..1cda880), canonicalized:
// comments stripped, whitespace collapsed, uppercased. A row matching one of
// these is "the old board", not a customization.
static const char *kHistoricalDefaultRows[] =
{
"KEY 0 BUTTON 0X1B", "KEY 1 BUTTON 0X10", "KEY 1 BUTTON 0X40",
"KEY 2 BUTTON 0X11", "KEY 2 BUTTON 0X46", "KEY 3 BUTTON 0X12",
"KEY 3 BUTTON 0X47", "KEY 4 BUTTON 0X13", "KEY 4 BUTTON 0X45",
"KEY 5 BUTTON 0X14", "KEY 6 BUTTON 0X15", "KEY 7 BUTTON 0X18",
"KEY 8 BUTTON 0X19", "KEY 9 BUTTON 0X1A",
"KEY A AXIS LEFTPEDAL DEFLECT + 2.5", "KEY B BUTTON 0X41",
"KEY B BUTTON 0X45", "KEY C BUTTON 0X2F", "KEY C BUTTON 0X46",
"KEY D AXIS RIGHTPEDAL DEFLECT + 2.5",
"KEY DOWN AXIS THROTTLE SLEW - 0.7", "KEY DOWN BUTTON 0X41",
"KEY E AXIS JOYSTICKX DEFLECT + 2.5", "KEY E KEYPAD 0 3",
"KEY EQUALS BUTTON 0X1D", "KEY F AXIS JOYSTICKY DEFLECT - 2.5",
"KEY F5 BUTTON 0X27", "KEY F6 BUTTON 0X26", "KEY F7 BUTTON 0X25",
"KEY F8 BUTTON 0X24", "KEY F9 BUTTON 0X22", "KEY G BUTTON 0X0E",
"KEY H BUTTON 0X2C", "KEY I KEYPAD 0 8", "KEY LALT BUTTON 0X3F",
"KEY LCTRL AXIS THROTTLE SLEW - 0.7", "KEY LCTRL BUTTON 0X47",
"KEY LEFT AXIS LEFTPEDAL DEFLECT + 2.5", "KEY LEFT BUTTON 0X44",
"KEY LSHIFT AXIS THROTTLE SLEW + 0.7", "KEY M BUTTON 0X18",
"KEY MINUS BUTTON 0X1C", "KEY N BUTTON 0X15",
"KEY NUMPAD2 AXIS JOYSTICKY DEFLECT - 2.5",
"KEY NUMPAD4 AXIS JOYSTICKX DEFLECT - 2.5",
"KEY NUMPAD5 AXIS THROTTLE SET 0",
"KEY NUMPAD6 AXIS JOYSTICKX DEFLECT + 2.5",
"KEY NUMPAD7 AXIS LEFTPEDAL DEFLECT + 2.5",
"KEY NUMPAD8 AXIS JOYSTICKY DEFLECT + 2.5",
"KEY NUMPAD9 AXIS RIGHTPEDAL DEFLECT + 2.5",
"KEY O KEYPAD 0 9", "KEY P KEYPAD 0 0",
"KEY Q AXIS JOYSTICKX DEFLECT - 2.5", "KEY Q KEYPAD 0 1",
"KEY R AXIS JOYSTICKY DEFLECT + 2.5", "KEY R KEYPAD 0 4",
"KEY RCTRL BUTTON 0X47", "KEY RIGHT AXIS RIGHTPEDAL DEFLECT + 2.5",
"KEY RIGHT BUTTON 0X43", "KEY S AXIS THROTTLE SLEW - 0.7",
"KEY SPACE BUTTON 0X40", "KEY T KEYPAD 0 5", "KEY U KEYPAD 0 7",
"KEY UP AXIS THROTTLE SLEW + 0.7", "KEY UP BUTTON 0X42",
"KEY V BUTTON 0X47", "KEY W AXIS THROTTLE SLEW + 0.7",
"KEY W KEYPAD 0 2", "KEY X AXIS THROTTLE SET 0", "KEY Y KEYPAD 0 6",
"PAD A BUTTON 0X40", "PAD B BUTTON 0X3F", "PAD DPAD_DOWN BUTTON 0X41",
"PAD DPAD_LEFT BUTTON 0X44", "PAD DPAD_RIGHT BUTTON 0X43",
"PAD DPAD_UP BUTTON 0X42", "PAD LB BUTTON 0X44", "PAD RB BUTTON 0X43",
"PAD X BUTTON 0X47", "PAD Y BUTTON 0X42",
"PADAXIS LT AXIS LEFTPEDAL", "PADAXIS LX AXIS JOYSTICKX",
"PADAXIS LY AXIS JOYSTICKY INVERT", "PADAXIS RT AXIS RIGHTPEDAL",
"PADAXIS RY AXIS THROTTLE SLEW 0.7",
};
// Canonicalize one bindings line for comparison: strip the comment, collapse
// runs of whitespace to single spaces, trim, uppercase. Returns the length
// (0 = the line is blank/comment-only).
static int
CanonicalizeBindingLine(const char *in, char *out, int out_size)
{
int n = 0;
int pending_space = 0;
for (const char *p = in; *p && *p != '#' && *p != '\n' && *p != '\r'; ++p)
{
if (*p == ' ' || *p == '\t')
{
if (n > 0)
pending_space = 1;
continue;
}
if (pending_space && n < out_size - 1)
{
out[n++] = ' ';
pending_space = 0;
}
if (n < out_size - 1)
out[n++] = (char)toupper((unsigned char)*p);
}
out[n] = 0;
return n;
}
static int
IsHistoricalDefaultRow(const char *canonical)
{
for (int i = 0;
i < (int)(sizeof(kHistoricalDefaultRows)
/ sizeof(kHistoricalDefaultRows[0])); ++i)
{
if (strcmp(canonical, kHistoricalDefaultRows[i]) == 0)
return 1;
}
return 0;
}
// The first two tokens of a canonical row ("KEY W", "PAD A", "PADAXIS LX")
// identify the physical control it claims; a carried player row disables the
// new default for the same control.
static void
BindingRowControlOf(const char *canonical, char *out, int out_size)
{
int n = 0, spaces = 0;
for (const char *p = canonical; *p && n < out_size - 1; ++p)
{
if (*p == ' ' && ++spaces >= 2)
break;
out[n++] = *p;
}
out[n] = 0;
}
void
PadBindingProfile::MigrateBindingsFile(const char *path)
{
FILE *f = fopen(path, "rt");
if (f == NULL)
return; // fresh install: no migration
static char old_text[65536];
size_t got = fread(old_text, 1, sizeof(old_text) - 1, f);
old_text[got] = 0;
fclose(f);
if (strstr(old_text, "# bindings-board 2") != NULL)
return; // already current
//
// Save the original first -- the one honest escape hatch.
//
{
FILE *bak = fopen("bindings.old.txt", "wt");
if (bak != NULL)
{
fputs(old_text, bak);
fclose(bak);
}
}
//
// Walk the old file: collect carried player rows (with their original
// text) and the wizard section verbatim; drop old defaults and old
// commentary.
//
static const char *beginMarker =
"# >>> BT_JOYCONFIG generated -- do not edit between markers";
static const char *endMarker = "# <<< BT_JOYCONFIG end";
static char carried[16384]; carried[0] = 0;
static char wizard[32768]; wizard[0] = 0;
static char carried_controls[4096]; carried_controls[0] = 0;
int carried_count = 0, dropped_count = 0;
int in_wizard = 0;
const char *cursor = old_text;
while (*cursor)
{
char line[512];
int n = 0;
while (cursor[n] && cursor[n] != '\n' && n < (int)sizeof(line) - 1)
++n;
memcpy(line, cursor, n);
line[n] = 0;
cursor += n;
if (*cursor == '\n')
++cursor;
if (strstr(line, beginMarker) != NULL)
in_wizard = 1;
if (in_wizard)
{
if (strlen(wizard) + strlen(line) + 2 < sizeof(wizard))
{
strcat(wizard, line);
strcat(wizard, "\n");
}
if (strstr(line, endMarker) != NULL)
in_wizard = 0;
continue;
}
char canonical[256];
if (CanonicalizeBindingLine(line, canonical, sizeof(canonical)) == 0)
continue; // blank / old commentary
if (IsHistoricalDefaultRow(canonical))
{
++dropped_count; // the old board, not a choice
continue;
}
// A row the player authored: carry it, and remember which control it
// claims so the new default for that control stands down.
if (strlen(carried) + strlen(line) + 2 < sizeof(carried))
{
strcat(carried, line);
strcat(carried, "\n");
++carried_count;
}
char control[64];
BindingRowControlOf(canonical, control, sizeof(control));
if (strlen(carried_controls) + strlen(control) + 2
< sizeof(carried_controls))
{
strcat(carried_controls, "|");
strcat(carried_controls, control);
}
}
//
// Re-emit: the new defaults (minus any row whose control the player owns),
// then the carried rows, then the wizard section.
//
FILE *out = fopen(path, "wt");
if (out == NULL)
{
DEBUG_STREAM << "[padrio] bindings migration could not rewrite "
<< path << " -- keeping the old file\n" << std::flush;
return;
}
int disabled_count = 0;
cursor = defaultProfileText;
while (*cursor)
{
char line[512];
int n = 0;
while (cursor[n] && cursor[n] != '\n' && n < (int)sizeof(line) - 1)
++n;
memcpy(line, cursor, n);
line[n] = 0;
cursor += n;
if (*cursor == '\n')
++cursor;
char canonical[256];
int is_row = CanonicalizeBindingLine(line, canonical,
sizeof(canonical)) > 0;
if (is_row && carried_count > 0)
{
char control[64], needle[68];
BindingRowControlOf(canonical, control, sizeof(control));
sprintf(needle, "|%s", control);
const char *hit = strstr(carried_controls, needle);
// exact control match (the next char must be '|' or NUL)
if (hit != NULL)
{
const char *tail = hit + strlen(needle);
if (*tail == 0 || *tail == '|')
{
fprintf(out, "# (yours wins) %s\n", line);
++disabled_count;
continue;
}
}
}
fprintf(out, "%s\n", line);
}
if (carried_count > 0)
{
fputs("\n# --- carried over from your previous bindings.txt "
"(these beat the defaults above) ---\n", out);
fputs(carried, out);
}
if (wizard[0])
{
fputs("\n", out);
fputs(wizard, out);
}
fclose(out);
DEBUG_STREAM << "[padrio] bindings.txt MIGRATED to the board-2 layout: "
<< carried_count << " of your row(s) carried ("
<< disabled_count << " default(s) stood down for them), "
<< dropped_count << " old-default row(s) replaced, wizard section "
<< (wizard[0] ? "preserved" : "absent")
<< "; previous file saved as bindings.old.txt\n" << std::flush;
}
void
PadBindingProfile::WriteDefaultFile(const char *path)
{
FILE *f = fopen(path, "wt");
if (f == NULL)
{
DEBUG_STREAM << "[padrio] cannot write " << path
<< " -- using built-in defaults\n" << std::flush;
return;
}
fputs(defaultProfileText, f);
fclose(f);
DEBUG_STREAM << "[padrio] wrote default " << path << "\n" << std::flush;
}
//
// Parse one binding line into the tables. Returns 0 on success, -1 on a
// malformed line (caller logs). The line is tokenized in place.
//
int
PadBindingProfile::ParseLine(char *line, int line_number)
{
char *tokens[8];
int tokenCount = 0;
char *cursor = line;
while (tokenCount < 8)
{
while (*cursor == ' ' || *cursor == '\t') ++cursor;
if (*cursor == '\0' || *cursor == '#' ||
*cursor == '\r' || *cursor == '\n')
{
break;
}
tokens[tokenCount++] = cursor;
while (*cursor && *cursor != ' ' && *cursor != '\t' &&
*cursor != '\r' && *cursor != '\n')
{
++cursor;
}
if (*cursor) *cursor++ = '\0';
}
if (tokenCount == 0)
{
return 0; // blank / comment
}
if (tokenCount < 3)
{
return -1;
}
//
// Build the action from tokens[2]... (shared by key/pad rows).
//
Action action;
memset(&action, 0, sizeof(action));
int actionAt = 2;
if (_stricmp(tokens[0], "padaxis") == 0)
{
if (tokenCount < 4 || _stricmp(tokens[2], "axis") != 0)
{
return -1;
}
int axis = LookupName(padAxisNames,
sizeof(padAxisNames)/sizeof(padAxisNames[0]), tokens[1]);
int channel = LookupName(channelNames,
sizeof(channelNames)/sizeof(channelNames[0]), tokens[3]);
if (axis < 0 || channel < 0 ||
padAxisBindingCount >= (int)(sizeof(padAxisBindings)/sizeof(padAxisBindings[0])))
{
return -1;
}
PadAxisBinding &b = padAxisBindings[padAxisBindingCount];
b.axis = axis;
b.channel = channel;
b.invert = 0;
b.slew = 0;
b.slewRate = 0.0f;
for (int t = 4; t < tokenCount; ++t)
{
if (_stricmp(tokens[t], "invert") == 0)
{
b.invert = 1;
}
else if (_stricmp(tokens[t], "slew") == 0 && t+1 < tokenCount)
{
b.slew = 1;
b.slewRate = (float)atof(tokens[++t]);
}
else
{
return -1;
}
}
++padAxisBindingCount;
return 0;
}
//
// joydev <slot> [name substring...] -- select the device slot for the
// joy rows that follow; optional product-name match (joined verbatim).
//
if (_stricmp(tokens[0], "joydev") == 0)
{
int slot = (int)strtol(tokens[1], NULL, 0);
if (slot < 0 || slot >= JoyDeviceSlots)
{
return -1;
}
parseJoyDevice = slot;
joyDeviceMatch[slot][0] = '\0';
for (int t = 2; t < tokenCount; ++t)
{
if (t > 2)
{
strncat(joyDeviceMatch[slot], " ",
sizeof(joyDeviceMatch[slot]) - strlen(joyDeviceMatch[slot]) - 1);
}
strncat(joyDeviceMatch[slot], tokens[t],
sizeof(joyDeviceMatch[slot]) - strlen(joyDeviceMatch[slot]) - 1);
}
return 0;
}
//
// joyaxis <AXIS> axis <channel> [invert] [slew <rate>] [deadzone <f>]
//
if (_stricmp(tokens[0], "joyaxis") == 0)
{
if (tokenCount < 4 || _stricmp(tokens[2], "axis") != 0)
{
return -1;
}
int axis = LookupName(joyAxisNames,
sizeof(joyAxisNames)/sizeof(joyAxisNames[0]), tokens[1]);
int channel = LookupName(channelNames,
sizeof(channelNames)/sizeof(channelNames[0]), tokens[3]);
if (axis < 0 || channel < 0 ||
joyAxisBindingCount >= (int)(sizeof(joyAxisBindings)/sizeof(joyAxisBindings[0])))
{
return -1;
}
JoyAxisBinding &b = joyAxisBindings[joyAxisBindingCount];
b.device = parseJoyDevice;
b.axis = axis;
b.channel = channel;
b.invert = 0;
b.slew = 0;
b.slewRate = 0.0f;
b.deadzone = 0.08f; // stick default; levers set deadzone 0
for (int t = 4; t < tokenCount; ++t)
{
if (_stricmp(tokens[t], "invert") == 0)
{
b.invert = 1;
}
else if (_stricmp(tokens[t], "slew") == 0 && t+1 < tokenCount)
{
b.slew = 1;
b.slewRate = (float)atof(tokens[++t]);
}
else if (_stricmp(tokens[t], "deadzone") == 0 && t+1 < tokenCount)
{
b.deadzone = (float)atof(tokens[++t]);
if (b.deadzone < 0.0f) b.deadzone = 0.0f;
if (b.deadzone > 0.9f) b.deadzone = 0.9f;
}
else
{
return -1;
}
}
++joyAxisBindingCount;
return 0;
}
//
// joyhat's action clause starts one token later (joyhat <n> <dir> ...).
//
if (_stricmp(tokens[0], "joyhat") == 0)
{
actionAt = 3;
if (tokenCount < 5)
{
return -1;
}
}
//
// key/pad/joybutton/joyhat rows: parse the action clause.
//
if (_stricmp(tokens[actionAt], "button") == 0)
{
if (tokenCount < actionAt+2)
{
return -1;
}
action.kind = ActionButton;
action.address = (int)strtol(tokens[actionAt+1], NULL, 0);
if (action.address < 0 ||
action.address >= LBE4ControlsManager::ButtonCount)
{
return -1; // out of the buttonGroup range -- would overrun
}
}
else if (_stricmp(tokens[actionAt], "keypad") == 0)
{
if (tokenCount < actionAt+3)
{
return -1;
}
action.kind = ActionKeypad;
action.address = (int)strtol(tokens[actionAt+1], NULL, 0);
action.key = (int)strtol(tokens[actionAt+2], NULL, 0);
if (action.address < 0 ||
action.address >= LBE4ControlsManager::KeyboardCount ||
action.key < 0 || action.key > 15)
{
return -1;
}
}
else if (_stricmp(tokens[actionAt], "axis") == 0)
{
if (tokenCount < actionAt+3)
{
return -1;
}
action.channel = LookupName(channelNames,
sizeof(channelNames)/sizeof(channelNames[0]), tokens[actionAt+1]);
if (action.channel < 0)
{
return -1;
}
const char *mode = tokens[actionAt+2];
if (_stricmp(mode, "set") == 0)
{
if (tokenCount < actionAt+4)
{
return -1;
}
action.kind = ActionAxisSet;
action.rate = (float)atof(tokens[actionAt+3]);
}
else
{
if (tokenCount < actionAt+5)
{
return -1;
}
if (_stricmp(mode, "deflect") == 0)
{
action.kind = ActionAxisDeflect;
}
else if (_stricmp(mode, "slew") == 0)
{
action.kind = ActionAxisSlew;
}
else
{
return -1;
}
float sign;
if (strcmp(tokens[actionAt+3], "+") == 0) sign = 1.0f;
else if (strcmp(tokens[actionAt+3], "-") == 0) sign = -1.0f;
else
{
return -1;
}
action.rate = sign * (float)atof(tokens[actionAt+4]);
}
}
else
{
return -1;
}
if (_stricmp(tokens[0], "key") == 0)
{
int vk = LookupKeyName(tokens[1]);
if (vk < 0 ||
keyBindingCount >= (int)(sizeof(keyBindings)/sizeof(keyBindings[0])))
{
return -1;
}
keyBindings[keyBindingCount].virtualKey = vk;
keyBindings[keyBindingCount].action = action;
++keyBindingCount;
return 0;
}
else if (_stricmp(tokens[0], "pad") == 0)
{
if (action.kind != ActionButton && action.kind != ActionKeypad)
{
return -1; // pad buttons cannot drive axes; use padaxis
}
int mask = LookupName(padButtonNames,
sizeof(padButtonNames)/sizeof(padButtonNames[0]), tokens[1]);
if (mask < 0 ||
padButtonBindingCount >= (int)(sizeof(padButtonBindings)/sizeof(padButtonBindings[0])))
{
return -1;
}
padButtonBindings[padButtonBindingCount].buttonMask = (unsigned)mask;
padButtonBindings[padButtonBindingCount].action = action;
++padButtonBindingCount;
return 0;
}
else if (_stricmp(tokens[0], "joybutton") == 0)
{
if (action.kind != ActionButton && action.kind != ActionKeypad)
{
return -1; // joystick buttons cannot drive axes; use joyaxis
}
int button = (int)strtol(tokens[1], NULL, 0);
if (button < 0 || button >= 32 ||
joyButtonBindingCount >= (int)(sizeof(joyButtonBindings)/sizeof(joyButtonBindings[0])))
{
return -1;
}
joyButtonBindings[joyButtonBindingCount].device = parseJoyDevice;
joyButtonBindings[joyButtonBindingCount].button = button;
joyButtonBindings[joyButtonBindingCount].action = action;
++joyButtonBindingCount;
return 0;
}
else if (_stricmp(tokens[0], "joyhat") == 0)
{
if (action.kind != ActionButton)
{
return -1; // hat directions map to RIO buttons only
}
int hat = (int)strtol(tokens[1], NULL, 0);
int direction = LookupName(hatDirectionNames,
sizeof(hatDirectionNames)/sizeof(hatDirectionNames[0]), tokens[2]);
if (hat < 0 || hat >= 4 || direction < 0 ||
joyHatBindingCount >= (int)(sizeof(joyHatBindings)/sizeof(joyHatBindings[0])))
{
return -1;
}
joyHatBindings[joyHatBindingCount].device = parseJoyDevice;
joyHatBindings[joyHatBindingCount].hat = hat;
joyHatBindings[joyHatBindingCount].direction = direction;
joyHatBindings[joyHatBindingCount].action = action;
++joyHatBindingCount;
return 0;
}
return -1;
}
void
PadBindingProfile::LoadDefaults()
{
keyBindingCount = 0;
padButtonBindingCount = 0;
padAxisBindingCount = 0;
joyAxisBindingCount = 0;
joyButtonBindingCount = 0;
joyHatBindingCount = 0;
parseJoyDevice = 0;
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
//
// Parse the built-in default text line by line (one code path for
// defaults and files -- the defaults ARE a valid file).
//
const char *cursor = defaultProfileText;
char line[256];
int line_number = 0;
while (*cursor)
{
int n = 0;
while (cursor[n] && cursor[n] != '\n' && n < (int)sizeof(line)-1)
{
++n;
}
memcpy(line, cursor, n);
line[n] = '\0';
cursor += n + (cursor[n] == '\n' ? 1 : 0);
++line_number;
if (ParseLine(line, line_number) != 0)
{
DEBUG_STREAM << "[padrio] BUG: default profile line "
<< line_number << " malformed\n" << std::flush;
}
}
}
void
PadBindingProfile::Load()
{
MigrateBindingsFile(bindingsFileName); // one-time board-2 migration (no-op after)
FILE *f = fopen(bindingsFileName, "rt");
if (f == NULL)
{
WriteDefaultFile(bindingsFileName);
f = fopen(bindingsFileName, "rt");
}
if (f == NULL)
{
LoadDefaults();
return;
}
keyBindingCount = 0;
padButtonBindingCount = 0;
padAxisBindingCount = 0;
joyAxisBindingCount = 0;
joyButtonBindingCount = 0;
joyHatBindingCount = 0;
parseJoyDevice = 0;
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
char line[256];
int line_number = 0;
int accepted = 0, rejected = 0;
while (fgets(line, sizeof(line), f))
{
++line_number;
char parse_copy[256];
strcpy(parse_copy, line);
if (ParseLine(parse_copy, line_number) != 0)
{
DEBUG_STREAM << "[padrio] " << bindingsFileName << " line "
<< line_number << " rejected: " << line << std::flush;
++rejected;
}
else
{
++accepted;
}
}
fclose(f);
if (keyBindingCount == 0 && padAxisBindingCount == 0)
{
//
// A file that binds NOTHING is treated as damaged -- fall back so
// the developer is never left with a dead cockpit.
//
DEBUG_STREAM << "[padrio] " << bindingsFileName
<< " contains no usable bindings -- using built-in defaults\n"
<< std::flush;
LoadDefaults();
return;
}
DEBUG_STREAM << "[padrio] bindings loaded: " << keyBindingCount
<< " keys, " << padButtonBindingCount << " pad buttons, "
<< padAxisBindingCount << " pad axes, "
<< joyAxisBindingCount << " joy axes, "
<< joyButtonBindingCount << " joy buttons, "
<< joyHatBindingCount << " joy hats"
<< (rejected ? " (rejected lines logged above)" : "")
<< "\n" << std::flush;
}