Files
BT411/engine/MUNGA_L4/L4PADBINDINGS.cpp
T
arcattackandClaude Opus 5 150961176c merge fix-ups: the four review findings + a build portability gate the merge exposed
Applied on top of the glass-cockpit-refit merge, all from the pre-merge review:

1. README CONTROLS TABLE REWRITTEN for the new default board.  It still taught
   "W/S throttle, A/D turn" while the defaults moved flight to the numpad and
   handed the letter rows to the MFD banks -- a fresh install would read
   controls that do not work.  The new table teaches the numpad flight block,
   the panel-on-your-keys groups, F7 for control mode (was M), backtick for the
   camera (was V), the coolant valves on 1-3/QWE with the detent warning kept,
   and PgUp/PgDn volume.  UPGRADERS ARE TOLD EXPLICITLY that their preserved
   bindings.txt keeps their old keys, and that deleting it opts into the board.

2. BACKTICK VIEW-TOGGLE GUARDED.  The bindings-row-wins rule (KeyHasBinding)
   covered V and J/K/L but not backtick itself, which polled unconditionally --
   and BACKTICK is a nameable, unbound key, so binding it to a button
   double-dispatched (button + camera).  Both halves of the view poll are now
   guarded.

3. ENVIRON.INI REFUSES ONE-SHOTS.  The loader applied ANY key=value line, and
   BT_JOYCONFIG in the file would defeat the #66 one-shot in a sneaky way: the
   wizard DELETES the process var after running, so in every relaunched child
   there is no real env var left to win over the file -- capture wizard at
   every mission start.  BT_JOYCONFIG is now skipped with the reason in the
   template header, which also warns that test/debug hooks (BT_MP_FORCE_DMG
   and friends) do not belong in a file that silently persists forever.

4. VOLUME DOCUMENTED EVERYWHERE IT WAS MISSING.  CONTROLS.md gains the
   PgUp/PgDn row (the old "- / = (see below)" cell pointed at a note that never
   mentioned volume) and the note that the keys moved so they no longer share a
   key with anything in any layout; the bindings template header reserves
   PgUp/PgDn informally.

5. L4KEYLIGHT BUILD GATE (found by building the merge on this machine).  The
   C++/WinRT Dynamic Lighting TU does not compile against SDK 10.0.19041 --
   its bundled cppwinrt fails inside winrt/base itself (C2039 'wait_for').
   CMake now detects an SDK older than 10.0.22000 and builds a dormant stub
   with the same five-function scalar interface (logs once at Start, identical
   behaviour otherwise) -- extending the feature's own runtime philosophy
   ("no Dynamic Lighting -> log once, stay dormant") to build time.  Dynamic
   Lighting is a Windows 11 22H2 feature, so nothing real is lost on an older
   build machine, and machines with a newer SDK build the real mirror
   unchanged.

VERIFIED on the merged tree (4.11.572):
  * build clean -- 0 compile errors; the 40 /FORCE-tolerated unresolved
    externals are byte-identical to every pre-merge build (20
    CreateStreamedSubsystem + 20 DefaultData; the earlier claim that all 40
    were CSS was shorthand -- corrected here for the record).
  * solo boot: environ.ini template written + loaded, mode resolver announces
    the surround, historical bands L276/R276/T223/B336 confirmed, keylight
    stub logs its dormant line, BT_SHOT capture shows the under-glass button
    treatment and the corrected hat labels, 0 faults.
  * PgUp/PgDn volume PROVEN LIVE via injected keys: 5%->10%->15%->10%, saved
    to volume.cfg.  (Side catch: content\volume.cfg had been sitting at 0.00
    from an old test -- anyone using this tree had a silent game; reset.)
  * full relay cycle on the merged exe: 2 pods staged, launch pair, mission,
    StopMission, round RESET -- all clean.
  * all three console suites still pass; checkctx CLEAN.

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

886 lines
27 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.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()
{
}
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()
{
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;
}