You are never asked what you own. Two controls cannot simply be watched, so they are asked for differently. Yaw is asked for twice, right then left, and which axis answers is the measurement. The same axis both times is one control covering both directions - a twist grip, a rudder bar, pedals the driver has already mixed - and binds to the signed Pedals axis. Two different axes are two real pedals, one per foot, which is what the pod had, so they bind to the pod's own LeftPedal/RightPedal pair and the game does the mixing: both at once then does what both at once did in the pod. The throttle is zeroed first. A lever sits wherever it was last left, possibly hard against the stop that reads +1, so watching it move says nothing about which end means power. Close it, press SPACE, then open it, and the direction it travels from a known idle is the direction that means throttle. CONTROLS.md, the handbook and the packaged README say all of this, and joyconfig.bat's own header no longer promises "rudder-pedal setup" when racing pedals work too.
758 lines
25 KiB
C++
758 lines
25 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. 'lever' rides along for the rows that
|
|
// can take it - a NULL lever means the word is not legal here, and
|
|
// a padaxis row is exactly that: the pad's own triggers already
|
|
// read 0..1, so there is no half-travel to rescue.
|
|
//---------------------------------------------------------------
|
|
Logical ParseAxisOptions(
|
|
char *tokens[], int token_count, int first,
|
|
Logical *invert, Scalar *deadzone, Scalar *rate,
|
|
Logical *lever = NULL)
|
|
{
|
|
for (int i = first; i < token_count; ++i)
|
|
{
|
|
if (NameEquals(tokens[i], "invert"))
|
|
{
|
|
*invert = True;
|
|
}
|
|
else if (NameEquals(tokens[i], "lever") && lever != NULL)
|
|
{
|
|
*lever = 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;
|
|
}
|
|
//
|
|
// Built aside and only then committed: a row whose options
|
|
// go bad half way through is a REJECTED row, and taking the
|
|
// slot first would leave the good half of it bound anyway,
|
|
// under a log line that says it was skipped.
|
|
//
|
|
PadPadAxisBinding candidate;
|
|
memset(&candidate, 0, sizeof(candidate));
|
|
candidate.source = source;
|
|
candidate.axis = axis;
|
|
if (!ParseAxisOptions(tokens, token_count, 4,
|
|
&candidate.invert, &candidate.deadzone, &candidate.rate))
|
|
{
|
|
return False;
|
|
}
|
|
profile->padAxes[profile->padAxisCount++] = candidate;
|
|
return True;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// 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 candidate;
|
|
memset(&candidate, 0, sizeof(candidate));
|
|
candidate.device = *joy_slot;
|
|
candidate.source = source;
|
|
candidate.axis = axis;
|
|
if (!ParseAxisOptions(tokens, token_count, 4,
|
|
&candidate.invert, &candidate.deadzone, &candidate.rate,
|
|
&candidate.lever))
|
|
{
|
|
return False;
|
|
}
|
|
profile->joyAxes[profile->joyAxisCount++] = candidate;
|
|
return True;
|
|
}
|
|
|
|
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] [lever] [deadzone <d>] [rate <n>]\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. Two-pedal hardware - racing\n"
|
|
"# pedals, rudder pedals with an axis per foot - skips the\n"
|
|
"# composite and binds LeftPedal and RightPedal directly; the\n"
|
|
"# game mixes the pair into yaw the way the pod always did.\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"
|
|
"# 'lever' marks a source that rests at one END of its travel instead of\n"
|
|
"# in the middle - a floor pedal, a slider. Windows reports it as a full\n"
|
|
"# -1..1 axis all the same, so without the word half the travel sits\n"
|
|
"# below zero and the first half of the press does nothing; with it the\n"
|
|
"# travel maps onto 0..1 and the deadzone measures from the released end.\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"
|
|
"# ...and the same stick with racing pedals on a second device, one\n"
|
|
"# axis per foot instead of the composite:\n"
|
|
"#\n"
|
|
"# joydev 1 Pedals\n"
|
|
"# joyaxis Y axis LeftPedal lever deadzone 0.05\n"
|
|
"# joyaxis RZ axis RightPedal lever deadzone 0.05\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;
|
|
}
|
|
}
|