Rebindable input: vRIO bindings profile ported, full board coverage

PadRIO now loads bindings.txt (vRIO profile grammar: key/pad/padaxis
lines with toggle, deflect/rate, invert/deadzone options) written
self-documenting with the full default layout on first run. The
default is vRIO board-complete map - number and letter rows are the
MFD banks as printed on the panel, F-keys the secondary/screen
columns, numpad the pilot keypad (0x50-0x5F delivered as arcade RIO
KeyEvents, a new PadRIO capability), Space/arrows the joystick
column - with the desktop driving keys carved out: WASD stick, Q/E
pedals, PgUp/PgDn throttle, B reverse (vRIO gap key; R returns to
its bank). Pad bindings unchanged in spirit, plus Panic on LB and
config on Start/Back; axis signs are encoded in the profile now, so
L4PADFLIP flips on top of it.

Default profile parses with zero rejected lines (68 key buttons, 8
key axes, 12 pad buttons, 5 pad axes); single-player cycle and the
key-bomb tests stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-13 10:18:36 -05:00
co-authored by Claude Fable 5
parent fbd23ff1ca
commit b0def79de2
6 changed files with 844 additions and 105 deletions
+547
View File
@@ -0,0 +1,547 @@
#include "mungal4.h"
#pragma hdrstop
#include "l4padbindings.h"
#include <XInput.h>
#include <stdio.h>
//########################################################################
// Name tables: .NET Keys names (what vRIO uses) to virtual keys, pad
// names to XInput masks. Matching is case-insensitive.
//########################################################################
namespace
{
struct NameValue
{
const char *name;
int value;
};
const NameValue kKeyNames[] =
{
// letters and digits are generated in LookupKey
{ "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 },
{ "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 },
{ "Divide", VK_DIVIDE }, { "Multiply", VK_MULTIPLY },
{ "Subtract", VK_SUBTRACT }, { "Add", VK_ADD },
{ "Decimal", VK_DECIMAL },
{ "Up", VK_UP }, { "Down", VK_DOWN },
{ "Left", VK_LEFT }, { "Right", VK_RIGHT },
{ "Space", VK_SPACE }, { "Return", VK_RETURN }, { "Enter", VK_RETURN },
{ "Escape", VK_ESCAPE }, { "Tab", VK_TAB }, { "Back", VK_BACK },
{ "Home", VK_HOME }, { "End", VK_END },
{ "PageUp", VK_PRIOR }, { "PageDown", VK_NEXT },
{ "Prior", VK_PRIOR }, { "Next", VK_NEXT },
{ "Insert", VK_INSERT }, { "Delete", VK_DELETE },
{ "ShiftKey", VK_SHIFT }, { "ControlKey", VK_CONTROL },
{ "OemMinus", VK_OEM_MINUS }, { "Oemplus", VK_OEM_PLUS },
{ "Oemcomma", VK_OEM_COMMA }, { "OemPeriod", VK_OEM_PERIOD },
{ "OemOpenBrackets", VK_OEM_4 }, { "OemCloseBrackets", VK_OEM_6 },
{ "OemSemicolon", VK_OEM_1 }, { "OemQuotes", VK_OEM_7 },
{ "OemQuestion", VK_OEM_2 }, { "OemPipe", VK_OEM_5 },
{ "Oemtilde", VK_OEM_3 },
};
const NameValue kPadButtonNames[] =
{
{ "A", XINPUT_GAMEPAD_A }, { "B", XINPUT_GAMEPAD_B },
{ "X", XINPUT_GAMEPAD_X }, { "Y", XINPUT_GAMEPAD_Y },
{ "DPadUp", XINPUT_GAMEPAD_DPAD_UP },
{ "DPadDown", XINPUT_GAMEPAD_DPAD_DOWN },
{ "DPadLeft", XINPUT_GAMEPAD_DPAD_LEFT },
{ "DPadRight", XINPUT_GAMEPAD_DPAD_RIGHT },
{ "Start", XINPUT_GAMEPAD_START }, { "Back", XINPUT_GAMEPAD_BACK },
{ "LeftShoulder", XINPUT_GAMEPAD_LEFT_SHOULDER },
{ "RightShoulder", XINPUT_GAMEPAD_RIGHT_SHOULDER },
{ "LeftThumb", XINPUT_GAMEPAD_LEFT_THUMB },
{ "RightThumb", XINPUT_GAMEPAD_RIGHT_THUMB },
};
const NameValue kPadAxisNames[] =
{
{ "LeftStickX", BindPadLeftStickX }, { "LeftStickY", BindPadLeftStickY },
{ "RightStickX", BindPadRightStickX }, { "RightStickY", BindPadRightStickY },
{ "LeftTrigger", BindPadLeftTrigger }, { "RightTrigger", BindPadRightTrigger },
};
const NameValue kRioAxisNames[] =
{
{ "Throttle", BindAxisThrottle },
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
};
Logical NameEquals(const char *a, const char *b)
{
return _stricmp(a, b) == 0;
}
int LookupTable(const NameValue *table, int count, const char *name)
{
for (int i = 0; i < count; ++i)
{
if (NameEquals(table[i].name, name))
{
return table[i].value;
}
}
return -1;
}
int LookupKey(const char *name)
{
// single letter A-Z (VK == uppercase ASCII)
if (name[1] == '\0' &&
((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z')))
{
return toupper(name[0]);
}
// digit row: D0-D9 (VK == '0'-'9')
if ((name[0] == 'D' || name[0] == 'd') &&
name[1] >= '0' && name[1] <= '9' && name[2] == '\0')
{
return name[1];
}
return LookupTable(kKeyNames, sizeof(kKeyNames) / sizeof(kKeyNames[0]), name);
}
Logical ParseAddress(const char *text, int *address)
{
int value = -1;
if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X'))
{
if (sscanf(text + 2, "%x", &value) != 1)
{
return False;
}
}
else if (sscanf(text, "%d", &value) != 1)
{
return False;
}
if ((value >= 0x00 && value <= 0x47) || (value >= 0x50 && value <= 0x6F))
{
*address = value;
return True;
}
return False;
}
Logical ParseNumber(const char *text, Scalar *value)
{
float parsed;
if (sscanf(text, "%f", &parsed) != 1)
{
return False;
}
*value = (Scalar) parsed;
return True;
}
//---------------------------------------------------------------
// Line tokenizer: whitespace separated, '#' starts a comment
//---------------------------------------------------------------
int Tokenize(char *line, char *tokens[], int max_tokens)
{
char *hash = strchr(line, '#');
if (hash != NULL)
{
*hash = '\0';
}
int count = 0;
char *cursor = line;
while (count < max_tokens)
{
while (*cursor == ' ' || *cursor == '\t' || *cursor == '\r' || *cursor == '\n')
{
++cursor;
}
if (*cursor == '\0')
{
break;
}
tokens[count++] = cursor;
while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' &&
*cursor != '\r' && *cursor != '\n')
{
++cursor;
}
if (*cursor != '\0')
{
*cursor++ = '\0';
}
}
return count;
}
//---------------------------------------------------------------
// One line of the profile grammar
//---------------------------------------------------------------
Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile)
{
if (token_count < 4)
{
return False;
}
if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "button"))
{
int key = LookupKey(tokens[1]);
int address;
if (key < 0 || !ParseAddress(tokens[3], &address) ||
profile->keyButtonCount >= PadBindingProfile::maxKeyButtons)
{
return False;
}
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
PadKeyButtonBinding *binding = &profile->keyButtons[profile->keyButtonCount++];
memset(binding, 0, sizeof(*binding));
binding->virtualKey = key;
binding->address = address;
binding->toggle = toggle;
return True;
}
if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "axis"))
{
if (token_count < 6)
{
return False;
}
int key = LookupKey(tokens[1]);
int axis = LookupTable(kRioAxisNames,
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
int mode = NameEquals(tokens[4], "deflect") ? BindKeyDeflect
: NameEquals(tokens[4], "rate") ? BindKeyRate : -1;
Scalar value;
if (key < 0 || axis < 0 || mode < 0 || !ParseNumber(tokens[5], &value) ||
profile->keyAxisCount >= PadBindingProfile::maxKeyAxes)
{
return False;
}
PadKeyAxisBinding *binding = &profile->keyAxes[profile->keyAxisCount++];
binding->virtualKey = key;
binding->axis = axis;
binding->mode = mode;
binding->value = value;
return True;
}
if (NameEquals(tokens[0], "pad") && NameEquals(tokens[2], "button"))
{
int mask = LookupTable(kPadButtonNames,
sizeof(kPadButtonNames) / sizeof(kPadButtonNames[0]), tokens[1]);
int address;
if (mask < 0 || !ParseAddress(tokens[3], &address) ||
profile->padButtonCount >= PadBindingProfile::maxPadButtons)
{
return False;
}
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
PadPadButtonBinding *binding = &profile->padButtons[profile->padButtonCount++];
memset(binding, 0, sizeof(*binding));
binding->padMask = (unsigned short) mask;
binding->address = address;
binding->toggle = toggle;
return True;
}
if (NameEquals(tokens[0], "padaxis") && NameEquals(tokens[2], "axis"))
{
int source = LookupTable(kPadAxisNames,
sizeof(kPadAxisNames) / sizeof(kPadAxisNames[0]), tokens[1]);
int axis = LookupTable(kRioAxisNames,
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
if (source < 0 || axis < 0 ||
profile->padAxisCount >= PadBindingProfile::maxPadAxes)
{
return False;
}
PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++];
memset(binding, 0, sizeof(*binding));
binding->source = source;
binding->axis = axis;
for (int i = 4; i < token_count; ++i)
{
if (NameEquals(tokens[i], "invert"))
{
binding->invert = True;
}
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
{
if (!ParseNumber(tokens[++i], &binding->deadzone))
{
return False;
}
}
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
{
if (!ParseNumber(tokens[++i], &binding->rate))
{
return False;
}
}
else
{
return False;
}
}
return True;
}
return False;
}
//---------------------------------------------------------------
// The default profile: vRIO's board-complete layout, adapted for
// the desktop game - the driving keys (WASD/QE/PgUp/PgDn) stay
// axes, so six bank buttons lose their keyboard keys (they are
// still clickable on the on-screen cockpit, and rebindable here).
//---------------------------------------------------------------
const char kDefaultProfile[] =
"# RP412 input bindings - keyboard and Xbox (XInput) controller.\n"
"#\n"
"# One binding per line, '#' starts a comment, keywords are case-\n"
"# insensitive. Loaded at game start; delete this file to restore\n"
"# the defaults.\n"
"#\n"
"# key <name> button <addr> [toggle]\n"
"# key <name> axis <axis> deflect <n>\n"
"# key <name> axis <axis> rate <n-per-second>\n"
"# pad <button> button <addr> [toggle]\n"
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
"#\n"
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
"# <button> A B X Y DPadUp DPadDown DPadLeft DPadRight Start Back\n"
"# LeftShoulder RightShoulder LeftThumb RightThumb\n"
"# <src> LeftStickX LeftStickY RightStickX RightStickY\n"
"# LeftTrigger RightTrigger\n"
"#\n"
"# 'deflect' holds the axis there while the key is down and springs\n"
"# back on release; 'rate' walks the axis by <n> per second and the\n"
"# position sticks (the throttle). Every lamp button is also clickable\n"
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
"\n"
"# ---- Driving: keyboard --------------------------------------------\n"
"key W axis JoystickY deflect -1\n"
"key S axis JoystickY deflect 1\n"
"key A axis JoystickX deflect 1\n"
"key D axis JoystickX deflect -1\n"
"key Q axis LeftPedal deflect 1\n"
"key E axis RightPedal deflect 1\n"
"key PageUp axis Throttle rate 0.75\n"
"key PageDown axis Throttle rate -0.75\n"
"\n"
"# ---- Driving: Xbox controller (signs match the pod convention) ----\n"
"padaxis LeftStickX axis JoystickX invert deadzone 0.24\n"
"padaxis LeftStickY axis JoystickY invert deadzone 0.24\n"
"padaxis RightStickY axis Throttle deadzone 0.24 rate 0.75\n"
"padaxis LeftTrigger axis LeftPedal deadzone 0.12\n"
"padaxis RightTrigger axis RightPedal deadzone 0.12\n"
"\n"
"# ---- Joystick column: hat on the arrows, Main on Space, reverse\n"
"# ---- thrust on B (vRIO's gap key - the pod had it on the throttle)\n"
"key Space button 0x40 # Main trigger\n"
"key B button 0x3F # Throttle button (reverse thrust)\n"
"key Up button 0x42 # Hat Up\n"
"key Down button 0x41 # Hat Back\n"
"key Left button 0x44 # Hat Left\n"
"key Right button 0x43 # Hat Right\n"
"\n"
"# ---- Xbox controller: buttons -------------------------------------\n"
"pad A button 0x40 # Main\n"
"pad B button 0x46 # Middle\n"
"pad X button 0x45 # Pinky\n"
"pad Y button 0x47 # Upper\n"
"pad DPadUp button 0x42 # Hat Up\n"
"pad DPadDown button 0x41 # Hat Back\n"
"pad DPadLeft button 0x44 # Hat Left\n"
"pad DPadRight button 0x43 # Hat Right\n"
"pad LeftShoulder button 0x3D # Panic\n"
"pad RightShoulder button 0x3F # Throttle button (reverse)\n"
"pad Start button 0x37 # config 1\n"
"pad Back button 0x36 # config 2\n"
"\n"
"# ---- Upper MFD bank: the number row is the top MFD row and the\n"
"# ---- QWERTY row is the row under it, left to right across the\n"
"# ---- Left / Middle / Right MFDs. (W/A/S/D/Q/E drive; their bank\n"
"# ---- buttons live on the on-screen cockpit.)\n"
"key D1 button 0x2F\n"
"key D2 button 0x2E\n"
"key D3 button 0x2D\n"
"key D4 button 0x2C\n"
"key D5 button 0x27\n"
"key D6 button 0x26\n"
"key D7 button 0x25\n"
"key D8 button 0x24\n"
"key D9 button 0x37\n"
"key D0 button 0x36\n"
"key OemMinus button 0x35\n"
"key Oemplus button 0x34\n"
"key R button 0x28\n"
"key T button 0x23\n"
"key Y button 0x22\n"
"key U button 0x21\n"
"key I button 0x20\n"
"key O button 0x33\n"
"key P button 0x32\n"
"key OemOpenBrackets button 0x31\n"
"key OemCloseBrackets button 0x30\n"
"\n"
"# ---- Lower MFD bank: home row and the row below it, two 4-key\n"
"# ---- blocks split by an unbound gap key (G), mirroring the keypad\n"
"# ---- gap between the Lower Left and Lower Right MFDs.\n"
"key F button 0x0C\n"
"key H button 0x07\n"
"key J button 0x06\n"
"key K button 0x05\n"
"key L button 0x04\n"
"key Z button 0x0B\n"
"key X button 0x0A\n"
"key C button 0x09\n"
"key V button 0x08\n"
"key N button 0x03\n"
"key M button 0x02\n"
"key Oemcomma button 0x01\n"
"key OemPeriod button 0x00\n"
"\n"
"# ---- Secondary / Screen columns on the function keys, top to\n"
"# ---- bottom (0x16/0x17 and 0x1E/0x1F intentionally unmapped).\n"
"key F1 button 0x10\n"
"key F2 button 0x11\n"
"key F3 button 0x12\n"
"key F4 button 0x13\n"
"key F5 button 0x14\n"
"key F6 button 0x15\n"
"key F7 button 0x18\n"
"key F8 button 0x19\n"
"key F9 button 0x1A\n"
"key F10 button 0x1B\n"
"key F11 button 0x1C\n"
"key F12 button 0x1D\n"
"\n"
"# ---- Internal keypad on the numpad (hex keys on the operators) ----\n"
"key NumPad0 button 0x50\n"
"key NumPad1 button 0x51\n"
"key NumPad2 button 0x52\n"
"key NumPad3 button 0x53\n"
"key NumPad4 button 0x54\n"
"key NumPad5 button 0x55\n"
"key NumPad6 button 0x56\n"
"key NumPad7 button 0x57\n"
"key NumPad8 button 0x58\n"
"key NumPad9 button 0x59\n"
"key Divide button 0x5A # keypad A (/)\n"
"key Multiply button 0x5B # keypad B (*)\n"
"key Subtract button 0x5C # keypad C (-)\n"
"key Add button 0x5D # keypad D (+)\n"
"key Decimal button 0x5E # keypad E (.)\n"
"key Return button 0x5F # keypad F (Enter)\n";
const char kBindingsFileName[] = "bindings.txt";
}
//########################################################################
// Loader
//########################################################################
void
PadBindings_Load(PadBindingProfile *profile)
{
memset(profile, 0, sizeof(*profile));
//
// First run: write the default profile so the player has a
// self-documenting file to edit
//
FILE *file = fopen(kBindingsFileName, "rb");
if (file == NULL)
{
FILE *out = fopen(kBindingsFileName, "wb");
if (out != NULL)
{
fwrite(kDefaultProfile, 1, strlen(kDefaultProfile), out);
fclose(out);
DEBUG_STREAM << "PadBindings: wrote default " << kBindingsFileName
<< "\n" << std::flush;
}
file = fopen(kBindingsFileName, "rb");
}
//
// Parse: file if we have one, the built-in default if not
//
char *text = NULL;
long size = 0;
if (file != NULL)
{
fseek(file, 0, SEEK_END);
size = ftell(file);
fseek(file, 0, SEEK_SET);
text = new char[size + 1];
fread(text, 1, size, file);
text[size] = '\0';
fclose(file);
}
const char *source = (text != NULL) ? text : kDefaultProfile;
char line[256];
int line_number = 0;
int error_count = 0;
const char *cursor = source;
while (*cursor != '\0')
{
int length = 0;
while (cursor[length] != '\0' && cursor[length] != '\n' &&
length < (int) sizeof(line) - 1)
{
line[length] = cursor[length];
++length;
}
line[length] = '\0';
cursor += length;
if (*cursor == '\n')
{
++cursor;
}
++line_number;
char *tokens[12];
int token_count = Tokenize(line, tokens, 12);
if (token_count == 0)
{
continue;
}
if (!ParseLine(tokens, token_count, profile))
{
++error_count;
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
<< line_number << " rejected\n" << std::flush;
}
}
if (text != NULL)
{
delete[] text;
}
DEBUG_STREAM << "PadBindings: " << profile->keyButtonCount << " key buttons, "
<< profile->keyAxisCount << " key axes, "
<< profile->padButtonCount << " pad buttons, "
<< profile->padAxisCount << " pad axes"
<< (error_count ? " (with rejected lines)" : "")
<< "\n" << std::flush;
}
+116
View File
@@ -0,0 +1,116 @@
//===========================================================================//
// File: l4padbindings.h //
// Project: MUNGA Brick: PadRIO binding profiles //
// Contents: The rebindable input profile (vRIO bindings format) //
//---------------------------------------------------------------------------//
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
#pragma once
#include "..\munga\style.h"
//########################################################################
// The bindings file (vRIO's format, shared grammar):
//
// key <name> button <addr> [toggle]
// key <name> axis <axis> deflect <n> | rate <n>
// pad <button> button <addr> [toggle]
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
//
// <addr> is a RIO input address: lamp buttons 0x00-0x47, internal
// keypad 0x50-0x5F, external keypad 0x60-0x6F. Loaded from
// bindings.txt beside the exe; written there (self-documenting, with
// the full default profile) on first run. Bad lines are logged and
// skipped, good lines always win.
//########################################################################
enum PadBindRioAxis
{
BindAxisThrottle = 0,
BindAxisLeftPedal,
BindAxisRightPedal,
BindAxisJoystickY,
BindAxisJoystickX,
BindAxisCount
};
enum PadBindKeyMode
{
BindKeyDeflect = 0, // hold at value while down, spring back
BindKeyRate // walk by value/second while down, position holds
};
enum PadBindPadAxis
{
BindPadLeftStickX = 0,
BindPadLeftStickY,
BindPadRightStickX,
BindPadRightStickY,
BindPadLeftTrigger,
BindPadRightTrigger,
BindPadAxisCount
};
struct PadKeyButtonBinding
{
int virtualKey;
int address; // 0x00-0x47 button, 0x50-0x6F keypad
Logical toggle;
// runtime edge/latch state
Logical wasDown;
Logical latched;
};
struct PadKeyAxisBinding
{
int virtualKey;
int axis; // PadBindRioAxis
int mode; // PadBindKeyMode
Scalar value;
};
struct PadPadButtonBinding
{
unsigned short padMask; // XINPUT_GAMEPAD_*
int address;
Logical toggle;
Logical wasDown;
Logical latched;
};
struct PadPadAxisBinding
{
int source; // PadBindPadAxis
int axis; // PadBindRioAxis
Logical invert;
Scalar deadzone; // normalized 0..1
Scalar rate; // 0 = direct position, >0 = speed integrate
};
struct PadBindingProfile
{
enum
{
maxKeyButtons = 128,
maxKeyAxes = 32,
maxPadButtons = 32,
maxPadAxes = 16
};
PadKeyButtonBinding keyButtons[maxKeyButtons];
int keyButtonCount;
PadKeyAxisBinding keyAxes[maxKeyAxes];
int keyAxisCount;
PadPadButtonBinding padButtons[maxPadButtons];
int padButtonCount;
PadPadAxisBinding padAxes[maxPadAxes];
int padAxisCount;
};
// Load bindings.txt from the working directory into the profile,
// writing the default profile file first when absent. Errors go to
// the debug log with line numbers.
void
PadBindings_Load(PadBindingProfile *profile);
+151 -88
View File
@@ -7,54 +7,12 @@
#pragma comment(lib, "xinput9_1_0.lib") #pragma comment(lib, "xinput9_1_0.lib")
//######################################################################## //########################################################################
// Binding tables (vRIO default profile, condensed) // Input helpers; the binding tables live in bindings.txt now
// (l4padbindings.cpp writes and parses the vRIO-format profile)
//######################################################################## //########################################################################
namespace namespace
{ {
struct KeyBinding
{
int virtualKey;
int rioUnit;
};
const KeyBinding keyboardButtonMap[] =
{
{ VK_SPACE, 0x40 }, // ButtonJoystickTrigger
{ 'R', 0x3F }, // ButtonThrottle1 (reverse thrust)
{ VK_UP, 0x42 }, // ButtonJoystickHatUp
{ VK_DOWN, 0x41 }, // ButtonJoystickHatDown
{ VK_RIGHT, 0x43 }, // ButtonJoystickHatRight
{ VK_LEFT, 0x44 }, // ButtonJoystickHatLeft
{ VK_F1, 0x37 }, // ButtonAuxUpperRight1 (config)
{ VK_F2, 0x36 }, // ButtonAuxUpperRight2 (config)
};
struct PadBinding
{
unsigned short padMask;
int rioUnit;
};
const PadBinding padButtonMap[] =
{
{ XINPUT_GAMEPAD_A, 0x40 }, // trigger
{ XINPUT_GAMEPAD_B, 0x3F }, // reverse thrust
{ XINPUT_GAMEPAD_X, 0x45 }, // pinky
{ XINPUT_GAMEPAD_Y, 0x46 }, // thumb low
{ XINPUT_GAMEPAD_LEFT_SHOULDER, 0x46 }, // thumb low
{ XINPUT_GAMEPAD_RIGHT_SHOULDER, 0x47 }, // thumb high
{ XINPUT_GAMEPAD_DPAD_UP, 0x42 },
{ XINPUT_GAMEPAD_DPAD_DOWN, 0x41 },
{ XINPUT_GAMEPAD_DPAD_RIGHT, 0x43 },
{ XINPUT_GAMEPAD_DPAD_LEFT, 0x44 },
{ XINPUT_GAMEPAD_START, 0x37 }, // config 1
{ XINPUT_GAMEPAD_BACK, 0x36 }, // config 2
};
// Full throttle sweep takes ~1.3 s at full stick deflection.
const Scalar throttleRatePerSecond = 0.75f;
Scalar StickValue(int raw, int dead_zone) Scalar StickValue(int raw, int dead_zone)
{ {
if (raw > -dead_zone && raw < dead_zone) if (raw > -dead_zone && raw < dead_zone)
@@ -125,9 +83,12 @@ PadRIO::PadRIO()
sentJoystickX = sentJoystickY = (Scalar) 0; sentJoystickX = sentJoystickY = (Scalar) 0;
memset(buttonDown, 0, sizeof(buttonDown)); memset(buttonDown, 0, sizeof(buttonDown));
memset(keypadDown, 0, sizeof(keypadDown));
memset(lampState, 0, sizeof(lampState)); memset(lampState, 0, sizeof(lampState));
memset(screenButton, 0, sizeof(screenButton)); memset(screenButton, 0, sizeof(screenButton));
PadBindings_Load(&profile);
invertX = False; invertX = False;
invertY = False; invertY = False;
const char *flip = getenv("L4PADFLIP"); const char *flip = getenv("L4PADFLIP");
@@ -208,6 +169,17 @@ void
JoystickY = (Scalar) 0; JoystickY = (Scalar) 0;
analogRequested = True; analogRequested = True;
memset(lampState, 0, sizeof(lampState)); memset(lampState, 0, sizeof(lampState));
memset(keypadDown, 0, sizeof(keypadDown));
for (int i = 0; i < profile.keyButtonCount; ++i)
{
profile.keyButtons[i].latched = False;
profile.keyButtons[i].wasDown = False;
}
for (int i = 0; i < profile.padButtonCount; ++i)
{
profile.padButtons[i].latched = False;
profile.padButtons[i].wasDown = False;
}
} }
void void
@@ -295,27 +267,60 @@ void
} }
//--------------------------------------------------------------- //---------------------------------------------------------------
// Buttons: build the desired state across pad + keyboard, then // Buttons: build the desired state from the binding profile
// diff against what we last reported. // (keyboard + pad, with toggle latches), merge the on-screen
// cockpit buttons, then diff against what we last reported.
// Keypad addresses (0x50-0x6F) collect separately - they become
// arcade KeyEvents, not button events.
//--------------------------------------------------------------- //---------------------------------------------------------------
unsigned char desired[buttonUnits]; unsigned char desired[buttonUnits];
unsigned char keypadDesired[keypadUnits];
memset(desired, 0, sizeof(desired)); memset(desired, 0, sizeof(desired));
memset(keypadDesired, 0, sizeof(keypadDesired));
if (pad_live) for (int i = 0; i < profile.keyButtonCount; ++i)
{ {
for (int i = 0; i < (int)(sizeof(padButtonMap)/sizeof(padButtonMap[0])); ++i) PadKeyButtonBinding *binding = &profile.keyButtons[i];
Logical down = KeyDown(binding->virtualKey);
if (binding->toggle && down && !binding->wasDown)
{ {
if (pad.Gamepad.wButtons & padButtonMap[i].padMask) binding->latched = !binding->latched;
}
binding->wasDown = down;
if (binding->toggle ? binding->latched : down)
{
if (binding->address < buttonUnits)
{ {
desired[padButtonMap[i].rioUnit] = 1; desired[binding->address] = 1;
}
else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits)
{
keypadDesired[binding->address - 0x50] = 1;
} }
} }
} }
for (int i = 0; i < (int)(sizeof(keyboardButtonMap)/sizeof(keyboardButtonMap[0])); ++i) for (int i = 0; i < profile.padButtonCount; ++i)
{ {
if (KeyDown(keyboardButtonMap[i].virtualKey)) PadPadButtonBinding *binding = &profile.padButtons[i];
Logical down = pad_live &&
(pad.Gamepad.wButtons & binding->padMask) != 0;
if (binding->toggle && down && !binding->wasDown)
{ {
desired[keyboardButtonMap[i].rioUnit] = 1; binding->latched = !binding->latched;
}
binding->wasDown = down;
if (binding->toggle ? binding->latched : down)
{
if (binding->address < buttonUnits)
{
desired[binding->address] = 1;
}
else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits)
{
keypadDesired[binding->address - 0x50] = 1;
}
} }
} }
for (int i = 0; i < buttonUnits; ++i) for (int i = 0; i < buttonUnits; ++i)
@@ -340,53 +345,111 @@ void
} }
//--------------------------------------------------------------- //---------------------------------------------------------------
// Axes // Keypads: presses become the arcade RIO KeyEvents. Unit 0 is the
// pilot's internal keypad (0x50-0x5F), unit 1 the external
// operator keypad (0x60-0x6F); the key is the hex digit 0-15.
//--------------------------------------------------------------- //---------------------------------------------------------------
Scalar x = (Scalar) 0, y = (Scalar) 0; for (int pad_key = 0; pad_key < keypadUnits; ++pad_key)
Scalar left_pedal = (Scalar) 0, right_pedal = (Scalar) 0;
Scalar throttle_rate = (Scalar) 0;
if (pad_live)
{ {
x += StickValue(pad.Gamepad.sThumbLX, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); if (keypadDesired[pad_key] != keypadDown[pad_key])
y += StickValue(pad.Gamepad.sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
throttle_rate += StickValue(pad.Gamepad.sThumbRY, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE);
if (pad.Gamepad.bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
{ {
left_pedal += (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f; keypadDown[pad_key] = keypadDesired[pad_key];
} if (keypadDesired[pad_key])
if (pad.Gamepad.bRightTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) {
{ RIOEvent an_event;
right_pedal += (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f; an_event.Type = KeyEvent;
an_event.Data.Keyboard.Unit = (pad_key >= 0x10) ? 1 : 0;
an_event.Data.Keyboard.Key = pad_key & 0x0F;
QueueEvent(an_event);
}
} }
} }
// keyboard: WASD stick deflect, Q/E pedals, PgUp/PgDn throttle //---------------------------------------------------------------
if (KeyDown('A')) x -= 1.0f; // Axes, from the profile. 'deflect' sources sum into a springy
if (KeyDown('D')) x += 1.0f; // position; 'rate' sources integrate the throttle (the pod's only
if (KeyDown('W')) y += 1.0f; // sticky axis) by value per second.
if (KeyDown('S')) y -= 1.0f; //---------------------------------------------------------------
if (KeyDown('Q')) left_pedal += 1.0f; Scalar deflect[BindAxisCount];
if (KeyDown('E')) right_pedal += 1.0f; Scalar rate[BindAxisCount];
if (KeyDown(VK_PRIOR)) throttle_rate += 1.0f; // PgUp memset(deflect, 0, sizeof(deflect));
if (KeyDown(VK_NEXT)) throttle_rate -= 1.0f; // PgDn memset(rate, 0, sizeof(rate));
for (int i = 0; i < profile.keyAxisCount; ++i)
{
const PadKeyAxisBinding *binding = &profile.keyAxes[i];
if (KeyDown(binding->virtualKey))
{
if (binding->mode == BindKeyRate)
{
rate[binding->axis] += binding->value;
}
else
{
deflect[binding->axis] += binding->value;
}
}
}
if (pad_live)
{
for (int i = 0; i < profile.padAxisCount; ++i)
{
const PadPadAxisBinding *binding = &profile.padAxes[i];
Scalar value = (Scalar) 0;
switch (binding->source)
{
case BindPadLeftStickX:
value = StickValue(pad.Gamepad.sThumbLX, (int)(binding->deadzone * 32767.0f));
break;
case BindPadLeftStickY:
value = StickValue(pad.Gamepad.sThumbLY, (int)(binding->deadzone * 32767.0f));
break;
case BindPadRightStickX:
value = StickValue(pad.Gamepad.sThumbRX, (int)(binding->deadzone * 32767.0f));
break;
case BindPadRightStickY:
value = StickValue(pad.Gamepad.sThumbRY, (int)(binding->deadzone * 32767.0f));
break;
case BindPadLeftTrigger:
value = (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f;
if (value <= binding->deadzone) value = (Scalar) 0;
break;
case BindPadRightTrigger:
value = (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f;
if (value <= binding->deadzone) value = (Scalar) 0;
break;
}
if (binding->invert)
{
value = -value;
}
if (binding->rate > 0.0f)
{
rate[binding->axis] += value * binding->rate;
}
else
{
deflect[binding->axis] += value;
}
}
}
throttleAccum = Clamp01(throttleAccum + rate[BindAxisThrottle] * delta_t);
Scalar x = deflect[BindAxisJoystickX];
Scalar y = deflect[BindAxisJoystickY];
if (x > 1.0f) x = 1.0f; if (x > 1.0f) x = 1.0f;
if (x < -1.0f) x = -1.0f; if (x < -1.0f) x = -1.0f;
if (y > 1.0f) y = 1.0f; if (y > 1.0f) y = 1.0f;
if (y < -1.0f) y = -1.0f; if (y < -1.0f) y = -1.0f;
throttleAccum = Clamp01(throttleAccum + throttle_rate * throttleRatePerSecond * delta_t); Throttle = Clamp01(throttleAccum + deflect[BindAxisThrottle]);
LeftPedal = Clamp01(deflect[BindAxisLeftPedal]);
Throttle = throttleAccum; RightPedal = Clamp01(deflect[BindAxisRightPedal]);
LeftPedal = Clamp01(left_pedal); // The profile encodes the pod's stick sign convention; L4PADFLIP
RightPedal = Clamp01(right_pedal); // flips on top of it per axis.
// The pod's stick convention is opposite the XInput axes on both X and JoystickX = invertX ? -x : x;
// Y (confirmed in playtest), so the default is negated; L4PADFLIP JoystickY = invertY ? -y : y;
// flips back per axis.
JoystickX = invertX ? x : -x;
JoystickY = invertY ? y : -y;
//--------------------------------------------------------------- //---------------------------------------------------------------
// Emit an analog event when asked to, or when anything moved // Emit an analog event when asked to, or when anything moved
+20 -14
View File
@@ -1,29 +1,27 @@
#pragma once #pragma once
#include "l4rio.h" #include "l4rio.h"
#include "l4padbindings.h"
//######################################################################## //########################################################################
//############################### PadRIO ################################# //############################### PadRIO #################################
//######################################################################## //########################################################################
// //
// A cockpit-less RIO: synthesizes the RIOBase control surface from an // A cockpit-less RIO: synthesizes the RIOBase control surface from an
// XInput controller and the PC keyboard, following vRIO's default // XInput controller and the PC keyboard. Selected with L4CONTROLS=PAD.
// binding profile. Selected with L4CONTROLS=PAD.
// //
// Left stick -> joystick X/Y [-1..1] // Every input is REBINDABLE: bindings.txt beside the exe (vRIO's
// Left/right trigger-> left/right pedal [0..1] // profile format, written with the full default layout on first run)
// Right stick Y -> throttle rate (position holds) [0..1] // maps keys and pad controls to RIO button addresses (0x00-0x47), the
// A / B -> joystick trigger / reverse thrust // pilot/external keypads (0x50-0x6F, delivered as arcade KeyEvents),
// X / Y / LB / RB -> pinky / thumb-low / thumb-low / thumb-high // and the five axes with deflect/rate semantics. Defaults follow
// DPad -> joystick hat // vRIO's board-complete layout with the driving keys carved out:
// Back / Start -> config buttons (AuxUpperRight2 / 1) // WASD stick, Q/E pedals, PgUp/PgDn throttle, Space trigger, B
// // reverse, arrows hat, letter rows = MFD banks, F-keys = secondary
// Keyboard: WASD = stick deflect (spring-back), PgUp/PgDn = throttle, // columns, numpad = pilot keypad.
// Q/E = pedals, Space = trigger, R = reverse, arrows = hat, F1/F2 =
// config buttons.
// //
// Set L4PADFLIP to a string containing 'X' and/or 'Y' to invert the // Set L4PADFLIP to a string containing 'X' and/or 'Y' to invert the
// stick axes. // stick axes on top of whatever the profile produces.
// //
class PadRIO : class PadRIO :
public RIOBase public RIOBase
@@ -102,6 +100,14 @@ protected:
unsigned char unsigned char
buttonDown[buttonUnits]; buttonDown[buttonUnits];
// keypad pressed-state (0x50-0x6F), for KeyEvent edges
enum { keypadUnits = 0x20 };
unsigned char
keypadDown[keypadUnits];
PadBindingProfile
profile;
Scalar Scalar
throttleAccum; throttleAccum;
Logical Logical
+2
View File
@@ -254,6 +254,7 @@
<ClCompile Include=".\L4STEAMTRANSPORT.cpp" /> <ClCompile Include=".\L4STEAMTRANSPORT.cpp" />
<ClCompile Include=".\L4PARTICLES.cpp" /> <ClCompile Include=".\L4PARTICLES.cpp" />
<ClCompile Include=".\L4MFDVIEW.cpp" /> <ClCompile Include=".\L4MFDVIEW.cpp" />
<ClCompile Include=".\L4PADBINDINGS.cpp" />
<ClCompile Include=".\L4PADRIO.cpp" /> <ClCompile Include=".\L4PADRIO.cpp" />
<ClCompile Include=".\L4PCSPAK.cpp" /> <ClCompile Include=".\L4PCSPAK.cpp" />
<ClCompile Include=".\L4PLASMA.cpp" /> <ClCompile Include=".\L4PLASMA.cpp" />
@@ -443,6 +444,7 @@
<ClInclude Include=".\L4MPPR.h" /> <ClInclude Include=".\L4MPPR.h" />
<ClInclude Include=".\L4NET.H" /> <ClInclude Include=".\L4NET.H" />
<ClInclude Include=".\L4NETTRANSPORT.h" /> <ClInclude Include=".\L4NETTRANSPORT.h" />
<ClInclude Include=".\L4PADBINDINGS.h" />
<ClInclude Include=".\L4STEAMTRANSPORT.h" /> <ClInclude Include=".\L4STEAMTRANSPORT.h" />
<ClInclude Include=".\L4PARTICLES.h" /> <ClInclude Include=".\L4PARTICLES.h" />
<ClInclude Include=".\L4MFDVIEW.h" /> <ClInclude Include=".\L4MFDVIEW.h" />
+8 -3
View File
@@ -99,15 +99,20 @@ Red Planet 4.12 - desktop prototype
Run start-windowed.bat (or: rpl4opt.exe -windowed -res 800 600 -egg TEST.EGG). Run start-windowed.bat (or: rpl4opt.exe -windowed -res 800 600 -egg TEST.EGG).
No cockpit hardware needed. If there is no sound, run oalinst.exe once. No cockpit hardware needed. If there is no sound, run oalinst.exe once.
Controls (XInput controller and/or keyboard): Controls (XInput controller and/or keyboard) - EVERY input is
rebindable: edit bindings.txt beside the exe (written with the full
documented default layout on first run; delete it to restore).
Left stick / WASD joystick Left stick / WASD joystick
LT / RT or Q / E left / right pedal LT / RT or Q / E left / right pedal
Right stick Y, PgUp/PgDn throttle (holds position) Right stick Y, PgUp/PgDn throttle (holds position)
A / Space joystick trigger A / Space joystick trigger
B / R reverse thrust RB / B key reverse thrust
DPad / arrow keys joystick hat (look) DPad / arrow keys joystick hat (look)
Start,Back / F1,F2 config buttons Start,Back / 9,0 config buttons
Number+letter rows MFD bank buttons (as printed on the panel)
F1-F12 secondary / screen columns
Numpad pilot keypad (0-9, /*-+. = A-E, Enter = F)
Alt+Q abort the mission (score banked) Alt+Q abort the mission (score banked)
The full pod cockpit comes up in a single window: three green MFDs The full pod cockpit comes up in a single window: three green MFDs