Ported from BT411, which needed the same thing for its glass cockpit. PadRIO reads XInput, which covers Xbox-class pads and nothing else. A flight stick, a HOTAS throttle, a twist grip, rudder pedals or a wheel arrive through DirectInput instead, and until now the game could not see any of them - the only generic-joystick path left was the 1995 single- device DIJoystick behind L4CONTROLS=DIJOYSTICK, which is untouched here. L4JOY is the reader: up to four devices as normalized state blocks, hot- plug re-enumeration on the same ~3 s cadence PadRIO uses to look for a pad, and a device lost mid-race zeroed rather than left holding whatever was pressed when it went. XInput-class devices are excluded by VID/PID against the RawInput paths carrying the "IG_" marker - without that an Xbox pad arrives through both APIs and every button counts twice. bindings.txt gains four rows in the grammar it already had, using its own vocabulary (deadzone/rate) rather than BT411's: joydev <slot> [product-name substring] joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>] joybutton <n> button <addr> [toggle] joyhat <n> <up|down|left|right> button <addr> Slots resolve to a live device every poll, by name substring or ordinal, so unplugging and replugging does not rewrite anyone's file. Two things the pod's shape forced that BT411 solved differently: Pedals - a signed composite axis that decomposes into the pod's two pedals, positive right and negative left. The pod has a pedal each side; a twist grip or rudder bar is one signed control, and pressing one or the other but never both is exactly what it wants to say. It is a channel name like any other, so a pad stick can drive the turn too. A joyaxis on Throttle with no rate is a real lever and OWNS the channel - full travel maps onto the 0..1 the pod runs on, instead of nudging the accumulator that a spring-centred pad stick has to use. RP412JOYCONFIG=1 (joyconfig.bat) runs the capture wizard before the console screen: it asks the player to move each control, and derives the sign convention from the DIRECTION of the move. That is the point of it - a stick that reads positive pushed right and one that reads negative are equally common, and no amount of documentation gets a player to work out which they own. It writes only its own section, between marker lines, so hand-edited keyboard and pad rows survive re-running it. The wizard also prints every axis at rest before it starts. A driver that refuses the +-32767 range we ask for reports its own, and an axis then sits hard over instead of near zero; seeing "X +1.00" on an untouched stick is the difference between a five-minute fix and a bug report that says it configured itself. Each capture reports the move it saw for the same reason. Verified on the Logitech Extreme 3D on this machine. Enumeration finds it and excludes the Xbox pad, which still arrives separately through XInput. Every row shape parses - 7 axes, 2 buttons, 4 hat directions - and three deliberately malformed rows (a bad axis name, button 99, a "sideways" hat) are each rejected by line number rather than silently dropped. The wizard lists the device with its axes at rest reading X +0.00 Y -0.01 RZ -0.04 SL0 +1.00, waits on the first prompt without self-triggering, and with a hand on the stick captures X to steering, Y to pitch, RZ to the pedals and SL0 to the throttle, inverting the ones that read backwards. Running the captures through to a written file needs a hand on the stick, so that part is the machine's to confirm, not this build's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1053 lines
26 KiB
C++
1053 lines
26 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.
|
|
//
|
|
//########################################################################
|
|
|
|
#include <conio.h>
|
|
#include "l4padbindings.h"
|
|
|
|
namespace
|
|
{
|
|
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] : "?";
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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");
|
|
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");
|
|
}
|
|
}
|
|
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:
|
|
//
|
|
// 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)
|
|
//
|
|
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 },
|
|
{ "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 }
|
|
};
|
|
|
|
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,
|
|
axisSteps[s].allow_skip, &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");
|
|
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" : "");
|
|
}
|
|
//
|
|
// 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
|
|
}
|
|
|
|
//
|
|
// 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;
|
|
}
|