#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 #include #include #include #include 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 }, }; 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. //########################################################################### 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 button RIO button (hex 0x.. ok)\n" "# key keypad MFD keypad unit 0-2, key 0-15\n" "# key axis deflect <+|-> spring stick\n" "# key axis slew <+|-> throttle lever\n" "# key axis set jump/detent\n" "# pad button \n" "# padaxis axis [invert] [slew ]\n" "# Channels: Throttle JoystickX JoystickY LeftPedal RightPedal\n" "# Pad buttons: A B X Y LB RB BACK START LS RS DPAD_* Axes: LX LY RX RY LT RT\n" "\n" "# --- flight: numpad spring stick, pedals, throttle lever ---\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 LSHIFT axis Throttle slew + 0.7\n" "key LCTRL axis Throttle slew - 0.7\n" "key NUMPAD5 axis Throttle set 0\n" "\n" "# --- weapons / hat / handle buttons ---\n" "key SPACE button 0x40\n" "key LALT button 0x3F\n" "key V button 0x47\n" "key C button 0x46\n" "key B button 0x45\n" "key UP button 0x42\n" "key DOWN button 0x41\n" "key LEFT button 0x44\n" "key RIGHT button 0x43\n" "\n" "# --- secondary panel (12) on the number row ---\n" "key 1 button 0x10\n" "key 2 button 0x11\n" "key 3 button 0x12\n" "key 4 button 0x13\n" "key 5 button 0x14\n" "key 6 button 0x15\n" "key 7 button 0x18\n" "key 8 button 0x19\n" "key 9 button 0x1A\n" "key 0 button 0x1B\n" "key MINUS button 0x1C\n" "key EQUALS button 0x1D\n" "\n" "# --- aux banks on F5-F8 (upper center 1-4) ---\n" "key F5 button 0x27\n" "key F6 button 0x26\n" "key F7 button 0x25\n" "key F8 button 0x24\n" "\n" "# --- pilot MFD keypad on QWERTY row (unit 0, keys 0-9) ---\n" "key Q keypad 0 1\n" "key W keypad 0 2\n" "key E keypad 0 3\n" "key R keypad 0 4\n" "key T keypad 0 5\n" "key Y keypad 0 6\n" "key U keypad 0 7\n" "key I keypad 0 8\n" "key O keypad 0 9\n" "key P keypad 0 0\n" "\n" "# --- XInput pad ---\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"; //########################################################################### // PadBindingProfile //########################################################################### PadBindingProfile::PadBindingProfile(): keyBindingCount(0), padButtonBindingCount(0), padAxisBindingCount(0) { } 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; } // // key/pad 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; } return -1; } void PadBindingProfile::LoadDefaults() { keyBindingCount = 0; padButtonBindingCount = 0; padAxisBindingCount = 0; // // 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; 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" << (rejected ? " (rejected lines logged above)" : "") << "\n" << std::flush; }