diff --git a/MUNGA_L4/L4JOY.cpp b/MUNGA_L4/L4JOY.cpp index 353fd5c..d914499 100644 --- a/MUNGA_L4/L4JOY.cpp +++ b/MUNGA_L4/L4JOY.cpp @@ -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 +#include #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. // diff --git a/MUNGA_L4/L4PADBINDINGS.cpp b/MUNGA_L4/L4PADBINDINGS.cpp index 4b3399d..c633c51 100644 --- a/MUNGA_L4/L4PADBINDINGS.cpp +++ b/MUNGA_L4/L4PADBINDINGS.cpp @@ -201,11 +201,15 @@ namespace //--------------------------------------------------------------- // Shared tail of the two axis-source rows: [invert] [deadzone ] - // [rate ], in any order. + // [rate ], 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