Ported from BT411, which needed the same thing for its glass cockpit. PadRIO reads XInput, which covers Xbox-class pads and nothing else. A flight stick, a HOTAS throttle, a twist grip, rudder pedals or a wheel arrive through DirectInput instead, and until now the game could not see any of them - the only generic-joystick path left was the 1995 single- device DIJoystick behind L4CONTROLS=DIJOYSTICK, which is untouched here. L4JOY is the reader: up to four devices as normalized state blocks, hot- plug re-enumeration on the same ~3 s cadence PadRIO uses to look for a pad, and a device lost mid-race zeroed rather than left holding whatever was pressed when it went. XInput-class devices are excluded by VID/PID against the RawInput paths carrying the "IG_" marker - without that an Xbox pad arrives through both APIs and every button counts twice. bindings.txt gains four rows in the grammar it already had, using its own vocabulary (deadzone/rate) rather than BT411's: joydev <slot> [product-name substring] joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>] joybutton <n> button <addr> [toggle] joyhat <n> <up|down|left|right> button <addr> Slots resolve to a live device every poll, by name substring or ordinal, so unplugging and replugging does not rewrite anyone's file. Two things the pod's shape forced that BT411 solved differently: Pedals - a signed composite axis that decomposes into the pod's two pedals, positive right and negative left. The pod has a pedal each side; a twist grip or rudder bar is one signed control, and pressing one or the other but never both is exactly what it wants to say. It is a channel name like any other, so a pad stick can drive the turn too. A joyaxis on Throttle with no rate is a real lever and OWNS the channel - full travel maps onto the 0..1 the pod runs on, instead of nudging the accumulator that a spring-centred pad stick has to use. RP412JOYCONFIG=1 (joyconfig.bat) runs the capture wizard before the console screen: it asks the player to move each control, and derives the sign convention from the DIRECTION of the move. That is the point of it - a stick that reads positive pushed right and one that reads negative are equally common, and no amount of documentation gets a player to work out which they own. It writes only its own section, between marker lines, so hand-edited keyboard and pad rows survive re-running it. The wizard also prints every axis at rest before it starts. A driver that refuses the +-32767 range we ask for reports its own, and an axis then sits hard over instead of near zero; seeing "X +1.00" on an untouched stick is the difference between a five-minute fix and a bug report that says it configured itself. Each capture reports the move it saw for the same reason. Verified on the Logitech Extreme 3D on this machine. Enumeration finds it and excludes the Xbox pad, which still arrives separately through XInput. Every row shape parses - 7 axes, 2 buttons, 4 hat directions - and three deliberately malformed rows (a bad axis name, button 99, a "sideways" hat) are each rejected by line number rather than silently dropped. The wizard lists the device with its axes at rest reading X +0.00 Y -0.01 RZ -0.04 SL0 +1.00, waits on the first prompt without self-triggering, and with a hand on the stick captures X to steering, Y to pitch, RZ to the pedals and SL0 to the throttle, inverting the ones that read backwards. Running the captures through to a written file needs a hand on the stick, so that part is the machine's to confirm, not this build's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
717 lines
23 KiB
C++
717 lines
23 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4padbindings.h"
|
|
#include "l4joy.h" // joyButtonCount / joyHatCount, the parse limits
|
|
|
|
#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 }, { "Shift", VK_SHIFT },
|
|
{ "ControlKey", VK_CONTROL }, { "Ctrl", VK_CONTROL }, { "Control", VK_CONTROL },
|
|
{ "Menu", VK_MENU }, { "Alt", VK_MENU },
|
|
{ "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 },
|
|
{ "Pedals", BindAxisPedals },
|
|
};
|
|
|
|
// DirectInput's axis order, which is what the joy* rows name
|
|
const NameValue kJoyAxisNames[] =
|
|
{
|
|
{ "X", BindJoyAxisX }, { "Y", BindJoyAxisY }, { "Z", BindJoyAxisZ },
|
|
{ "RX", BindJoyAxisRX }, { "RY", BindJoyAxisRY }, { "RZ", BindJoyAxisRZ },
|
|
{ "SL0", BindJoyAxisSL0 }, { "SL1", BindJoyAxisSL1 },
|
|
};
|
|
|
|
const NameValue kJoyHatNames[] =
|
|
{
|
|
{ "up", 0 }, { "right", 1 }, { "down", 2 }, { "left", 3 },
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Shared tail of the two axis-source rows: [invert] [deadzone <d>]
|
|
// [rate <n>], in any order.
|
|
//---------------------------------------------------------------
|
|
Logical ParseAxisOptions(
|
|
char *tokens[], int token_count, int first,
|
|
Logical *invert, Scalar *deadzone, Scalar *rate)
|
|
{
|
|
for (int i = first; i < token_count; ++i)
|
|
{
|
|
if (NameEquals(tokens[i], "invert"))
|
|
{
|
|
*invert = True;
|
|
}
|
|
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
|
|
{
|
|
if (!ParseNumber(tokens[++i], deadzone))
|
|
{
|
|
return False;
|
|
}
|
|
}
|
|
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
|
|
{
|
|
if (!ParseNumber(tokens[++i], rate))
|
|
{
|
|
return False;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return False;
|
|
}
|
|
}
|
|
return True;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// One line of the profile grammar. joy_slot carries the joydev
|
|
// state forward from line to line - joy rows attach to the slot
|
|
// most recently declared.
|
|
//---------------------------------------------------------------
|
|
Logical ParseLine(
|
|
char *tokens[], int token_count, PadBindingProfile *profile,
|
|
int *joy_slot)
|
|
{
|
|
//
|
|
// joydev is the one row that can be two tokens long ("joydev 1"),
|
|
// so it is answered before the four-token floor below.
|
|
//
|
|
if (NameEquals(tokens[0], "joydev") && token_count >= 2)
|
|
{
|
|
int slot = -1;
|
|
if (sscanf(tokens[1], "%d", &slot) != 1 ||
|
|
slot < 0 || slot >= BindJoyDeviceSlots)
|
|
{
|
|
return False;
|
|
}
|
|
*joy_slot = slot;
|
|
//
|
|
// The rest of the line is a product-name substring, rejoined
|
|
// with single spaces ("Saitek Pro Flight" is four tokens).
|
|
//
|
|
profile->joyDeviceMatch[slot][0] = '\0';
|
|
for (int i = 2; i < token_count; ++i)
|
|
{
|
|
if (i > 2)
|
|
{
|
|
strncat(profile->joyDeviceMatch[slot], " ",
|
|
sizeof(profile->joyDeviceMatch[slot]) -
|
|
strlen(profile->joyDeviceMatch[slot]) - 1);
|
|
}
|
|
strncat(profile->joyDeviceMatch[slot], tokens[i],
|
|
sizeof(profile->joyDeviceMatch[slot]) -
|
|
strlen(profile->joyDeviceMatch[slot]) - 1);
|
|
}
|
|
return True;
|
|
}
|
|
|
|
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;
|
|
return ParseAxisOptions(tokens, token_count, 4,
|
|
&binding->invert, &binding->deadzone, &binding->rate);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Generic joystick rows, all attaching to the current joydev slot
|
|
//---------------------------------------------------------------
|
|
if (NameEquals(tokens[0], "joyaxis") && NameEquals(tokens[2], "axis"))
|
|
{
|
|
int source = LookupTable(kJoyAxisNames,
|
|
sizeof(kJoyAxisNames) / sizeof(kJoyAxisNames[0]), tokens[1]);
|
|
int axis = LookupTable(kRioAxisNames,
|
|
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
|
|
if (source < 0 || axis < 0 ||
|
|
profile->joyAxisCount >= PadBindingProfile::maxJoyAxes)
|
|
{
|
|
return False;
|
|
}
|
|
PadJoyAxisBinding *binding = &profile->joyAxes[profile->joyAxisCount++];
|
|
memset(binding, 0, sizeof(*binding));
|
|
binding->device = *joy_slot;
|
|
binding->source = source;
|
|
binding->axis = axis;
|
|
return ParseAxisOptions(tokens, token_count, 4,
|
|
&binding->invert, &binding->deadzone, &binding->rate);
|
|
}
|
|
|
|
if (NameEquals(tokens[0], "joybutton") && NameEquals(tokens[2], "button"))
|
|
{
|
|
int button = -1;
|
|
int address;
|
|
if (sscanf(tokens[1], "%d", &button) != 1 ||
|
|
button < 0 || button >= joyButtonCount ||
|
|
!ParseAddress(tokens[3], &address) ||
|
|
profile->joyButtonCount >= PadBindingProfile::maxJoyButtons)
|
|
{
|
|
return False;
|
|
}
|
|
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
|
|
PadJoyButtonBinding *binding =
|
|
&profile->joyButtons[profile->joyButtonCount++];
|
|
memset(binding, 0, sizeof(*binding));
|
|
binding->device = *joy_slot;
|
|
binding->button = button;
|
|
binding->address = address;
|
|
binding->toggle = toggle;
|
|
return True;
|
|
}
|
|
|
|
if (NameEquals(tokens[0], "joyhat") && token_count >= 5 &&
|
|
NameEquals(tokens[3], "button"))
|
|
{
|
|
int hat = -1;
|
|
int direction = LookupTable(kJoyHatNames,
|
|
sizeof(kJoyHatNames) / sizeof(kJoyHatNames[0]), tokens[2]);
|
|
int address;
|
|
if (sscanf(tokens[1], "%d", &hat) != 1 ||
|
|
hat < 0 || hat >= joyHatCount || direction < 0 ||
|
|
!ParseAddress(tokens[4], &address) ||
|
|
profile->joyHatCount >= PadBindingProfile::maxJoyHats)
|
|
{
|
|
return False;
|
|
}
|
|
PadJoyHatBinding *binding = &profile->joyHats[profile->joyHatCount++];
|
|
memset(binding, 0, sizeof(*binding));
|
|
binding->device = *joy_slot;
|
|
binding->hat = hat;
|
|
binding->direction = direction;
|
|
binding->address = address;
|
|
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"
|
|
"# joydev <slot> [product-name substring]\n"
|
|
"# joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
|
"# joybutton <n> button <addr> [toggle]\n"
|
|
"# joyhat <n> <up|down|left|right> button <addr>\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"
|
|
"# | Pedals - a signed axis that works the pedal PAIR, positive\n"
|
|
"# for the right pedal and negative for the left, so one rudder\n"
|
|
"# bar or twist grip drives both.\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"
|
|
"# ---- Flight sticks, HOTAS throttles and rudder pedals --------------\n"
|
|
"#\n"
|
|
"# EASIEST: run joyconfig.bat once. It asks you to move each control,\n"
|
|
"# works out which device and axis you moved and which way round it\n"
|
|
"# reads, and writes the joy* rows below a marker line at the end of\n"
|
|
"# this file. Everything you have written yourself is kept. Xbox-class\n"
|
|
"# pads need none of this - they are the pad* rows above.\n"
|
|
"#\n"
|
|
"# By hand: joydev picks the device for the rows that follow it - a\n"
|
|
"# name substring binds that product, a bare slot number binds the Nth\n"
|
|
"# stick Windows lists. <src> for joyaxis is the DirectInput axis name,\n"
|
|
"# X Y Z RX RY RZ SL0 SL1: a twist grip is usually RZ and a HOTAS\n"
|
|
"# throttle usually Z or SL0. A joyaxis on Throttle with no 'rate' is\n"
|
|
"# treated as a real lever and OWNS the channel - its full travel is\n"
|
|
"# the full throttle range, rather than nudging the position the way a\n"
|
|
"# spring-centred pad stick has to.\n"
|
|
"#\n"
|
|
"# joydev 0 T.16000M\n"
|
|
"# joyaxis X axis JoystickX invert deadzone 0.08\n"
|
|
"# joyaxis Y axis JoystickY invert deadzone 0.08\n"
|
|
"# joyaxis RZ axis Pedals deadzone 0.08\n"
|
|
"# joyaxis SL0 axis Throttle deadzone 0\n"
|
|
"# joybutton 0 button 0x40\n"
|
|
"# joyhat 0 up button 0x42\n"
|
|
"\n"
|
|
"# ---- Flight: number pad + modifiers -------------------------------\n"
|
|
"# The whole letter board stays free for the MFD banks; flight lives\n"
|
|
"# on the numpad with throttle and reverse on the modifier keys.\n"
|
|
"key NumPad8 axis JoystickY deflect -1 # stick forward\n"
|
|
"key NumPad2 axis JoystickY deflect 1 # stick back\n"
|
|
"key NumPad4 axis JoystickX deflect 1 # stick left\n"
|
|
"key NumPad6 axis JoystickX deflect -1 # stick right\n"
|
|
"key NumPad7 axis LeftPedal deflect 1\n"
|
|
"key NumPad9 axis RightPedal deflect 1\n"
|
|
"key NumPad0 button 0x40 # Main trigger (Space works too)\n"
|
|
"key Shift axis Throttle rate 0.75 # throttle up\n"
|
|
"key Ctrl axis Throttle rate -0.75 # throttle down\n"
|
|
"key Alt button 0x3F # Throttle button (reverse thrust)\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 ------------\n"
|
|
"key Space button 0x40 # Main trigger\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"
|
|
"\n"
|
|
"# Start and Back are left free - map them to whatever you want, e.g.\n"
|
|
"# the config buttons on the Upper Right MFD:\n"
|
|
"#pad Start button 0x37\n"
|
|
"#pad Back button 0x36\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.\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 Q button 0x2B\n"
|
|
"key W button 0x2A\n"
|
|
"key E button 0x29\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 / B), mirroring the\n"
|
|
"# ---- keypad gap between the Lower Left and Lower Right MFDs.\n"
|
|
"key A button 0x0F\n"
|
|
"key S button 0x0E\n"
|
|
"key D button 0x0D\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"
|
|
"# The pilot keypad (0x50-0x5F) and external keypad (0x60-0x6F) are\n"
|
|
"# unbound - the game never reads them - but remain bindable here\n"
|
|
"# (they arrive as arcade RIO key events).\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;
|
|
int joy_slot = 0; // joy rows before any joydev belong to slot 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, &joy_slot))
|
|
{
|
|
++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;
|
|
if (profile->joyAxisCount || profile->joyButtonCount || profile->joyHatCount)
|
|
{
|
|
DEBUG_STREAM << "PadBindings: joystick - " << profile->joyAxisCount
|
|
<< " axes, " << profile->joyButtonCount << " buttons, "
|
|
<< profile->joyHatCount << " hat directions\n" << std::flush;
|
|
}
|
|
}
|