Files
BT411/engine/MUNGA_L4/L4PADBINDINGS.cpp
T
CydandClaude Opus 5 5f88a4eeb2 The keyboard becomes the button board (RP412 bindings port) + a controls page
Ported RP412's bindings design and made it the DEFAULT: the letter and number
rows are the MFD button banks laid out WHERE THEY SIT ON THE PANEL, flight
moves to the numpad so the board stays free, F1-F12 are the map's two columns,
and G/B stay unbound as the physical gap between the lower clusters.  Expressed
in BT's OWN grammar (slew/deflect/set, the Turn channel) -- bindings.txt is a
documented compatibility surface and was not touched.

Coverage: 61 of 72 addresses on the keyboard, and the 11 absent ones are
exactly those the pod never wired (0x16/0x17/0x1E/0x1F column gaps, 0x38-0x3E
intercom/door).  All 72 stay clickable on the panel.

It lands on BT's addresses unreasonably well: 1-4 + QWER are the ENTIRE coolant
system (Condensers 1-6, flush, balance), F6/F7 the display and control-mode
cycles, F9-F12 Generators A-D, F4 the crouch button reconstructed earlier
today.

THE DELIBERATE COST.  A bound key is removed from the authentic 1995
typed-hotkey channel so it cannot double-dispatch, and this board binds nearly
everything -- so 5 (Quad page), z (Eng1), t/y/u/i/o (pilot select) and +/-
(target zoom) are given up.  Unbind a key to get its 1995 meaning back.
Documented in the file header, the markdown and the page.

NEW RULE: A BINDINGS ROW WINS OVER A BUILT-IN CONVENIENCE KEY
(PadRIO::KeyHasBinding).  V and J/K/L are board buttons now, and those polls
read GetAsyncKeyState directly -- without this they would have fired BOTH the
button and the built-in.  View toggle lives on BACKTICK alone.

CONTROLS.MAP rewritten to mirror the board so glass/pod/dev still feel
identical (the 2026-07-21 settlement): 90 bindings, 0 parse complaints.

HAT LABELS CORRECTED [T1].  INPUT_PATH_AUDIT flagged 0x41-0x44 and was right.
Settled from the streamed mapping (BT_CTRLMAP_LOG): elem 66 -> subsys 17
attrID 14, i.e. 0x42 is the TORSO subsystem, not a look; 0x44/0x43/0x41 are the
mapper's LookLeft/LookRight/LookBehind (attrID 10/11/12).  The .RES has no
"TORSO CENTER" string -- its names are LookBehind/Down/Forward/Left/Right -- so
the audit's wording was loose but its substance correct.  Swept L4GLASSWIN,
L4PADPANEL and L4VB16.

docs/CONTROLS.html: interactive keyboard (hover a key for its address and
meaning, MFD clusters outlined), pad diagram, panel map, the under-glass
press-target figure, radar placement thumbnails and the environ.ini table --
modelled on RP412's page, rewritten for BT's addresses and hazards.  mkdist
wraps the fragment in a doctype/charset shell and ships it beside CONTROLS.txt.

Verified: bindings.txt regenerates and loads (74 keys, 0 errors); CONTROLS.MAP
90 bindings, 0 errors; 72/72 panel addresses still dispatch; surround /
exploded / dock / pod / dev boot and simulate with zero faults.

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

841 lines
25 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 },
};
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"
"# 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"
"# --- the four mappable fire buttons ------------------------------------\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;
}