Files
RP412/MUNGA_L4/L4JOY.cpp
T
Cyd d35af59136 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.
2026-08-07 16:31:40 -05:00

1393 lines
38 KiB
C++

#include "mungal4.h"
#pragma hdrstop
//########################################################################
// L4JOY - the generic-joystick reader. Design and device model in
// L4JOY.h. Consumed by the PadRIO poll through the joydev / joyaxis /
// joybutton / joyhat bindings (L4PADBINDINGS / L4PADRIO).
//########################################################################
#include "l4joy.h"
#define DIRECTINPUT_VERSION 0x0800
#include <windows.h>
#include <dinput.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//########################################################################
// State
//########################################################################
namespace
{
struct JoyDevice
{
IDirectInputDevice8A *device;
RPJoyDeviceState state;
//
// Per-axis calibrated range. DIPROP_RANGE is set to +-32767 when
// the device is opened, but a driver may refuse, so normalization
// uses what the device actually reports.
//
LONG axisMin[joyAxisCount];
LONG axisMax[joyAxisCount];
};
IDirectInput8A *gDirectInput = NULL;
JoyDevice gDevices[joyMaxDevices];
int gDeviceCount = 0;
int gInitialized = 0;
unsigned long gLastProbeTick = 0;
int JoyLogEnabled()
{
static int log = -1;
if (log < 0)
{
const char *value = getenv("RP412JOYLOG");
log = (value != NULL && *value != '0') ? 1 : 0;
}
return log;
}
//###################################################################
// XInput-device exclusion.
//
// The documented WMI-free method: every XInput-capable device's
// RawInput device path carries the "IG_" marker. Collect the VID/PID
// of each such path once per enumeration pass and skip any
// DirectInput device whose guidProduct matches - DI packs VID in the
// low word and PID in the high word of guidProduct.Data1.
//
// Without this an Xbox pad arrives through both APIs and every
// button counts twice.
//###################################################################
enum { xinputVidPidMax = 16 };
unsigned long gXInputVidPid[xinputVidPidMax];
int gXInputVidPidCount = 0;
void CollectXInputVidPids()
{
gXInputVidPidCount = 0;
UINT device_count = 0;
if (GetRawInputDeviceList(NULL, &device_count,
sizeof(RAWINPUTDEVICELIST)) != 0 || device_count == 0)
{
return;
}
RAWINPUTDEVICELIST *list = (RAWINPUTDEVICELIST *)
malloc(device_count * sizeof(RAWINPUTDEVICELIST));
if (list == NULL)
{
return;
}
device_count = GetRawInputDeviceList(list, &device_count,
sizeof(RAWINPUTDEVICELIST));
if (device_count == (UINT) -1)
{
free(list);
return;
}
for (UINT i = 0; i < device_count; ++i)
{
if (list[i].dwType != RIM_TYPEHID)
{
continue;
}
char path[256];
UINT size = sizeof(path);
if (GetRawInputDeviceInfoA(list[i].hDevice, RIDI_DEVICENAME,
path, &size) == (UINT) -1)
{
continue;
}
path[sizeof(path) - 1] = '\0';
if (strstr(path, "IG_") == NULL && strstr(path, "ig_") == NULL)
{
continue;
}
//
// Parse "...VID_045E&PID_028E..." - case varies by driver.
//
const char *v = strstr(path, "VID_");
if (v == NULL) v = strstr(path, "vid_");
const char *p = strstr(path, "PID_");
if (p == NULL) p = strstr(path, "pid_");
if (v == NULL || p == NULL)
{
continue;
}
unsigned vid = (unsigned) strtoul(v + 4, NULL, 16);
unsigned pid = (unsigned) strtoul(p + 4, NULL, 16);
if (gXInputVidPidCount < xinputVidPidMax)
{
gXInputVidPid[gXInputVidPidCount++] =
((unsigned long) pid << 16) | vid;
}
}
free(list);
}
int IsXInputProduct(const GUID &guid_product)
{
for (int i = 0; i < gXInputVidPidCount; ++i)
{
if (gXInputVidPid[i] == (unsigned long) guid_product.Data1)
{
return 1;
}
}
return 0;
}
//###################################################################
// Device open
//###################################################################
//
// A window handle of THIS process for SetCooperativeLevel. BACKGROUND
// + NONEXCLUSIVE matches the XInput model: the device stays readable
// while the game runs, and PadRIO's own rules remain the arbiter of
// what acts on it.
//
BOOL CALLBACK FindProcessWindowCallback(HWND hwnd, LPARAM lparam)
{
DWORD process_id = 0;
GetWindowThreadProcessId(hwnd, &process_id);
if (process_id == GetCurrentProcessId())
{
*(HWND *) lparam = hwnd;
return FALSE;
}
return TRUE;
}
HWND FindProcessWindow()
{
HWND hwnd = NULL;
EnumWindows(FindProcessWindowCallback, (LPARAM) &hwnd);
return (hwnd != NULL) ? hwnd : GetDesktopWindow();
}
BOOL CALLBACK SetAxisRangeCallback(
LPCDIDEVICEOBJECTINSTANCEA object, LPVOID context)
{
IDirectInputDevice8A *device = (IDirectInputDevice8A *) context;
DIPROPRANGE range;
range.diph.dwSize = sizeof(DIPROPRANGE);
range.diph.dwHeaderSize = sizeof(DIPROPHEADER);
range.diph.dwHow = DIPH_BYID;
range.diph.dwObj = object->dwType;
range.lMin = -32767;
range.lMax = 32767;
device->SetProperty(DIPROP_RANGE, &range.diph); // best effort
return DIENUM_CONTINUE;
}
BOOL CALLBACK EnumDevicesCallback(
const DIDEVICEINSTANCEA *instance, VOID *context)
{
HWND owner = *(HWND *) context;
if (gDeviceCount >= joyMaxDevices)
{
return DIENUM_STOP;
}
if (IsXInputProduct(instance->guidProduct))
{
if (JoyLogEnabled())
{
DEBUG_STREAM << "Joy: skipping XInput-class device \""
<< instance->tszProductName << "\"\n" << std::flush;
}
return DIENUM_CONTINUE;
}
IDirectInputDevice8A *device = NULL;
if (FAILED(gDirectInput->CreateDevice(instance->guidInstance,
&device, NULL)) || device == NULL)
{
return DIENUM_CONTINUE;
}
if (FAILED(device->SetDataFormat(&c_dfDIJoystick2)))
{
device->Release();
return DIENUM_CONTINUE;
}
//
// Best effort: some environments reject a cooperative level on
// the desktop window, and the default still polls.
//
device->SetCooperativeLevel(owner,
DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
device->EnumObjects(SetAxisRangeCallback, device, DIDFT_AXIS);
device->Acquire();
JoyDevice &slot = gDevices[gDeviceCount];
memset(&slot, 0, sizeof(slot));
slot.device = device;
slot.state.attached = 1;
strncpy(slot.state.name, instance->tszProductName,
sizeof(slot.state.name) - 1);
for (int a = 0; a < joyAxisCount; ++a)
{
slot.axisMin[a] = -32767;
slot.axisMax[a] = 32767;
}
for (int h = 0; h < joyHatCount; ++h)
{
slot.state.hat[h] = -1;
}
++gDeviceCount;
DEBUG_STREAM << "Joy: device " << (gDeviceCount - 1) << ": \""
<< slot.state.name << "\" attached\n" << std::flush;
return DIENUM_CONTINUE;
}
void ReleaseAllDevices()
{
for (int i = 0; i < gDeviceCount; ++i)
{
if (gDevices[i].device != NULL)
{
gDevices[i].device->Unacquire();
gDevices[i].device->Release();
gDevices[i].device = NULL;
}
gDevices[i].state.attached = 0;
}
gDeviceCount = 0;
}
void Enumerate()
{
ReleaseAllDevices();
CollectXInputVidPids();
HWND owner = FindProcessWindow();
gDirectInput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumDevicesCallback,
&owner, DIEDFL_ATTACHEDONLY);
}
float NormalizeAxis(LONG value, LONG range_min, LONG range_max)
{
if (range_max <= range_min)
{
return 0.0f;
}
float normalized =
((float)(value - range_min) / (float)(range_max - range_min))
* 2.0f - 1.0f;
if (normalized < -1.0f) normalized = -1.0f;
if (normalized > 1.0f) normalized = 1.0f;
return normalized;
}
}
//########################################################################
// Public surface
//########################################################################
int
RPJoyInit(void)
{
if (gInitialized)
{
return gDeviceCount;
}
gInitialized = 1;
if (FAILED(DirectInput8Create(GetModuleHandleA(NULL), DIRECTINPUT_VERSION,
IID_IDirectInput8A, (void **) &gDirectInput, NULL))
|| gDirectInput == NULL)
{
gDirectInput = NULL;
DEBUG_STREAM << "Joy: DirectInput8Create failed - generic joysticks "
<< "unavailable\n" << std::flush;
return 0;
}
Enumerate();
return gDeviceCount;
}
void
RPJoyShutdown(void)
{
ReleaseAllDevices();
if (gDirectInput != NULL)
{
gDirectInput->Release();
gDirectInput = NULL;
}
gInitialized = 0;
}
void
RPJoyPoll(void)
{
if (!gInitialized)
{
RPJoyInit();
}
if (gDirectInput == NULL)
{
return;
}
//
// Hot-plug: when nothing is attached, re-enumerate on the same ~3 s
// cadence PadRIO uses to look for a pad. Enumeration is far too heavy
// to run every frame.
//
int any_attached = 0;
for (int i = 0; i < gDeviceCount; ++i)
{
if (gDevices[i].state.attached)
{
any_attached = 1;
break;
}
}
if (!any_attached)
{
unsigned long now = GetTickCount();
if (gLastProbeTick != 0 && now - gLastProbeTick < 3000)
{
return;
}
gLastProbeTick = now;
Enumerate();
}
for (int i = 0; i < gDeviceCount; ++i)
{
JoyDevice &slot = gDevices[i];
if (slot.device == NULL || !slot.state.attached)
{
continue;
}
HRESULT result = slot.device->Poll();
if (FAILED(result))
{
result = slot.device->Acquire();
if (SUCCEEDED(result))
{
result = slot.device->Poll();
}
}
DIJOYSTATE2 joystate;
if (SUCCEEDED(result) || result == DI_NOEFFECT)
{
result = slot.device->GetDeviceState(sizeof(joystate), &joystate);
}
if (FAILED(result))
{
//
// Unplugged, or the driver died. Mark it detached and zero
// the state: the binding layer then sees everything released
// rather than holding whatever was pressed at the moment the
// device went away.
//
DEBUG_STREAM << "Joy: device " << i << " (\"" << slot.state.name
<< "\") lost\n" << std::flush;
slot.state.attached = 0;
memset(slot.state.axis, 0, sizeof(slot.state.axis));
slot.state.buttons = 0;
for (int h = 0; h < joyHatCount; ++h)
{
slot.state.hat[h] = -1;
}
continue;
}
LONG raw[joyAxisCount];
raw[0] = joystate.lX;
raw[1] = joystate.lY;
raw[2] = joystate.lZ;
raw[3] = joystate.lRx;
raw[4] = joystate.lRy;
raw[5] = joystate.lRz;
raw[6] = joystate.rglSlider[0];
raw[7] = joystate.rglSlider[1];
for (int a = 0; a < joyAxisCount; ++a)
{
slot.state.axis[a] =
NormalizeAxis(raw[a], slot.axisMin[a], slot.axisMax[a]);
}
slot.state.buttons = 0;
for (int b = 0; b < joyButtonCount; ++b)
{
if (joystate.rgbButtons[b] & 0x80)
{
slot.state.buttons |= (1u << b);
}
}
for (int h = 0; h < joyHatCount; ++h)
{
DWORD pov = joystate.rgdwPOV[h];
//
// Centered reads as 0xFFFF in the low word per the DI
// contract; some drivers return the full 0xFFFFFFFF.
//
slot.state.hat[h] = (LOWORD(pov) == 0xFFFF) ? -1 : (int) pov;
}
}
}
int
RPJoyDeviceCount(void)
{
return gDeviceCount;
}
const RPJoyDeviceState *
RPJoyDevice(int index)
{
if (index < 0 || index >= gDeviceCount || !gDevices[index].state.attached)
{
return NULL;
}
return &gDevices[index].state;
}
int
RPJoyFindDevice(const char *name_substring)
{
if (name_substring == NULL || *name_substring == '\0')
{
return -1;
}
char want[64];
strncpy(want, name_substring, sizeof(want) - 1);
want[sizeof(want) - 1] = '\0';
_strlwr(want);
for (int i = 0; i < gDeviceCount; ++i)
{
if (!gDevices[i].state.attached)
{
continue;
}
char have[64];
strncpy(have, gDevices[i].state.name, sizeof(have) - 1);
have[sizeof(have) - 1] = '\0';
_strlwr(have);
if (strstr(have, want) != NULL)
{
return i;
}
}
return -1;
}
//########################################################################
//########################### RP412JOYCONFIG #############################
//########################################################################
//
// The interactive capture wizard.
//
// Console UI - the game is a GUI app, so it allocates one. It detects
// which device and axis the player moves for each pod control, derives
// the sign convention from the DIRECTION of the move, then writes the
// joystick section of bindings.txt between marker lines. Everything
// outside the markers is preserved byte for byte, so a player's own
// keyboard and pad edits survive re-running it.
//
// Deriving the sign from the move is the point: a stick that reads
// positive when pushed right and one that reads negative are equally
// 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;
int device;
int axis; // axis index, -1 for buttons
int button; // button index, -1 for axes
int invert;
char line[128];
};
const char *JoyAxisToken(int axis)
{
static const char *names[joyAxisCount] =
{ "X", "Y", "Z", "RX", "RY", "RZ", "SL0", "SL1" };
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])
{
//
// ~600 ms of samples gives the at-rest position of every axis. A
// HOTAS throttle rests wherever its lever was left, so an
// assumed zero would read as a huge deflection.
//
for (int pass = 0; pass < 20; ++pass)
{
RPJoyPoll();
Sleep(30);
}
for (int d = 0; d < joyMaxDevices; ++d)
{
const RPJoyDeviceState *state = RPJoyDevice(d);
for (int a = 0; a < joyAxisCount; ++a)
{
baseline[d][a] = (state != NULL) ? state->axis[a] : 0.0f;
}
}
}
//
// Wait for a decisive axis move. Returns 1 with the outputs filled,
// 0 on skip (SPACE), -1 on abort (ESC) or timeout.
//
int WizardCaptureAxis(
const float baseline[joyMaxDevices][joyAxisCount],
const WizardCapture *taken, int taken_count,
int allow_skip,
int *out_device, int *out_axis, float *out_delta, float *out_final)
{
unsigned long deadline = GetTickCount() + 30000;
unsigned long hold_since = 0;
int candidate_device = -1, candidate_axis = -1;
while (GetTickCount() < deadline)
{
while (_kbhit())
{
int key = _getch();
if (key == 27)
{
return -1;
}
if (key == ' ' && allow_skip)
{
return 0;
}
}
RPJoyPoll();
int best_device = -1, best_axis = -1;
float best_magnitude = 0.0f, best_delta = 0.0f;
for (int d = 0; d < joyMaxDevices; ++d)
{
const RPJoyDeviceState *state = RPJoyDevice(d);
if (state == NULL)
{
continue;
}
for (int a = 0; a < joyAxisCount; ++a)
{
//
// An axis already claimed cannot be claimed again -
// otherwise one twitchy axis wins every prompt.
//
int already = 0;
for (int t = 0; t < taken_count; ++t)
{
if (taken[t].used && taken[t].axis >= 0 &&
taken[t].device == d && taken[t].axis == a)
{
already = 1;
}
}
if (already)
{
continue;
}
float delta = state->axis[a] - baseline[d][a];
float magnitude = (delta < 0.0f) ? -delta : delta;
if (magnitude > best_magnitude)
{
best_magnitude = magnitude;
best_delta = delta;
best_device = d;
best_axis = a;
}
}
}
//
// Nearly half of full travel, held for a quarter second: big
// enough that a resting hand or a noisy pot cannot trip it,
// and the hold means a knocked stick passing through does
// not either.
//
if (best_magnitude > 0.45f)
{
if (candidate_device != best_device || candidate_axis != best_axis)
{
candidate_device = best_device;
candidate_axis = best_axis;
hold_since = GetTickCount();
}
else if (GetTickCount() - hold_since > 250)
{
const RPJoyDeviceState *state = RPJoyDevice(best_device);
*out_device = best_device;
*out_axis = best_axis;
*out_delta = best_delta;
*out_final = (state != NULL)
? state->axis[best_axis] : best_delta;
return 1;
}
}
else
{
candidate_device = candidate_axis = -1;
}
Sleep(15);
}
return -1;
}
//
// Wait for a fresh button press. Returns 1 with the outputs filled,
// 0 on skip, -1 on abort or timeout.
//
int WizardCaptureButton(int allow_skip, int *out_device, int *out_button)
{
//
// Baseline the buttons already held, so a trigger squeezed since
// the last prompt does not answer this one by itself.
//
unsigned baseline[joyMaxDevices];
RPJoyPoll();
for (int d = 0; d < joyMaxDevices; ++d)
{
const RPJoyDeviceState *state = RPJoyDevice(d);
baseline[d] = (state != NULL) ? state->buttons : 0;
}
unsigned long deadline = GetTickCount() + 30000;
while (GetTickCount() < deadline)
{
while (_kbhit())
{
int key = _getch();
if (key == 27)
{
return -1;
}
if (key == ' ' && allow_skip)
{
return 0;
}
}
RPJoyPoll();
for (int d = 0; d < joyMaxDevices; ++d)
{
const RPJoyDeviceState *state = RPJoyDevice(d);
if (state == NULL)
{
continue;
}
unsigned fresh = state->buttons & ~baseline[d];
if (fresh != 0)
{
int button = 0;
while ((fresh & 1u) == 0)
{
fresh >>= 1;
++button;
}
*out_device = d;
*out_button = button;
return 1;
}
baseline[d] &= state->buttons; // releases refresh it
}
Sleep(15);
}
return -1;
}
}
int
RPJoyConfigWizard(void)
{
//
// The game is a GUI-subsystem app and has no console of its own.
//
if (GetConsoleWindow() == NULL)
{
AllocConsole();
}
FILE *io;
freopen_s(&io, "CONOUT$", "w", stdout);
freopen_s(&io, "CONIN$", "r", stdin);
printf("\n=== Red Planet joystick setup (RP412JOYCONFIG) ===\n\n");
//
// Make sure bindings.txt exists before we start: the wizard only
// writes its own section, and the keyboard and pad rows come from
// the default write.
//
{
PadBindingProfile ensure_default;
PadBindings_Load(&ensure_default);
}
int xinput_slot = WizardXInputSlot();
if (RPJoyInit() == 0)
{
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;
}
//
// Let the devices settle and report where every axis is sitting.
// Worth printing rather than assuming: a driver that refuses the
// +-32767 range we ask for reports its own, and an axis then rests
// 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".
//
{
float settle[joyMaxDevices][joyAxisCount];
WizardBaseline(settle);
printf("Detected devices (axes at rest):\n");
for (int d = 0; d < RPJoyDeviceCount(); ++d)
{
const RPJoyDeviceState *state = RPJoyDevice(d);
if (state == NULL)
{
continue;
}
printf(" [%d] %s\n ", d, state->name);
for (int a = 0; a < joyAxisCount; ++a)
{
printf("%s %+.2f ", JoyAxisToken(a), settle[d][a]);
}
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);
}
}
WizardCapture captures[16];
memset(captures, 0, sizeof(captures));
int capture_count = 0;
//
// 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
//
// Yaw and the throttle are not this simple and are asked for below.
//
struct AxisStep
{
const char *prompt;
const char *channel;
int wants_negative;
};
static const AxisStep axisSteps[] =
{
{ "STEER: push the STICK / turn the WHEEL fully RIGHT",
"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 }
};
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)
{
printf("%s ...\n", axisSteps[s].prompt);
WizardBaseline(baseline);
int device, axis;
float delta, final_value;
int got = WizardCaptureAxis(baseline, captures, capture_count,
0, &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;
}
WizardCapture &capture = captures[capture_count++];
capture.used = 1;
capture.device = device;
capture.axis = axis;
capture.button = -1;
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
// the wrong control answered can see what the wizard saw.
//
printf(" -> device %d (%s) axis %s%s [moved %+.2f, now %+.2f]\n\n",
device,
(RPJoyDevice(device) != NULL) ? RPJoyDevice(device)->name : "?",
JoyAxisToken(axis), capture.invert ? " (inverted)" : "",
delta, final_value);
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.
//
struct ButtonStep
{
const char *prompt;
int address;
int allow_skip;
};
static const ButtonStep buttonSteps[] =
{
{ "MAIN: press the TRIGGER", 0x40, 0 },
{ "MIDDLE: press your second fire button", 0x46, 1 },
{ "UPPER: press your third fire button", 0x47, 1 },
{ "PINKY: press your fourth fire button", 0x45, 1 },
{ "REVERSE: press the button for reverse thrust", 0x3F, 1 },
{ "PANIC: press the button for the panic stop", 0x3D, 1 }
};
for (int s = 0; s < (int)(sizeof(buttonSteps) / sizeof(buttonSteps[0])); ++s)
{
printf("%s ... %s\n", buttonSteps[s].prompt,
buttonSteps[s].allow_skip ? "(SPACE to skip)" : "");
int device, button;
int got = WizardCaptureButton(buttonSteps[s].allow_skip,
&device, &button);
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");
continue;
}
WizardCapture &capture = captures[capture_count++];
capture.used = 1;
capture.device = device;
capture.axis = -1;
capture.button = button;
sprintf(capture.line, "joybutton %d button 0x%02X",
button, buttonSteps[s].address);
printf(" -> device %d button %d\n\n", device, button);
Sleep(400);
}
if (capture_count == 0)
{
printf("Nothing captured - nothing written.\n");
printf("Press any key to continue into the game.\n");
_getch();
return 1;
}
//
// Group by device into joydev slots, in order of first use. The hat
// on the first device gets the pod's look cluster automatically -
// hats are standardized, so there is nothing to ask.
//
int slot_of_device[joyMaxDevices];
int slot_count = 0;
for (int d = 0; d < joyMaxDevices; ++d)
{
slot_of_device[d] = -1;
}
for (int i = 0; i < capture_count; ++i)
{
if (captures[i].used && slot_of_device[captures[i].device] < 0)
{
slot_of_device[captures[i].device] = slot_count++;
}
}
//
// Rewrite bindings.txt, preserving everything outside the markers.
//
static const char *beginMarker =
"# >>> RP412JOYCONFIG generated - do not edit between the markers";
static const char *endMarker = "# <<< RP412JOYCONFIG end";
static char kept[65536];
kept[0] = '\0';
{
FILE *in = fopen("bindings.txt", "rt");
if (in != NULL)
{
char line[512];
int inside = 0;
size_t used = 0;
while (fgets(line, sizeof(line), in) != NULL)
{
if (strstr(line, "RP412JOYCONFIG generated") != NULL)
{
inside = 1;
continue;
}
if (strstr(line, "RP412JOYCONFIG end") != NULL)
{
inside = 0;
continue;
}
if (!inside && used + strlen(line) < sizeof(kept) - 1)
{
strcpy(kept + used, line);
used += strlen(line);
}
}
fclose(in);
}
}
FILE *out = fopen("bindings.txt", "wt");
if (out == NULL)
{
printf("ERROR: cannot write bindings.txt (wrong working directory?)\n");
printf("Press any key to continue into the game.\n");
_getch();
return 1;
}
fputs(kept, out);
if (kept[0] != '\0' && kept[strlen(kept) - 1] != '\n')
{
fputs("\n", out);
}
fprintf(out, "%s\n", beginMarker);
int hat_done = 0;
for (int d = 0; d < joyMaxDevices; ++d)
{
if (slot_of_device[d] < 0)
{
continue;
}
const RPJoyDeviceState *state = RPJoyDevice(d);
fprintf(out, "joydev %d %s\n", slot_of_device[d],
(state != NULL) ? state->name : "");
for (int i = 0; i < capture_count; ++i)
{
if (captures[i].used && captures[i].device == d)
{
fprintf(out, "%s\n", captures[i].line);
}
}
if (!hat_done)
{
hat_done = 1;
fprintf(out, "joyhat 0 up button 0x42\n");
fprintf(out, "joyhat 0 down button 0x41\n");
fprintf(out, "joyhat 0 left button 0x44\n");
fprintf(out, "joyhat 0 right button 0x43\n");
}
}
fprintf(out, "%s\n", endMarker);
fclose(out);
printf("bindings.txt written (%d controls + hat looks).\n", capture_count);
printf("The game uses them from here on - have fun.\n\n");
printf("Press any key to continue into the game.\n");
_getch();
return 0;
}