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>
1185 lines
37 KiB
C++
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;
|
|
}
|