From ac57a474ef30b3e8ea1b8e89d58d5963532e3a7e Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 17 Jul 2026 22:11:16 -0500 Subject: [PATCH] Input: the RIOBase seam + PadRIO -- the glass cockpit gets hands (step 2b) L4RIO.h splits the abstract cockpit-control surface (RIOBase: enums, the five analog Scalars, GetNextEvent/SetLamp, no-op serial ops, NEW IsOperational) out of the serial RIO (RIO : PCSerialPacket, RIOBase -- byte-for-byte behavior kept, ctor assigns as before); LBE4ControlsManager holds a RIOBase* and gains the gated L4CONTROLS=PAD factory arm (BT_GLASS; OFF build logs+ignores the token). NEW gated TUs: L4PADRIO (XInput+keyboard synthesize the surface; 3s hot-plug re-probe; focus-guarded keys; per-poll AnalogEvent heartbeat; lampState[] + static SetScreenButton/GetLampState for the panel) and L4PADBINDINGS (content\bindings.txt profile, self-documenting default written on first run; deflect/slew/set axis model; addresses validated against ButtonCount). Verified live (glass build, L4CONTROLS=PAD): bindings written+parsed 44/10/5, XInput pad detected, 121 streamed mappings install via stock PrimaryRIO path, 2157 frames clean. Pod build (gates OFF) compiles the split with zero behavioral delta. Co-Authored-By: Claude Fable 5 --- CMakeLists.txt | 6 + engine/MUNGA_L4/L4CTRL.cpp | 22 ++ engine/MUNGA_L4/L4CTRL.h | 4 +- engine/MUNGA_L4/L4PADBINDINGS.cpp | 553 ++++++++++++++++++++++++++++++ engine/MUNGA_L4/L4PADBINDINGS.h | 113 ++++++ engine/MUNGA_L4/L4PADRIO.cpp | 503 +++++++++++++++++++++++++++ engine/MUNGA_L4/L4PADRIO.h | 110 ++++++ engine/MUNGA_L4/L4RIO.cpp | 24 ++ engine/MUNGA_L4/L4RIO.h | 131 +++++-- 9 files changed, 1438 insertions(+), 28 deletions(-) create mode 100644 engine/MUNGA_L4/L4PADBINDINGS.cpp create mode 100644 engine/MUNGA_L4/L4PADBINDINGS.h create mode 100644 engine/MUNGA_L4/L4PADRIO.cpp create mode 100644 engine/MUNGA_L4/L4PADRIO.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3155b31..5c857fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -248,6 +248,12 @@ add_library(munga_engine STATIC "engine/MUNGA_L4/bgfload.cpp" "engine/MUNGA_L4/image.cpp" ) +if(BT_GLASS) + target_sources(munga_engine PRIVATE + "engine/MUNGA_L4/L4PADRIO.cpp" + "engine/MUNGA_L4/L4PADBINDINGS.cpp" + ) +endif() target_include_directories(munga_engine BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/engine/shim" "${DXSDK}/Include") diff --git a/engine/MUNGA_L4/L4CTRL.cpp b/engine/MUNGA_L4/L4CTRL.cpp index a788adc..bbc0f90 100644 --- a/engine/MUNGA_L4/L4CTRL.cpp +++ b/engine/MUNGA_L4/L4CTRL.cpp @@ -7,6 +7,9 @@ #include "l4dinput.h" #include "..\munga\appmgr.h" #include "dxutils.h" +#ifdef BT_GLASS +#include "l4padrio.h" +#endif // #define LOCAL_TEST @@ -766,6 +769,25 @@ LBE4ControlsManager::LBE4ControlsManager(): primaryControlType = LBE4ControlsManager::PrimaryRIO; } } + else if (strcmpi(temp, "PAD") == 0) + { +#ifdef BT_GLASS + // + // The hardware-less cockpit device (glass cockpit, step 2b): + // XInput pad + PC keyboard synthesize the RIO control + // surface; the whole stock RIO path above the seam (mapper, + // lamps, streamed .CTL mappings) runs unchanged. + // + rioPointer = new PadRIO(); + Check(rioPointer); + Register_Object(rioPointer); + flags.RIOExists = 1; + primaryControlType = LBE4ControlsManager::PrimaryRIO; +#else + DEBUG_STREAM << "L4CONTROLS=PAD ignored: PadRIO is not in " + "this build (configure with -DBT_GLASS=ON)\n" << std::flush; +#endif + } else if (strcmpi(temp, "KEYBOARD") == 0) { flags.keyboardExists = 1; diff --git a/engine/MUNGA_L4/L4CTRL.h b/engine/MUNGA_L4/L4CTRL.h index 9743d37..a0e1ddf 100644 --- a/engine/MUNGA_L4/L4CTRL.h +++ b/engine/MUNGA_L4/L4CTRL.h @@ -544,7 +544,9 @@ public: //------------------------------------------------------------------- // RIO data //------------------------------------------------------------------- - RIO + // The abstract device surface (glass-cockpit step 2b): serial RIO on + // the pod, PadRIO (BT_GLASS) on a desktop -- same consumption sites. + RIOBase *rioPointer; protected: diff --git a/engine/MUNGA_L4/L4PADBINDINGS.cpp b/engine/MUNGA_L4/L4PADBINDINGS.cpp new file mode 100644 index 0000000..9e32b46 --- /dev/null +++ b/engine/MUNGA_L4/L4PADBINDINGS.cpp @@ -0,0 +1,553 @@ +#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; +} diff --git a/engine/MUNGA_L4/L4PADBINDINGS.h b/engine/MUNGA_L4/L4PADBINDINGS.h new file mode 100644 index 0000000..cf11b5f --- /dev/null +++ b/engine/MUNGA_L4/L4PADBINDINGS.h @@ -0,0 +1,113 @@ +#pragma once + +//########################################################################### +// +// L4PADBINDINGS -- the PadRIO input-binding profile (BT_GLASS only). +// +// Maps PC keyboard keys and XInput pad controls onto the pod's abstract +// control surface: RIO button addresses (buttonGroup elements), MFD keypad +// units, and the five analog channels (Throttle, JoystickX/Y, Left/Right +// Pedal). The profile is read from "bindings.txt" in the working directory +// (content\); a self-documenting default file is written on first run. +// Grammar (one binding per line, '#' comments): +// +// key button +// key keypad +// key axis deflect <+|-> +// key axis slew <+|-> +// key axis set +// pad button +// padaxis axis [invert] [slew ] +// +// deflect = held drives the channel toward +/-1 at /s, auto-centers +// on release (the spring-centered stick model). +// slew = held moves the channel at /s and it STAYS (the throttle +// lever model). +// set = press jumps the channel to (all-stop detent). +// +//########################################################################### + +class PadBindingProfile +{ +public: + enum Channel { + ChannelThrottle = 0, + ChannelJoystickX, + ChannelJoystickY, + ChannelLeftPedal, + ChannelRightPedal, + ChannelCount + }; + + enum ActionKind { + ActionButton, // RIO button address (buttonGroup element) + ActionKeypad, // MFD keypad unit + key + ActionAxisDeflect, + ActionAxisSlew, + ActionAxisSet + }; + + struct Action + { + ActionKind kind; + int address; // ActionButton: RIO address; ActionKeypad: unit + int key; // ActionKeypad: key number (0-9=digits, 10+=A..) + int channel; // ActionAxis*: Channel + float rate; // deflect/slew: units per second (signed); + // set: the target value + }; + + struct KeyBinding + { + int virtualKey; // Win32 VK code + Action action; + }; + + struct PadButtonBinding + { + unsigned buttonMask; // XINPUT_GAMEPAD_* bit + Action action; // ActionButton / ActionKeypad only + }; + + enum PadAxis { + PadAxisLX = 0, PadAxisLY, PadAxisRX, PadAxisRY, + PadAxisLT, PadAxisRT, + PadAxisCount + }; + + struct PadAxisBinding + { + int axis; // PadAxis + int channel; // Channel + int invert; // flip the sign + int slew; // 0 = direct absolute, 1 = value drives d/dt + float slewRate; // full-deflection slew speed (units/s) + }; + + PadBindingProfile(); + ~PadBindingProfile(); + + // + // Load "bindings.txt" from the working directory; if it does not exist, + // write the default profile there first (self-documenting). Always + // leaves a usable profile (falls back to built-in defaults on a + // malformed file, logging each rejected line). + // + void + Load(); + + int keyBindingCount; + KeyBinding keyBindings[192]; + int padButtonBindingCount; + PadButtonBinding padButtonBindings[32]; + int padAxisBindingCount; + PadAxisBinding padAxisBindings[12]; + +protected: + void + LoadDefaults(); + void + WriteDefaultFile(const char *path); + int + ParseLine(char *line, int line_number); +}; diff --git a/engine/MUNGA_L4/L4PADRIO.cpp b/engine/MUNGA_L4/L4PADRIO.cpp new file mode 100644 index 0000000..4458be4 --- /dev/null +++ b/engine/MUNGA_L4/L4PADRIO.cpp @@ -0,0 +1,503 @@ +#include "mungal4.h" +#pragma hdrstop + +//########################################################################### +// L4PADRIO -- the hardware-less cockpit device (BT_GLASS only; this TU is +// only in the build when the gate is on -- see CMakeLists.txt). +// Design + input model: L4PADRIO.h. +//########################################################################### + +#include "l4padrio.h" +#include "l4ctrl.h" + +#include +#include +#include +#include + +#pragma comment(lib, "xinput9_1_0.lib") + +PadRIO *PadRIO::activeInstance = NULL; + +// +// XInput normalization: thumbs to -1..1 past the stock deadzone, triggers +// to 0..1 past the stock threshold. +// +static float + NormalizeThumb(int value, int dead_zone) +{ + float sign = (value < 0) ? -1.0f : 1.0f; + float magnitude = (float)(value < 0 ? -value : value); + if (magnitude <= (float)dead_zone) + { + return 0.0f; + } + if (magnitude > 32767.0f) + { + magnitude = 32767.0f; + } + return sign * (magnitude - dead_zone) / (32767.0f - dead_zone); +} + +static float + NormalizeTrigger(int value) +{ + if (value <= XINPUT_GAMEPAD_TRIGGER_THRESHOLD) + { + return 0.0f; + } + return (float)(value - XINPUT_GAMEPAD_TRIGGER_THRESHOLD) + / (float)(255 - XINPUT_GAMEPAD_TRIGGER_THRESHOLD); +} + +// +// The keyboard is live only while a window of THIS process is foreground +// (the mech4.cpp focus-guard idiom) -- alt-tabbed developers must not +// drive the mech. +// +static int + ProcessHasFocus() +{ + HWND foreground = GetForegroundWindow(); + if (foreground == NULL) + { + return 0; + } + DWORD process_id = 0; + GetWindowThreadProcessId(foreground, &process_id); + return process_id == GetCurrentProcessId(); +} + +//########################################################################### +// Construction +//########################################################################### + +PadRIO::PadRIO(): + RIOBase(), + eventHead(0), + eventTail(0), + lastPollMilliseconds(0), + lastPadProbeMilliseconds(0), + padIndex(-1), + previousPadButtons(0) +{ + memset(previousKeyHeld, 0, sizeof(previousKeyHeld)); + memset(channelValue, 0, sizeof(channelValue)); + memset(lampState, 0, sizeof(lampState)); + + bindings.Load(); + + // + // Per-channel spring return rate = the fastest deflect rate bound to + // the channel (a channel with no deflect bindings never auto-centers). + // + for (int c = 0; c < PadBindingProfile::ChannelCount; ++c) + { + channelReturnRate[c] = 0.0f; + } + for (int k = 0; k < bindings.keyBindingCount; ++k) + { + const PadBindingProfile::Action &action = bindings.keyBindings[k].action; + if (action.kind == PadBindingProfile::ActionAxisDeflect) + { + float rate = action.rate < 0.0f ? -action.rate : action.rate; + if (rate > channelReturnRate[action.channel]) + { + channelReturnRate[action.channel] = rate; + } + } + } + + flipStickAxes = + (getenv("L4PADFLIP") != NULL && *getenv("L4PADFLIP") != '0'); + + // + // Never revision 0.0 -- some diagnostics print it; give the synthetic + // board a recognizable version. + // + MajorRevision = 9; + MinorRevision = 9; + + activeInstance = this; + DEBUG_STREAM << "[padrio] PadRIO up (XInput probe pending; keyboard " + << "live on focus; L4PADFLIP=" << flipStickAxes << ")\n" << std::flush; +} + +PadRIO::~PadRIO() +{ + if (activeInstance == this) + { + activeInstance = NULL; + } +} + +//########################################################################### +// Event queue +//########################################################################### + +void + PadRIO::PushEvent(const RIOEvent &event) +{ + int next = (eventHead + 1) % EventQueueSize; + if (next == eventTail) + { + DEBUG_STREAM << "[padrio] event queue overflow -- event dropped\n" + << std::flush; + return; + } + eventQueue[eventHead] = event; + eventHead = next; +} + +void + PadRIO::EmitButton(int address, int pressed) +{ + RIOEvent event; + event.Type = pressed ? ButtonPressedEvent : ButtonReleasedEvent; + event.Data.Unit = address; + PushEvent(event); +} + +void + PadRIO::EmitKeypad(int unit, int key) +{ + RIOEvent event; + event.Type = KeyEvent; + event.Data.Keyboard.Unit = unit; + event.Data.Keyboard.Key = key; + PushEvent(event); +} + +//########################################################################### +// The poll -- one pass per frame (time-gated so the manager's drain loop +// terminates; an AnalogEvent is emitted every pass to keep the manager's +// five-scalar push running, matching the serial board's analog cadence). +//########################################################################### + +void + PadRIO::Poll() +{ + unsigned long now = timeGetTime(); + float dt = (lastPollMilliseconds == 0) + ? 0.0f + : (float)(now - lastPollMilliseconds) * 0.001f; + if (dt > 0.1f) + { + dt = 0.1f; // resumed from a stall -- don't slam the integrators + } + lastPollMilliseconds = now; + + // + //----------------------------------------------------------------- + // XInput: hot-plug probe every ~3 s, then read the connected pad. + //----------------------------------------------------------------- + // + XINPUT_STATE pad_state; + int pad_connected = 0; + if (padIndex >= 0) + { + if (XInputGetState(padIndex, &pad_state) == ERROR_SUCCESS) + { + pad_connected = 1; + } + else + { + DEBUG_STREAM << "[padrio] XInput pad " << padIndex + << " disconnected\n" << std::flush; + padIndex = -1; + previousPadButtons = 0; + } + } + if (padIndex < 0 && (lastPadProbeMilliseconds == 0 || + now - lastPadProbeMilliseconds >= 3000)) + { + lastPadProbeMilliseconds = now; + for (int slot = 0; slot < 4; ++slot) + { + if (XInputGetState(slot, &pad_state) == ERROR_SUCCESS) + { + padIndex = slot; + pad_connected = 1; + DEBUG_STREAM << "[padrio] XInput pad found in slot " + << slot << "\n" << std::flush; + break; + } + } + } + + // + //----------------------------------------------------------------- + // Keyboard bindings: edges fire button/keypad events; held keys + // accumulate axis motion. All keys read as RELEASED without focus + // so held buttons let go when the developer alt-tabs. + //----------------------------------------------------------------- + // + int focused = ProcessHasFocus(); + float slewDelta[PadBindingProfile::ChannelCount]; + int deflectHeld[PadBindingProfile::ChannelCount]; + memset(slewDelta, 0, sizeof(slewDelta)); + memset(deflectHeld, 0, sizeof(deflectHeld)); + + for (int k = 0; k < bindings.keyBindingCount; ++k) + { + const PadBindingProfile::KeyBinding &binding = bindings.keyBindings[k]; + int held = focused && + (GetAsyncKeyState(binding.virtualKey) & 0x8000) != 0; + int was_held = previousKeyHeld[k]; + previousKeyHeld[k] = (unsigned char)held; + + switch (binding.action.kind) + { + case PadBindingProfile::ActionButton: + if (held != was_held) + { + EmitButton(binding.action.address, held); + } + break; + + case PadBindingProfile::ActionKeypad: + if (held && !was_held) + { + EmitKeypad(binding.action.address, binding.action.key); + } + break; + + case PadBindingProfile::ActionAxisDeflect: + if (held) + { + deflectHeld[binding.action.channel] = 1; + channelValue[binding.action.channel] += + binding.action.rate * dt; + } + break; + + case PadBindingProfile::ActionAxisSlew: + if (held) + { + slewDelta[binding.action.channel] += + binding.action.rate * dt; + } + break; + + case PadBindingProfile::ActionAxisSet: + if (held && !was_held) + { + channelValue[binding.action.channel] = binding.action.rate; + } + break; + } + } + + // + // Spring return: a deflect-managed channel with no deflect key held + // re-centers at its fastest bound rate. + // + for (int c = 0; c < PadBindingProfile::ChannelCount; ++c) + { + channelValue[c] += slewDelta[c]; + if (!deflectHeld[c] && channelReturnRate[c] > 0.0f) + { + float step = channelReturnRate[c] * dt; + if (channelValue[c] > step) + { + channelValue[c] -= step; + } + else if (channelValue[c] < -step) + { + channelValue[c] += step; + } + else + { + channelValue[c] = 0.0f; + } + } + } + + // + //----------------------------------------------------------------- + // Pad: button edges + axis writes (direct absolute past the + // deadzone; slew axes integrate). + //----------------------------------------------------------------- + // + if (pad_connected) + { + unsigned buttons = pad_state.Gamepad.wButtons; + for (int b = 0; b < bindings.padButtonBindingCount; ++b) + { + const PadBindingProfile::PadButtonBinding &binding = + bindings.padButtonBindings[b]; + int held = (buttons & binding.buttonMask) != 0; + int was_held = (previousPadButtons & binding.buttonMask) != 0; + if (held == was_held) + { + continue; + } + if (binding.action.kind == PadBindingProfile::ActionButton) + { + EmitButton(binding.action.address, held); + } + else if (binding.action.kind == PadBindingProfile::ActionKeypad + && held) + { + EmitKeypad(binding.action.address, binding.action.key); + } + } + previousPadButtons = buttons; + + for (int a = 0; a < bindings.padAxisBindingCount; ++a) + { + const PadBindingProfile::PadAxisBinding &binding = + bindings.padAxisBindings[a]; + float raw = 0.0f; + switch (binding.axis) + { + case PadBindingProfile::PadAxisLX: + raw = NormalizeThumb(pad_state.Gamepad.sThumbLX, + XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + break; + case PadBindingProfile::PadAxisLY: + raw = NormalizeThumb(pad_state.Gamepad.sThumbLY, + XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + break; + case PadBindingProfile::PadAxisRX: + raw = NormalizeThumb(pad_state.Gamepad.sThumbRX, + XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE); + break; + case PadBindingProfile::PadAxisRY: + raw = NormalizeThumb(pad_state.Gamepad.sThumbRY, + XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE); + break; + case PadBindingProfile::PadAxisLT: + raw = NormalizeTrigger(pad_state.Gamepad.bLeftTrigger); + break; + case PadBindingProfile::PadAxisRT: + raw = NormalizeTrigger(pad_state.Gamepad.bRightTrigger); + break; + } + if (binding.invert) + { + raw = -raw; + } + if (binding.slew) + { + channelValue[binding.channel] += raw * binding.slewRate * dt; + } + else if (raw != 0.0f) + { + // + // Direct absolute: a deflected pad axis owns the channel; + // centered (inside the deadzone) it leaves the keyboard + // integration alone. + // + channelValue[binding.channel] = raw; + } + } + } + + // + //----------------------------------------------------------------- + // Clamp and publish the control surface. Throttle is the 0..1 + // lever the mapper detents at 1.0; the rest are -1..1. + //----------------------------------------------------------------- + // + for (int c = 0; c < PadBindingProfile::ChannelCount; ++c) + { + float low = (c == PadBindingProfile::ChannelThrottle) ? 0.0f : -1.0f; + if (channelValue[c] < low) channelValue[c] = low; + if (channelValue[c] > 1.0f) channelValue[c] = 1.0f; + } + + float stick_sign = flipStickAxes ? -1.0f : 1.0f; + Throttle = (Scalar)channelValue[PadBindingProfile::ChannelThrottle]; + JoystickX = (Scalar)(stick_sign * + channelValue[PadBindingProfile::ChannelJoystickX]); + JoystickY = (Scalar)(stick_sign * + channelValue[PadBindingProfile::ChannelJoystickY]); + LeftPedal = (Scalar)channelValue[PadBindingProfile::ChannelLeftPedal]; + RightPedal = (Scalar)channelValue[PadBindingProfile::ChannelRightPedal]; + + // + // The analog heartbeat: tells the manager to run the five-scalar + // push this frame (LBE4ControlsManager::Execute gates the push on + // new_RIO_values). + // + RIOEvent analog; + analog.Type = AnalogEvent; + analog.Data.Unit = 0; + PushEvent(analog); +} + +//########################################################################### +// RIOBase surface +//########################################################################### + +Logical + PadRIO::GetNextEvent(RIOEvent *destinationPointer) +{ + Check_Pointer(destinationPointer); + + if (eventTail == eventHead) + { + // + // Queue drained: poll at most once per millisecond tick so the + // manager's per-frame drain loop terminates (the poll always + // enqueues the analog heartbeat). + // + unsigned long now = timeGetTime(); + if (now == lastPollMilliseconds) + { + return False; + } + Poll(); + } + if (eventTail == eventHead) + { + return False; + } + *destinationPointer = eventQueue[eventTail]; + eventTail = (eventTail + 1) % EventQueueSize; + return True; +} + +void + PadRIO::SetLamp(int lampNumber, int state) +{ + if (lampNumber >= 0 && lampNumber < LampCount) + { + lampState[lampNumber] = state; + } +} + +//########################################################################### +// The on-screen panel entries +//########################################################################### + +Logical + PadRIO::IsActive() +{ + return activeInstance != NULL; +} + +void + PadRIO::SetScreenButton(int unit, int pressed) +{ + if (activeInstance == NULL) + { + return; + } + if (unit < 0 || unit >= LBE4ControlsManager::ButtonCount) + { + return; + } + activeInstance->EmitButton(unit, pressed); +} + +int + PadRIO::GetLampState(int unit) +{ + if (activeInstance == NULL || unit < 0 || unit >= LampCount) + { + return 0; + } + return activeInstance->lampState[unit]; +} diff --git a/engine/MUNGA_L4/L4PADRIO.h b/engine/MUNGA_L4/L4PADRIO.h new file mode 100644 index 0000000..1fc1db6 --- /dev/null +++ b/engine/MUNGA_L4/L4PADRIO.h @@ -0,0 +1,110 @@ +#pragma once + +//########################################################################### +// +// L4PADRIO -- the hardware-less cockpit device (BT_GLASS only). +// +// PadRIO synthesizes the pod's abstract control surface (RIOBase: the five +// analog Scalars + button/keypad events + lamps) from an XInput controller +// and the PC keyboard, so the whole stock RIO path above the seam -- the +// LBE4ControlsManager push, MechRIOMapper, streamed .CTL mappings, lamp +// writes -- runs with no serial hardware. Selected at runtime with +// L4CONTROLS=PAD (the factory arm in L4CTRL.cpp). +// +// Input model (bindings: L4PADBINDINGS.h, content\bindings.txt): +// - keyboard keys emit button/keypad edge events and drive the analog +// channels (spring-stick deflect / throttle-lever slew / detent set) +// - XInput pad buttons emit button events; pad axes drive the channels +// directly (or as slew rates); hot-plug re-probe every ~3 s +// - on-screen cockpit buttons (the L4PADPANEL window, or any WndProc) +// inject through the static SetScreenButton() entry +// - lamps are recorded in lampState[] for the on-screen panel to render +// +// The keyboard is only read while a window of THIS process has focus. +// L4PADFLIP=1 inverts both stick axes. +// +//########################################################################### + +#include "l4rio.h" +#include "l4padbindings.h" + +class PadRIO : + public RIOBase +{ +public: + PadRIO(); + ~PadRIO(); + + Logical + GetNextEvent(RIOEvent *destinationPointer); + + void + SetLamp(int lampNumber, int state); + + Logical + IsOperational() const + { return True; } + + // + // The on-screen panel / WndProc entries. Safe no-ops when no PadRIO + // is active (pod build never links this TU; a glass build without + // L4CONTROLS=PAD never constructs one). Same-thread use only: the + // panel WndProc runs on the app thread's message pump. + // + static Logical + IsActive(); + static void + SetScreenButton(int unit, int pressed); + static int + GetLampState(int unit); + + enum { LampCount = 128 }; + +protected: + void + Poll(); + void + PushEvent(const RIOEvent &event); + void + EmitButton(int address, int pressed); + void + EmitKeypad(int unit, int key); + + PadBindingProfile + bindings; + + // + // Event queue (ring; drop-newest on overflow, logged). + // + enum { EventQueueSize = 128 }; + RIOEvent + eventQueue[EventQueueSize]; + int + eventHead, eventTail; + + // + // Poll state. + // + unsigned long + lastPollMilliseconds; + unsigned long + lastPadProbeMilliseconds; + int + padIndex; // connected XInput slot, -1 = none + unsigned + previousPadButtons; + unsigned char + previousKeyHeld[192]; // per key BINDING (parallel to the profile) + float + channelValue[PadBindingProfile::ChannelCount]; + float + channelReturnRate[PadBindingProfile::ChannelCount]; + int + flipStickAxes; // L4PADFLIP + + int + lampState[LampCount]; + + static PadRIO + *activeInstance; +}; diff --git a/engine/MUNGA_L4/L4RIO.cpp b/engine/MUNGA_L4/L4RIO.cpp index 2f2bc4a..1ebad97 100644 --- a/engine/MUNGA_L4/L4RIO.cpp +++ b/engine/MUNGA_L4/L4RIO.cpp @@ -731,6 +731,30 @@ void Check_Fpu(); } +//######################################################################## +//############################# RIOBase ################################## +//######################################################################## + +// +// The shared control surface starts zeroed; a concrete device (serial RIO, +// PadRIO) populates it. (The serial RIO ctor re-assigns these -- harmless.) +// +RIOBase::RIOBase(): + TestModeActive(0), + Throttle((Scalar) 0), + LeftPedal((Scalar) 0), + RightPedal((Scalar) 0), + JoystickX((Scalar) 0), + JoystickY((Scalar) 0), + MajorRevision(0), + MinorRevision(0) +{ +} + +RIOBase::~RIOBase() +{ +} + //######################################################################## //############################### RIO #################################### //######################################################################## diff --git a/engine/MUNGA_L4/L4RIO.h b/engine/MUNGA_L4/L4RIO.h index 0007210..dd8b4fa 100644 --- a/engine/MUNGA_L4/L4RIO.h +++ b/engine/MUNGA_L4/L4RIO.h @@ -109,26 +109,21 @@ protected: lowestInput; }; -class RIO : - public PCSerialPacket +//########################################################################### +// +// RIOBase -- the abstract cockpit-controls surface (glass-cockpit step 2b). +// +// Splits the device-independent state + API out of the serial RIO so a +// hardware-less device (PadRIO, built under BT_GLASS) can stand in behind +// the same LBE4ControlsManager consumption sites (the manager holds a +// RIOBase* -- L4CTRL.h). The serial RIO keeps its exact behavior; every +// existing RIO::/RIO:: use resolves here through public +// inheritance. Serial-flavored operations default to no-ops so a device +// without the concept (no board check, no deadband hardware) needs no code. +// +//########################################################################### +class RIOBase { -protected: - enum RIOCommand{ - CheckRequest=0x80, - VersionRequest, - AnalogRequest, - ResetRequest, - LampRequest, - CheckReply, - VersionReply, - AnalogReply, - ButtonPressed, - ButtonReleased, - KeyPressed, - KeyReleased, - TestModeChange - }; - public: enum RIOStatusType { BoardOk=0, BoardMissing=1, BoardBad=2, @@ -166,6 +161,90 @@ public: }Data; }; + RIOBase(); + virtual ~RIOBase(); + + // + // The operations every consumer needs a real implementation of. + // + virtual Logical + GetNextEvent(RIOEvent *destinationPointer) = 0; + virtual void + SetLamp(int lampNumber, int state) = 0; + + // + // Device liveness: True when the device is actually delivering input + // (the serial RIO's `operational` flag). The game-side keyboard input + // bridges stand down when an operational device exists (step 2c). + // + virtual Logical + IsOperational() const = 0; + + // + // Serial-flavored operations -- no-ops for devices without the concept. + // + virtual void + ForceCenterJoystick() {} + virtual void + SetJoystickDeadBand(Scalar dead_band) {} + virtual void + SetThrottleDeadBand(Scalar dead_band) {} + virtual void + SetPedalsDeadBand(Scalar dead_band) {} + virtual void + RequestCheck() {} + virtual void + RequestVersion() {} + virtual void + RequestAnalogUpdate() {} + virtual void + GeneralReset() {} + virtual void + ResetThrottle() {} + virtual void + ResetLeftPedal() {} + virtual void + ResetRightPedal() {} + virtual void + ResetVerticalJoystick() {} + virtual void + ResetHorizontalJoystick() {} + + // + // The shared control surface the manager's Execute() pushes from. + // + int + TestModeActive; + + Scalar + Throttle, LeftPedal, RightPedal, JoystickX, JoystickY; + + int + MajorRevision, MinorRevision; +}; + +class RIO : + public PCSerialPacket, + public RIOBase +{ +protected: + enum RIOCommand{ + CheckRequest=0x80, + VersionRequest, + AnalogRequest, + ResetRequest, + LampRequest, + CheckReply, + VersionReply, + AnalogReply, + ButtonPressed, + ButtonReleased, + KeyPressed, + KeyReleased, + TestModeChange + }; + +public: //Win32 Serial support: ADB 02/13/07 //RIO(Word port, Word intNum, Logical perform_tests = True); RIO(const char* port, Logical perform_tests = True); @@ -177,6 +256,10 @@ public: Logical GetNextEvent(RIOEvent *destinationPointer); + Logical + IsOperational() const + { return operational; } + void ForceCenterJoystick(), SetJoystickDeadBand(Scalar dead_band), @@ -267,14 +350,8 @@ public: TransmitQueueCount() { return PCSerialPacket::TransmitQueueCount(); } - int - TestModeActive; - - Scalar - Throttle, LeftPedal, RightPedal, JoystickX, JoystickY; - - int - MajorRevision, MinorRevision; + // (TestModeActive, the five analog Scalars, and Major/MinorRevision + // moved to RIOBase -- the shared control surface.) int remoteRetryCount; int remoteAbandonCount;