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 <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 <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 },
|
||||
};
|
||||
|
||||
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 <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\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;
|
||||
}
|
||||
@@ -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 <KEYNAME> button <addr>
|
||||
// key <KEYNAME> keypad <unit> <key>
|
||||
// key <KEYNAME> axis <channel> deflect <+|-> <rate>
|
||||
// key <KEYNAME> axis <channel> slew <+|-> <rate>
|
||||
// key <KEYNAME> axis <channel> set <value>
|
||||
// pad <PADBTN> button <addr>
|
||||
// padaxis <PADAXIS> axis <channel> [invert] [slew <rate>]
|
||||
//
|
||||
// deflect = held drives the channel toward +/-1 at <rate>/s, auto-centers
|
||||
// on release (the spring-centered stick model).
|
||||
// slew = held moves the channel at <rate>/s and it STAYS (the throttle
|
||||
// lever model).
|
||||
// set = press jumps the channel to <value> (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);
|
||||
};
|
||||
@@ -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 <windows.h>
|
||||
#include <xinput.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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];
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 ####################################
|
||||
//########################################################################
|
||||
|
||||
+104
-27
@@ -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::<enum>/RIO::<member> 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;
|
||||
|
||||
Reference in New Issue
Block a user