The joystick wizard works out the shape of your pedals

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.
This commit is contained in:
Cyd
2026-08-07 16:31:40 -05:00
parent 6e829f815e
commit d35af59136
7 changed files with 590 additions and 89 deletions
+387 -47
View File
@@ -508,13 +508,67 @@ int
// common, and no amount of documentation gets a player to work out which
// they own.
//
// The same principle runs deeper than the sign. A player should not have
// to know the SHAPE of their own rig either, so the wizard works that
// out too, and the two controls that cannot simply be watched are asked
// for differently:
//
// yaw asked for twice, right then left. One axis answering both
// is a twist grip or rudder bar - the signed Pedals
// composite. Two different axes are two real pedals, one per
// foot, the pod's own arrangement, bound to the real pair.
//
// throttle a lever sits wherever it was left, so no movement of it
// says which end is open. The player is asked to put it at
// ZERO and say so; the reading is taken there, and the
// direction it travels from a known idle means power.
//
//########################################################################
#include <conio.h>
#include <XInput.h>
#include "l4padbindings.h"
namespace
{
//
// Xbox-class pads are kept out of the capture on purpose: their
// layout is fixed and NAMED, so unlike a DirectInput axis there is
// nothing to identify by watching, and letting one answer a prompt
// would only bind it twice. Invisible is the wrong answer though -
// a player whose whole rig is a pad, or a wheel running in XInput
// mode, should be told it is already mapped rather than left reading
// "no devices found" and wondering what is broken.
//
int WizardXInputSlot(void)
{
XINPUT_STATE state;
for (int i = 0; i < 4; ++i)
{
if (XInputGetState((DWORD) i, &state) == ERROR_SUCCESS)
{
return i;
}
}
return -1;
}
//
// What an Xbox-class pad already does, said once and in one place.
// The triggers are the interesting half: XInput reports each as its
// own 0..255 byte rather than two halves of a shared axis, which is
// the pod's two-pedal arrangement exactly, and unipolar already - no
// 'lever' to fold, no sign to discover.
//
void WizardReportXInput(int slot)
{
printf(" [XInput slot %d] Xbox-class controller - ALREADY MAPPED, and\n"
" not part of this setup. Its two triggers are the pod's\n"
" left and right pedals, the left stick is the joystick and\n"
" the right stick the throttle. Edit the pad rows of\n"
" bindings.txt by hand to change any of that.\n", slot);
}
struct WizardCapture
{
int used;
@@ -532,6 +586,39 @@ namespace
return (axis >= 0 && axis < joyAxisCount) ? names[axis] : "?";
}
//
// A pedal is a ONE-WAY control: its spring holds it at the released
// end of its travel, so the direction of the press is the whole
// story and the row it writes says 'lever' - the -1..1 axis the
// driver reports then folds onto the 0..1 the channel runs on
// instead of throwing away the half that reads below zero.
//
// Where it RESTS is what decides that, and the wizard can see it.
// An axis sitting near the MIDDLE is not a pedal at all - a stick
// axis pressed into service as one - and already reads zero at
// rest, so 'lever' would jam it at half depression for good.
//
void WizardWritePedal(WizardCapture *capture, int axis, float rest,
float delta, const char *channel)
{
capture->invert = (delta < 0.0f);
if (rest > 0.5f || rest < -0.5f)
{
sprintf(capture->line, "joyaxis %s axis %s%s lever deadzone 0.05",
JoyAxisToken(axis), channel,
capture->invert ? " invert" : "");
}
else
{
printf(" (%s rests near centre rather than at one end, so it\n"
" is bound as a plain axis rather than as a pedal)\n",
JoyAxisToken(axis));
sprintf(capture->line, "joyaxis %s axis %s%s deadzone 0.08",
JoyAxisToken(axis), channel,
capture->invert ? " invert" : "");
}
}
void WizardBaseline(float baseline[joyMaxDevices][joyAxisCount])
{
//
@@ -746,11 +833,30 @@ int
PadBindings_Load(&ensure_default);
}
int xinput_slot = WizardXInputSlot();
if (RPJoyInit() == 0)
{
printf("No generic (non-Xbox) game devices found.\n");
printf("Plug in the stick, throttle or pedals and run joyconfig again.\n");
printf("(Xbox-class controllers already work - no setup needed.)\n\n");
if (xinput_slot >= 0)
{
//
// Not a failure, and it should not read like one: the pad IS
// the rig, and it is already configured. Say what it does
// rather than asking for hardware they have not got.
//
printf("Nothing here needs configuring.\n\n");
WizardReportXInput(xinput_slot);
printf("\nThere are no generic (DirectInput) sticks, throttles or\n");
printf("pedals attached, and those are the only thing this setup\n");
printf("has to work out. Plug one in and run joyconfig again if\n");
printf("you add one.\n\n");
}
else
{
printf("No generic (non-Xbox) game devices found.\n");
printf("Plug in the stick, throttle or pedals and run joyconfig again.\n");
printf("(Xbox-class controllers already work - no setup needed.)\n\n");
}
printf("Press any key to exit.\n");
_getch();
return 1;
@@ -783,48 +889,51 @@ int
}
printf("\n");
}
//
// Listed with the rest so a player who squeezes a trigger at a
// prompt and sees nothing happen knows why, rather than deciding
// the wizard cannot see their pad.
//
if (xinput_slot >= 0)
{
WizardReportXInput(xinput_slot);
}
}
printf("\nFor each prompt, MOVE the control you want, or press SPACE to\n"
"skip it, ESC to abort. Keep everything else still.\n\n");
WizardCapture captures[16];
memset(captures, 0, sizeof(captures));
int capture_count = 0;
//
// The pod's analog channels. wants_negative says the asked-for move
// should read NEGATIVE in the pod's sign convention, which is what
// decides whether the captured axis gets an invert:
// The stick, whose two axes are spring-centred and so give their
// sign away the moment they move. wants_negative says the asked-for
// move should read NEGATIVE in the pod's sign convention, which is
// what decides whether the captured axis gets an invert:
//
// JoystickX left +1, right -1
// JoystickY forward -1, back +1
// Pedals right +1, left -1 (the composite that decomposes
// into the pod's two pedals)
//
// Yaw and the throttle are not this simple and are asked for below.
//
struct AxisStep
{
const char *prompt;
const char *channel;
int wants_negative;
int lever; // full-travel lever: sign from where
// it ENDS, not which way it moved
int allow_skip;
};
static const AxisStep axisSteps[] =
{
{ "STEER: push the STICK / turn the WHEEL fully RIGHT",
"JoystickX", 1, 0, 0 },
"JoystickX", 1 },
{ "PITCH: push the STICK fully FORWARD\n"
" (add or remove the word invert on that line in\n"
" bindings.txt to flip it later)",
"JoystickY", 1, 0, 0 },
{ "PEDALS: twist the stick / press the RIGHT rudder pedal\n"
" (SPACE if you have neither)",
"Pedals", 0, 0, 1 },
{ "THROTTLE: move the throttle lever to FULL (SPACE if none)",
"Throttle", 0, 1, 1 }
"JoystickY", 1 }
};
printf("\nFor each prompt, MOVE the control you want, or press SPACE to\n"
"skip it, ESC to abort. Keep everything else still.\n\n");
float baseline[joyMaxDevices][joyAxisCount];
for (int s = 0; s < (int)(sizeof(axisSteps) / sizeof(axisSteps[0])); ++s)
@@ -834,7 +943,7 @@ int
int device, axis;
float delta, final_value;
int got = WizardCaptureAxis(baseline, captures, capture_count,
axisSteps[s].allow_skip, &device, &axis, &delta, &final_value);
0, &device, &axis, &delta, &final_value);
if (got < 0)
{
printf("\nAborted - nothing written.\n");
@@ -842,38 +951,18 @@ int
_getch();
return 1;
}
if (got == 0)
{
printf(" skipped.\n\n");
continue;
}
WizardCapture &capture = captures[capture_count++];
capture.used = 1;
capture.device = device;
capture.axis = axis;
capture.button = -1;
if (axisSteps[s].lever)
{
//
// A lever has no rest position to move away from, so the
// sign comes from where it finished: full-forward reading
// negative means the axis runs backwards for us.
//
capture.invert = (final_value < 0.0f);
sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0",
JoyAxisToken(axis), axisSteps[s].channel,
capture.invert ? " invert" : "");
}
else
{
int went_negative = (delta < 0.0f);
capture.invert = axisSteps[s].wants_negative
? !went_negative : went_negative;
sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0.08",
JoyAxisToken(axis), axisSteps[s].channel,
capture.invert ? " invert" : "");
}
int went_negative = (delta < 0.0f);
capture.invert = axisSteps[s].wants_negative
? !went_negative : went_negative;
sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0.08",
JoyAxisToken(axis), axisSteps[s].channel,
capture.invert ? " invert" : "");
//
// The move is reported, not just the axis: a capture nobody made
// shows up here as a small delta, and a player who wonders why
@@ -887,6 +976,257 @@ int
Sleep(800); // let the control come back to rest
}
//---------------------------------------------------------------
// Yaw. The pod steered on two foot pedals mixed into the turn, and
// hardware answers that in two shapes - but a player should not have
// to know which shape they own, and plenty do not. So ask for RIGHT,
// then ask for LEFT, and watch WHICH axis answers each time:
//
// the same axis twice one control covering both directions - a
// twist grip, a rudder bar, pedals whose
// driver has already mixed them - which is
// the signed Pedals composite
//
// two different axes two real pedals, one per foot, which is
// what the pod itself had. They bind to the
// pod's own pair and the game does the
// mixing, so both at once does what both at
// once did in the pod.
//
// The LEFT capture is deliberately offered the RIGHT axis again -
// the usual claimed-axis exclusion would make every rig look like a
// pair, since "the same axis answered twice" is the measurement.
//---------------------------------------------------------------
{
printf("YAW RIGHT: press the RIGHT rudder pedal, or twist / push\n"
" the stick RIGHT (SPACE if you have no yaw control) ...\n");
WizardBaseline(baseline);
int right_device, right_axis;
float right_delta, right_final;
int got = WizardCaptureAxis(baseline, captures, capture_count, 1,
&right_device, &right_axis, &right_delta, &right_final);
if (got < 0)
{
printf("\nAborted - nothing written.\n");
printf("Press any key to continue into the game.\n");
_getch();
return 1;
}
if (got == 0)
{
printf(" skipped - no yaw control.\n\n");
}
else
{
float right_rest = baseline[right_device][right_axis];
printf(" -> device %d (%s) axis %s [moved %+.2f]\n",
right_device,
(RPJoyDevice(right_device) != NULL)
? RPJoyDevice(right_device)->name : "?",
JoyAxisToken(right_axis), right_delta);
Sleep(800); // let it come back to rest before we re-baseline
printf("YAW LEFT: now the other way - press the LEFT pedal, or\n"
" twist / push the stick LEFT ...\n");
WizardBaseline(baseline);
int left_device, left_axis;
float left_delta, left_final;
int got_left = WizardCaptureAxis(baseline, captures, capture_count,
1, &left_device, &left_axis, &left_delta, &left_final);
if (got_left < 0)
{
printf("\nAborted - nothing written.\n");
printf("Press any key to continue into the game.\n");
_getch();
return 1;
}
int same_axis = (got_left == 0) ||
(left_device == right_device && left_axis == right_axis);
if (got_left != 0)
{
printf(" -> device %d (%s) axis %s [moved %+.2f]\n",
left_device,
(RPJoyDevice(left_device) != NULL)
? RPJoyDevice(left_device)->name : "?",
JoyAxisToken(left_axis), left_delta);
}
if (same_axis)
{
//
// One axis, both ways: the signed composite, positive
// for the right pedal. Signed from the RIGHT answer,
// which is the one the convention is written in.
//
WizardCapture &capture = captures[capture_count++];
capture.used = 1;
capture.device = right_device;
capture.axis = right_axis;
capture.button = -1;
capture.invert = (right_delta < 0.0f);
sprintf(capture.line, "joyaxis %s axis Pedals%s deadzone 0.08",
JoyAxisToken(right_axis), capture.invert ? " invert" : "");
if (got_left == 0)
{
printf(" left skipped - taking %s as one control that\n"
" covers both ways.\n", JoyAxisToken(right_axis));
}
else if ((left_delta < 0.0f) == (right_delta < 0.0f))
{
//
// Both moves read the same way, which no single
// control does. Say so rather than write a row that
// turns one way only and let them wonder.
//
printf(" NOTE: both moves pushed %s the SAME way"
" (%+.2f then %+.2f).\n"
" Bound as one control anyway - check that line if"
" yaw only turns\n one way.\n",
JoyAxisToken(right_axis), right_delta, left_delta);
}
else
{
printf(" ONE axis both ways%s: bound as the pedal PAIR,\n"
" a twist grip or rudder bar working both pedals.\n",
capture.invert ? " (inverted)" : "");
}
}
else
{
//
// Two axes: the pod's own arrangement, one pedal per
// foot, so they bind to the real pair rather than to the
// composite that stands in for it.
//
WizardCapture &right_capture = captures[capture_count++];
right_capture.used = 1;
right_capture.device = right_device;
right_capture.axis = right_axis;
right_capture.button = -1;
WizardWritePedal(&right_capture, right_axis, right_rest,
right_delta, "RightPedal");
WizardCapture &left_capture = captures[capture_count++];
left_capture.used = 1;
left_capture.device = left_device;
left_capture.axis = left_axis;
left_capture.button = -1;
WizardWritePedal(&left_capture, left_axis,
baseline[left_device][left_axis], left_delta, "LeftPedal");
printf(" TWO axes: %s is the right pedal, %s the left - the\n"
" pod's own arrangement, and the game mixes them into"
" the turn.\n",
JoyAxisToken(right_axis), JoyAxisToken(left_axis));
}
printf("\n");
Sleep(800);
}
}
//---------------------------------------------------------------
// The throttle, which cannot be read the way everything else is. A
// lever sits wherever it was last left - halfway, or hard against
// the stop that happens to read +1 - so watching it move says
// nothing about which END means power. Nor can the wizard ask the
// player which end that is: nobody knows what their driver reports.
//
// So it asks for the one thing the player DOES know - where zero is
// - and takes the reading there. Everything after that follows: the
// direction it travels from a known idle is the direction that
// means open.
//---------------------------------------------------------------
{
printf("THROTTLE: set the lever to ZERO - idle, fully closed - and\n"
" press SPACE. Here SPACE means \"it is at zero now\",\n"
" not skip; press S if you have no throttle lever ...\n");
int have_throttle = 0;
for (;;)
{
int key = _getch();
if (key == 27)
{
printf("\nAborted - nothing written.\n");
printf("Press any key to continue into the game.\n");
_getch();
return 1;
}
if (key == ' ')
{
have_throttle = 1;
break;
}
if (key == 's' || key == 'S')
{
printf(" skipped - no throttle lever.\n\n");
break;
}
}
if (have_throttle)
{
printf(" reading zero ...\n");
WizardBaseline(baseline);
printf(" now OPEN the throttle to FULL"
" (SPACE to skip) ...\n");
int device, axis;
float delta, final_value;
int got = WizardCaptureAxis(baseline, captures, capture_count, 1,
&device, &axis, &delta, &final_value);
if (got < 0)
{
printf("\nAborted - nothing written.\n");
printf("Press any key to continue into the game.\n");
_getch();
return 1;
}
if (got == 0)
{
printf(" skipped.\n\n");
}
else
{
float idle = baseline[device][axis];
WizardCapture &capture = captures[capture_count++];
capture.used = 1;
capture.device = device;
capture.axis = axis;
capture.button = -1;
capture.invert = (delta < 0.0f);
sprintf(capture.line, "joyaxis %s axis Throttle%s deadzone 0",
JoyAxisToken(axis), capture.invert ? " invert" : "");
printf(" -> device %d (%s) axis %s%s"
" [zero at %+.2f, opened %+.2f]\n",
device,
(RPJoyDevice(device) != NULL) ? RPJoyDevice(device)->name : "?",
JoyAxisToken(axis), capture.invert ? " (inverted)" : "",
idle, delta);
if (idle > -0.5f && idle < 0.5f)
{
//
// Zero somewhere in the middle of the travel. The
// lever owns the channel outright, so its whole
// -1..1 range becomes 0-100% and an idle at the
// centre is half power. Worth saying plainly.
//
printf(" NOTE: your zero reads %+.2f rather than an end"
" stop, and a\n"
" throttle's FULL travel becomes the pod's 0-100%%"
" - so at that\n"
" position the pod would sit near half power. Use"
" the lever's\n"
" real closed stop, or edit that row by hand.\n",
idle);
}
printf("\n");
Sleep(800);
}
}
}
//
// The pod's stick-head buttons, at their RIO addresses.
//
+58 -17
View File
@@ -201,11 +201,15 @@ namespace
//---------------------------------------------------------------
// Shared tail of the two axis-source rows: [invert] [deadzone <d>]
// [rate <n>], in any order.
// [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 *invert, Scalar *deadzone, Scalar *rate,
Logical *lever = NULL)
{
for (int i = first; i < token_count; ++i)
{
@@ -213,6 +217,10 @@ namespace
{
*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))
@@ -355,12 +363,23 @@ namespace
{
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);
//
// 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;
}
//---------------------------------------------------------------
@@ -377,13 +396,19 @@ namespace
{
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);
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"))
@@ -453,7 +478,7 @@ namespace
"# 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"
"# 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"
@@ -462,7 +487,10 @@ namespace
"# <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"
"# 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"
@@ -493,6 +521,12 @@ namespace
"# 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"
@@ -500,6 +534,13 @@ namespace
"# 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"
+9 -1
View File
@@ -19,7 +19,7 @@
// pad <button> button <addr> [toggle]
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
// joydev <slot> [product-name substring...]
// joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
// joyaxis <src> axis <axis> [invert] [lever] [deadzone <d>] [rate <n>]
// joybutton <n> button <addr> [toggle]
// joyhat <n> <up|down|left|right> button <addr>
//
@@ -37,6 +37,13 @@
// layout - X Y Z RX RY RZ SL0 SL1 - where a twist grip is usually RZ
// and a HOTAS throttle usually Z or SL0. RP412JOYCONFIG=1 writes these
// rows for you by asking the player to move each control.
//
// 'lever' says the source is a one-way control that rests at one end of
// its travel rather than in the middle - a floor pedal, a throttle
// slider. DirectInput reports it as a full -1..1 axis all the same, so
// without the keyword half the travel sits below zero and the first
// half of the press does nothing. It maps that travel onto 0..1, and
// the deadzone then measures from the released end instead of centre.
//########################################################################
enum PadBindRioAxis
@@ -136,6 +143,7 @@ struct PadJoyAxisBinding
int source; // PadBindJoyAxis
int axis; // PadBindRioAxis
Logical invert;
Logical lever; // rests at one end: -1..1 travel means 0..1
Scalar deadzone; // normalized 0..1
Scalar rate; // 0 = direct position, >0 = speed integrate
};
+22 -1
View File
@@ -65,6 +65,25 @@ namespace
return value;
}
//
// A one-way control - a floor pedal, a slider - rests at one END of
// its travel, not in the middle, and DirectInput still reports it as
// a full -1..1 axis. Fold that travel onto 0..1 so the pedal starts
// answering as soon as it moves instead of at half depression, and
// measure the deadzone from the released end, where the slack in a
// tired return spring actually lives.
//
Scalar JoyLeverValue(Scalar raw, Scalar deadzone)
{
Scalar value = (raw + 1.0f) * 0.5f;
if (value <= deadzone)
{
return (Scalar) 0;
}
if (value > 1.0f) value = 1.0f;
return value;
}
//
// A POV hat reports centidegrees clockwise from up, or -1 centered.
// The 45-degree window each way is what makes the diagonals press
@@ -686,7 +705,9 @@ void
throttleLever = True;
continue;
}
Scalar value = JoyAxisValue(raw, binding->deadzone);
Scalar value = binding->lever
? JoyLeverValue(raw, binding->deadzone)
: JoyAxisValue(raw, binding->deadzone);
if (binding->rate > 0.0f)
{
rate[binding->axis] += value * binding->rate;
+45 -11
View File
@@ -163,15 +163,34 @@ it off.
---
## Flight sticks, HOTAS and rudder pedals
## Flight sticks, HOTAS and pedals
Anything that is not an Xbox-class pad — a flight stick, a HOTAS
throttle, a twist grip, rudder pedals, a wheel — comes in through
DirectInput rather than XInput, and needs to be told which axis is
which. **Run `joyconfig.bat` once.** It asks you to move each control in
turn, works out which device and axis you moved and which way round it
reads, and writes the joystick rows of `bindings.txt`. Then it carries
on into the setup screen so you can try them straight away.
throttle, a twist grip, rudder or racing pedals, a wheel — comes in
through DirectInput rather than XInput, and needs to be told which axis
is which. **Run `joyconfig.bat` once.** It asks you to move each control
in turn, works out which device and axis you moved and which way round
it reads, and writes the joystick rows of `bindings.txt`. Then it
carries on into the setup screen so you can try them straight away.
You are never asked what you own. Two of the controls can't simply be
watched, though, 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, rudder pedals their driver
has already mixed, racing pedals in combined mode. Two different axes
are two real pedals, one per foot, which is what the pod itself had, and
they bind to the pod's own pair so the game does the mixing — both at
once then does what both at once did in the pod. Either way you just
move the thing twice; the wizard works out the shape.
**The throttle is set to zero first.** A lever sits wherever it was last
left — halfway, or hard against the stop that happens to read `+1` — so
watching it move says nothing about which end means power. The wizard
asks you to close it, press SPACE to say so, and *then* open it: the
direction it travels from a known idle is the direction that means
throttle. (Press `S` there if you have no throttle lever.)
It only writes its own section, between two marker lines, so anything
you have edited yourself survives re-running it. Xbox-class controllers
@@ -189,15 +208,30 @@ joybutton 0 button 0x40 # trigger
joyhat 0 up button 0x42
```
Two separate pedals bind to the pod's real pair instead, usually on a
device of their own:
```
joydev 1 Pedals
joyaxis Y axis LeftPedal lever deadzone 0.05
joyaxis RZ axis RightPedal lever deadzone 0.05
```
Axis names are DirectInput's — `X Y Z RX RY RZ SL0 SL1` — where a twist
grip is usually `RZ` and a HOTAS throttle usually `Z` or `SL0`.
Two things are worth knowing. `Pedals` is a signed axis that drives the
Three things are worth knowing. `Pedals` is a signed axis that drives the
pedal *pair*, positive for the right pedal and negative for the left, so
one rudder bar or twist grip works both the way a foot never could. And
a `joyaxis` on `Throttle` with no `rate` is treated as a real lever: its
one rudder bar or twist grip works both the way a foot never could. A
`joyaxis` on `Throttle` with no `rate` is treated as a real lever: its
full travel *is* the throttle range, rather than nudging a position the
way a spring-centred pad stick has to.
way a spring-centred pad stick has to. And `lever` says the same thing
about any other one-way control — a floor pedal rests at one *end* of
its travel, not in the middle, but Windows still reports it as a full
1..+1 axis, so without the word the first half of the press does
nothing. With it, the travel maps onto the channel's 0..1 and the
deadzone measures from the released end, where a tired return spring's
slack actually lives. Add or remove `invert` if a pedal reads backwards.
## Rebinding
+48 -2
View File
@@ -1183,6 +1183,10 @@
<dt>It watches</dt>
<dd>Which way<small>the direction you moved sets the sign</small></dd>
</div>
<div>
<dt>And which</dt>
<dd>Yaw twice<small>right then left: one axis or two?</small></dd>
</div>
<div>
<dt>Then</dt>
<dd>Straight in<small>the game carries on to the setup screen</small></dd>
@@ -1199,6 +1203,23 @@
answer off the movement. Skip a prompt with <code>SPACE</code> if you
have no such control, <code>ESC</code> to abort without writing.
</p>
<p>
It goes further than the sign: it works out the <b>shape</b> of your
rig the same way. <b>Yaw is asked for twice</b>, right and then left,
and which axis answers is the measurement — the same axis both times
is one control covering both ways (a twist grip, a rudder bar, pedals
their driver has already mixed), two different axes are two real
pedals, one per foot. You just move the thing twice.
</p>
<p>
The <b>throttle</b> is the one prompt that works backwards, because a
lever sits wherever it was left: halfway, or hard against the stop
that happens to read <span class="mono">+1</span>. Watching it move
cannot tell you which end means power. So close it, press
<code>SPACE</code> to say <i>it is at zero now</i>, and then open it —
the travel from a known idle is the direction that means throttle.
<code>S</code> there if you have no lever at all.
</p>
</div>
<p class="sub">
@@ -1223,7 +1244,7 @@
<pre><i># grammar</i>
<b>joydev</b> &lt;slot&gt; [product-name substring]
<b>joyaxis</b> &lt;src&gt; <b>axis</b> &lt;axis&gt; [invert]
<b>joyaxis</b> &lt;src&gt; <b>axis</b> &lt;axis&gt; [invert] [lever]
[deadzone &lt;d&gt;] [rate &lt;n&gt;]
<b>joybutton</b> &lt;n&gt; <b>button</b> &lt;addr&gt; [toggle]
<b>joyhat</b> &lt;n&gt; &lt;up|down|left|right&gt;
@@ -1235,7 +1256,7 @@
<div class="tbl-scroll" style="margin-top:22px">
<table>
<caption>Two rules the pod's shape asks for</caption>
<caption>Three rules the pod's shape asks for</caption>
<tbody>
<tr>
<th class="mono">Pedals</th>
@@ -1246,6 +1267,31 @@
that presses one or the other and never both, which is exactly
what this says. It is a channel name like any other, so a gamepad
stick can drive the turn with it too.
<br><br>
Two real pedals — racing pedals, or rudder pedals with an axis
per foot — bind to
<span class="mono">LeftPedal</span> and
<span class="mono">RightPedal</span> one axis each instead, and
the wizard picks that up on its own from the two yaw prompts
answering on two different axes. That is the pod's own
arrangement: the game mixes your two feet into the turn rather
than taking a turn already mixed for it, and both pedals at once
does what both pedals at once did in the pod.
</td>
</tr>
<tr>
<th class="mono">lever</th>
<td>
A pedal rests at one <b>end</b> of its travel, not in the middle,
and Windows reports it as a full
<span class="mono">1..+1</span> axis regardless — so half the
travel reads below zero and the first half of the press would do
nothing at all. <span class="mono">lever</span> says the control
is one-way: the travel maps onto the channel's
<span class="mono">0..1</span>, and the deadzone then measures
from the released end, where the slack in a tired return spring
actually lives. The wizard writes it for you; add or remove
<span class="mono">invert</span> if a pedal reads backwards.
</td>
</tr>
<tr>
+21 -10
View File
@@ -220,12 +220,18 @@ start rpl4opt.exe -fit
Set-Content -Path "$dist\joyconfig.bat" -Encoding ascii -Value @"
@echo off
rem Red Planet 4.12 - one-time JOYSTICK / HOTAS / rudder-pedal setup.
rem A console wizard asks you to move each control (stick, twist or
rem rudder, throttle lever, fire buttons); it works out what you moved
rem and which way it reads, and writes the joystick section of
rem bindings.txt. When it finishes the game carries on into the setup
rem screen so you can try the bindings straight away.
rem Red Planet 4.12 - one-time JOYSTICK / HOTAS / PEDAL setup.
rem A console wizard asks you to move each control (stick, yaw, throttle
rem lever, fire buttons); it works out what you moved and which way it
rem reads, and writes the joystick section of bindings.txt. When it
rem finishes the game carries on into the setup screen so you can try
rem the bindings straight away.
rem You are never asked what you own. Yaw is asked for twice, right then
rem left, and which axis answers tells it what you have: one axis both
rem ways is a twist grip or rudder bar, two axes are two real pedals,
rem one per foot, the way the pod itself was. The throttle asks you to
rem close it and press SPACE first, since a lever sits wherever it was
rem left and no amount of watching says which end means power.
rem Xbox-class controllers need NO setup - they work out of the box.
rem Re-run this any time to redo the bindings; anything you have edited
rem yourself in bindings.txt is kept.
@@ -243,10 +249,15 @@ Run start-fullscreen.bat for borderless over the whole monitor, or
start-windowed.bat to keep a title bar. No cockpit hardware needed.
If there is no sound, run oalinst.exe once.
Got a flight stick, HOTAS or rudder pedals? Run joyconfig.bat once. It
asks you to move each control, works out which axis you moved and which
way round it reads, and writes the joystick rows of bindings.txt. Xbox-
class controllers need none of that - they work out of the box.
Got a flight stick, HOTAS or pedals? Run joyconfig.bat once. It asks
you to move each control, works out which axis you moved and which way
round it reads, and writes the joystick rows of bindings.txt. It never
asks what you own: yaw is asked for twice, right then left, and one
axis answering both ways is a twist grip or rudder bar while two axes
are two real pedals, one per foot, as the pod itself had. The throttle
asks you to close it and press SPACE before you open it, since a lever
sits wherever it was left. Xbox-class controllers need none of that -
they work out of the box.
Controls (XInput controller and/or keyboard) - EVERY input is
rebindable: edit bindings.txt beside the exe (written with the full