Generic joystick / HOTAS / pedals support: DirectInput 8 layer + capture wizard
Tester ask: configure non-Xbox controllers. New L4JOY layer (DI8): enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling with range normalization, reacquire-on-loss, 3s hot-plug re-enum. XInput devices are excluded via the documented RawInput IG_ VID/PID check (live-verified skipping a real XBOX360 pad -- no double-feed). bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/ deadzone; hats as 4-direction buttons with diagonal chords). PadRIO runs them through the existing channel/event machinery -- per-binding edge arrays release automatically on detach (the #24 rule), and a direct throttle axis maps full lever travel onto the 0..1 channel while the gait detent still snaps a lever parked in the walk/run dead band. BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move each control when prompted (stick/twist/throttle/fire), invert derived from the move direction, hat look cluster auto-added, output written between markers in bindings.txt with the rest of the file preserved. Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched. Verified: grammar accept/reject per line, XInput-exclusion live, 30s in-mission no-device polling clean. Awaiting a tester with real stick hardware for axis verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
86e387b6c7
commit
c2ee7299b2
@@ -0,0 +1,989 @@
|
||||
#include "mungal4.h"
|
||||
#pragma hdrstop
|
||||
|
||||
//###########################################################################
|
||||
// L4JOY -- generic-joystick reader (DirectInput 8) for the glass cockpit.
|
||||
// Design + device model: 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
|
||||
//###########################################################################
|
||||
|
||||
struct JoyDevice
|
||||
{
|
||||
IDirectInputDevice8A *device;
|
||||
BTJoyDeviceState state;
|
||||
//
|
||||
// Per-axis calibrated range (DIPROP_RANGE is set to +-32767 at open, but
|
||||
// a driver may refuse; normalization uses what the device reports).
|
||||
//
|
||||
LONG axisMin[BTJoyAxisCount];
|
||||
LONG axisMax[BTJoyAxisCount];
|
||||
};
|
||||
|
||||
static IDirectInput8A *sDirectInput = NULL;
|
||||
static JoyDevice sDevices[BTJoyMaxDevices];
|
||||
static int sDeviceCount = 0;
|
||||
static int sInitialized = 0;
|
||||
static unsigned long sLastProbeMilliseconds = 0;
|
||||
|
||||
static int
|
||||
JoyLogEnabled()
|
||||
{
|
||||
static int s_log = -1;
|
||||
if (s_log < 0)
|
||||
{
|
||||
const char *value = getenv("BT_JOY_LOG");
|
||||
s_log = (value != NULL && *value != '0') ? 1 : 0;
|
||||
}
|
||||
return s_log;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// XInput-device exclusion.
|
||||
//
|
||||
// The documented wbem-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 carries a matching VID/PID (DI packs VID in the low word,
|
||||
// PID in the high word of guidProduct.Data1).
|
||||
//###########################################################################
|
||||
|
||||
enum { XInputVidPidMax = 16 };
|
||||
static unsigned long sXInputVidPid[XInputVidPidMax];
|
||||
static int sXInputVidPidCount = 0;
|
||||
|
||||
static void
|
||||
CollectXInputVidPids()
|
||||
{
|
||||
sXInputVidPidCount = 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).
|
||||
//
|
||||
unsigned vid = 0, pid = 0;
|
||||
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;
|
||||
}
|
||||
vid = (unsigned)strtoul(v + 4, NULL, 16);
|
||||
pid = (unsigned)strtoul(p + 4, NULL, 16);
|
||||
if (sXInputVidPidCount < XInputVidPidMax)
|
||||
{
|
||||
sXInputVidPid[sXInputVidPidCount++] =
|
||||
((unsigned long)pid << 16) | vid;
|
||||
}
|
||||
}
|
||||
free(list);
|
||||
}
|
||||
|
||||
static int
|
||||
IsXInputProduct(const GUID &guid_product)
|
||||
{
|
||||
//
|
||||
// DirectInput packs VID low-word / PID high-word into Data1.
|
||||
//
|
||||
for (int i = 0; i < sXInputVidPidCount; ++i)
|
||||
{
|
||||
if (sXInputVidPid[i] == (unsigned long)guid_product.Data1)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Device open
|
||||
//###########################################################################
|
||||
|
||||
//
|
||||
// A window handle of THIS process for SetCooperativeLevel. BACKGROUND +
|
||||
// NONEXCLUSIVE polling matches the XInput model (device readable while the
|
||||
// game runs; PadRIO's focus rules stay the arbiter of what acts on it).
|
||||
//
|
||||
static 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;
|
||||
}
|
||||
|
||||
static HWND
|
||||
FindProcessWindow()
|
||||
{
|
||||
HWND hwnd = NULL;
|
||||
EnumWindows(FindProcessWindowCallback, (LPARAM)&hwnd);
|
||||
return hwnd != NULL ? hwnd : GetDesktopWindow();
|
||||
}
|
||||
|
||||
static 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; a driver
|
||||
// may refuse -- read back
|
||||
return DIENUM_CONTINUE;
|
||||
}
|
||||
|
||||
struct EnumPassContext
|
||||
{
|
||||
HWND hwnd;
|
||||
};
|
||||
|
||||
static BOOL CALLBACK
|
||||
EnumDevicesCallback(const DIDEVICEINSTANCEA *instance, VOID *context)
|
||||
{
|
||||
EnumPassContext *pass = (EnumPassContext *)context;
|
||||
|
||||
if (sDeviceCount >= BTJoyMaxDevices)
|
||||
{
|
||||
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(sDirectInput->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; the default (nonexclusive) still polls.
|
||||
//
|
||||
device->SetCooperativeLevel(pass->hwnd,
|
||||
DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
|
||||
device->EnumObjects(SetAxisRangeCallback, device, DIDFT_AXIS);
|
||||
device->Acquire();
|
||||
|
||||
JoyDevice &slot = sDevices[sDeviceCount];
|
||||
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 < BTJoyAxisCount; ++a)
|
||||
{
|
||||
slot.axisMin[a] = -32767;
|
||||
slot.axisMax[a] = 32767;
|
||||
}
|
||||
for (int h = 0; h < BTJoyHatCount; ++h)
|
||||
{
|
||||
slot.state.hat[h] = -1;
|
||||
}
|
||||
++sDeviceCount;
|
||||
|
||||
DEBUG_STREAM << "[joy] device " << (sDeviceCount - 1) << ": \""
|
||||
<< slot.state.name << "\" attached\n" << std::flush;
|
||||
return DIENUM_CONTINUE;
|
||||
}
|
||||
|
||||
static void
|
||||
ReleaseAllDevices()
|
||||
{
|
||||
for (int i = 0; i < sDeviceCount; ++i)
|
||||
{
|
||||
if (sDevices[i].device != NULL)
|
||||
{
|
||||
sDevices[i].device->Unacquire();
|
||||
sDevices[i].device->Release();
|
||||
sDevices[i].device = NULL;
|
||||
}
|
||||
sDevices[i].state.attached = 0;
|
||||
}
|
||||
sDeviceCount = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
Enumerate()
|
||||
{
|
||||
ReleaseAllDevices();
|
||||
CollectXInputVidPids();
|
||||
|
||||
EnumPassContext pass;
|
||||
pass.hwnd = FindProcessWindow();
|
||||
sDirectInput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumDevicesCallback,
|
||||
&pass, DIEDFL_ATTACHEDONLY);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Public surface
|
||||
//###########################################################################
|
||||
|
||||
int
|
||||
BTJoyInit(void)
|
||||
{
|
||||
if (sInitialized)
|
||||
{
|
||||
return sDeviceCount;
|
||||
}
|
||||
sInitialized = 1;
|
||||
|
||||
if (FAILED(DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION,
|
||||
IID_IDirectInput8A, (void **)&sDirectInput, NULL))
|
||||
|| sDirectInput == NULL)
|
||||
{
|
||||
sDirectInput = NULL;
|
||||
DEBUG_STREAM << "[joy] DirectInput8Create failed -- generic "
|
||||
<< "joysticks unavailable\n" << std::flush;
|
||||
return 0;
|
||||
}
|
||||
Enumerate();
|
||||
return sDeviceCount;
|
||||
}
|
||||
|
||||
void
|
||||
BTJoyShutdown(void)
|
||||
{
|
||||
ReleaseAllDevices();
|
||||
if (sDirectInput != NULL)
|
||||
{
|
||||
sDirectInput->Release();
|
||||
sDirectInput = NULL;
|
||||
}
|
||||
sInitialized = 0;
|
||||
}
|
||||
|
||||
static 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;
|
||||
}
|
||||
|
||||
void
|
||||
BTJoyPoll(void)
|
||||
{
|
||||
if (!sInitialized)
|
||||
{
|
||||
BTJoyInit();
|
||||
}
|
||||
if (sDirectInput == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Hot-plug: when nothing is attached (or a device died), re-enumerate
|
||||
// on the PadRIO probe cadence (~3 s) -- enumeration is too heavy for
|
||||
// every frame.
|
||||
//
|
||||
int any_attached = 0;
|
||||
for (int i = 0; i < sDeviceCount; ++i)
|
||||
{
|
||||
if (sDevices[i].state.attached)
|
||||
{
|
||||
any_attached = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_attached)
|
||||
{
|
||||
unsigned long now = timeGetTime();
|
||||
if (sLastProbeMilliseconds != 0 &&
|
||||
now - sLastProbeMilliseconds < 3000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
sLastProbeMilliseconds = now;
|
||||
Enumerate();
|
||||
}
|
||||
|
||||
for (int i = 0; i < sDeviceCount; ++i)
|
||||
{
|
||||
JoyDevice &slot = sDevices[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 detached; the binding
|
||||
// layer sees zeroed state and releases everything it held (the
|
||||
// issue-#24 stuck-button rule), and the probe above will try to
|
||||
// re-enumerate.
|
||||
//
|
||||
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 < BTJoyHatCount; ++h)
|
||||
{
|
||||
slot.state.hat[h] = -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
LONG raw[BTJoyAxisCount];
|
||||
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 < BTJoyAxisCount; ++a)
|
||||
{
|
||||
slot.state.axis[a] =
|
||||
NormalizeAxis(raw[a], slot.axisMin[a], slot.axisMax[a]);
|
||||
}
|
||||
|
||||
slot.state.buttons = 0;
|
||||
for (int b = 0; b < BTJoyButtonCount; ++b)
|
||||
{
|
||||
if (joystate.rgbButtons[b] & 0x80)
|
||||
{
|
||||
slot.state.buttons |= (1u << b);
|
||||
}
|
||||
}
|
||||
|
||||
for (int h = 0; h < BTJoyHatCount; ++h)
|
||||
{
|
||||
DWORD pov = joystate.rgdwPOV[h];
|
||||
//
|
||||
// Centered reads as -1 (0xFFFF in LOWORD per the DI contract;
|
||||
// some drivers return the full 0xFFFFFFFF).
|
||||
//
|
||||
slot.state.hat[h] =
|
||||
(LOWORD(pov) == 0xFFFF) ? -1 : (int)pov;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
BTJoyDeviceCount(void)
|
||||
{
|
||||
return sDeviceCount;
|
||||
}
|
||||
|
||||
const BTJoyDeviceState *
|
||||
BTJoyDevice(int index)
|
||||
{
|
||||
if (index < 0 || index >= sDeviceCount ||
|
||||
!sDevices[index].state.attached)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
return &sDevices[index].state;
|
||||
}
|
||||
|
||||
int
|
||||
BTJoyFindDevice(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 < sDeviceCount; ++i)
|
||||
{
|
||||
if (!sDevices[i].state.attached)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
char have[64];
|
||||
strncpy(have, sDevices[i].state.name, sizeof(have) - 1);
|
||||
have[sizeof(have) - 1] = '\0';
|
||||
_strlwr(have);
|
||||
if (strstr(have, want) != NULL)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// BT_JOYCONFIG -- the interactive capture wizard.
|
||||
//
|
||||
// Console UI (AllocConsole; the game is a GUI app). Detects which device/
|
||||
// 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 (replacing any previous generated
|
||||
// section; the rest of the file is preserved byte-for-byte).
|
||||
//###########################################################################
|
||||
|
||||
#include <conio.h>
|
||||
#include "l4padbindings.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct WizardCapture
|
||||
{
|
||||
int used;
|
||||
int device;
|
||||
int axis; // axis steps (-1 for buttons)
|
||||
int button; // button steps (-1 for axes)
|
||||
int invert;
|
||||
float deadzone;
|
||||
char line[128];
|
||||
};
|
||||
|
||||
const char *joyAxisToken(int axis)
|
||||
{
|
||||
static const char *names[BTJoyAxisCount] =
|
||||
{ "X", "Y", "Z", "RX", "RY", "RZ", "SL0", "SL1" };
|
||||
return (axis >= 0 && axis < BTJoyAxisCount) ? names[axis] : "?";
|
||||
}
|
||||
|
||||
void WizardBaseline(float baseline[BTJoyMaxDevices][BTJoyAxisCount])
|
||||
{
|
||||
//
|
||||
// ~600 ms of samples -> the at-rest position of every axis (a HOTAS
|
||||
// throttle rests wherever its lever is; never assume 0).
|
||||
//
|
||||
for (int pass = 0; pass < 20; ++pass)
|
||||
{
|
||||
BTJoyPoll();
|
||||
Sleep(30);
|
||||
}
|
||||
for (int d = 0; d < BTJoyMaxDevices; ++d)
|
||||
{
|
||||
const BTJoyDeviceState *state = BTJoyDevice(d);
|
||||
for (int a = 0; a < BTJoyAxisCount; ++a)
|
||||
{
|
||||
baseline[d][a] = state != 0 ? state->axis[a] : 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Wait for a decisive axis move. Returns 1 with (device, axis, delta,
|
||||
// final) filled, 0 on skip (SPACE), -1 on abort (ESC) / timeout.
|
||||
//
|
||||
int WizardCaptureAxis(
|
||||
const float baseline[BTJoyMaxDevices][BTJoyAxisCount],
|
||||
const WizardCapture *taken, int taken_count,
|
||||
int allow_skip,
|
||||
int *out_device, int *out_axis, float *out_delta, float *out_final)
|
||||
{
|
||||
unsigned long deadline = timeGetTime() + 30000;
|
||||
unsigned long hold_since = 0;
|
||||
int cand_device = -1, cand_axis = -1;
|
||||
|
||||
while (timeGetTime() < deadline)
|
||||
{
|
||||
while (_kbhit())
|
||||
{
|
||||
int key = _getch();
|
||||
if (key == 27)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (key == ' ' && allow_skip)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
BTJoyPoll();
|
||||
|
||||
int best_device = -1, best_axis = -1;
|
||||
float best_mag = 0.0f, best_delta = 0.0f;
|
||||
for (int d = 0; d < BTJoyMaxDevices; ++d)
|
||||
{
|
||||
const BTJoyDeviceState *state = BTJoyDevice(d);
|
||||
if (state == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int a = 0; a < BTJoyAxisCount; ++a)
|
||||
{
|
||||
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_mag)
|
||||
{
|
||||
best_mag = magnitude;
|
||||
best_delta = delta;
|
||||
best_device = d;
|
||||
best_axis = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best_mag > 0.45f)
|
||||
{
|
||||
if (cand_device != best_device || cand_axis != best_axis)
|
||||
{
|
||||
cand_device = best_device;
|
||||
cand_axis = best_axis;
|
||||
hold_since = timeGetTime();
|
||||
}
|
||||
else if (timeGetTime() - hold_since > 250)
|
||||
{
|
||||
const BTJoyDeviceState *state = BTJoyDevice(best_device);
|
||||
*out_device = best_device;
|
||||
*out_axis = best_axis;
|
||||
*out_delta = best_delta;
|
||||
*out_final = state != 0 ? state->axis[best_axis] : best_delta;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cand_device = cand_axis = -1;
|
||||
}
|
||||
Sleep(15);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
//
|
||||
// Wait for a fresh button press. Returns 1 with (device, button), 0 on
|
||||
// skip, -1 on abort/timeout.
|
||||
//
|
||||
int WizardCaptureButton(
|
||||
int allow_skip, int *out_device, int *out_button)
|
||||
{
|
||||
unsigned baseline[BTJoyMaxDevices];
|
||||
BTJoyPoll();
|
||||
for (int d = 0; d < BTJoyMaxDevices; ++d)
|
||||
{
|
||||
const BTJoyDeviceState *state = BTJoyDevice(d);
|
||||
baseline[d] = state != 0 ? state->buttons : 0;
|
||||
}
|
||||
|
||||
unsigned long deadline = timeGetTime() + 30000;
|
||||
while (timeGetTime() < deadline)
|
||||
{
|
||||
while (_kbhit())
|
||||
{
|
||||
int key = _getch();
|
||||
if (key == 27)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (key == ' ' && allow_skip)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
BTJoyPoll();
|
||||
for (int d = 0; d < BTJoyMaxDevices; ++d)
|
||||
{
|
||||
const BTJoyDeviceState *state = BTJoyDevice(d);
|
||||
if (state == 0)
|
||||
{
|
||||
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 the baseline
|
||||
}
|
||||
Sleep(15);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int
|
||||
BTJoyConfigWizard(void)
|
||||
{
|
||||
//
|
||||
// Console (the game is a GUI subsystem app).
|
||||
//
|
||||
if (GetConsoleWindow() == NULL)
|
||||
{
|
||||
AllocConsole();
|
||||
}
|
||||
FILE *io;
|
||||
freopen_s(&io, "CONOUT$", "w", stdout);
|
||||
freopen_s(&io, "CONIN$", "r", stdin);
|
||||
|
||||
printf("\n=== BattleTech joystick setup (BT_JOYCONFIG) ===\n\n");
|
||||
|
||||
//
|
||||
// Make sure a bindings.txt exists (the wizard APPENDS its section; the
|
||||
// keyboard/pad sections come from the default write).
|
||||
//
|
||||
{
|
||||
PadBindingProfile ensure_default;
|
||||
ensure_default.Load();
|
||||
}
|
||||
|
||||
if (BTJoyInit() == 0)
|
||||
{
|
||||
printf("No generic (non-Xbox) game devices found.\n");
|
||||
printf("Plug in the stick/throttle/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;
|
||||
}
|
||||
|
||||
printf("Detected devices:\n");
|
||||
for (int d = 0; d < BTJoyDeviceCount(); ++d)
|
||||
{
|
||||
const BTJoyDeviceState *state = BTJoyDevice(d);
|
||||
if (state != 0)
|
||||
{
|
||||
printf(" [%d] %s\n", d, state->name);
|
||||
}
|
||||
}
|
||||
printf("\nFor each prompt, MOVE the control you want, or press SPACE to\n"
|
||||
"skip that control, ESC to abort. Keep everything else still.\n\n");
|
||||
|
||||
WizardCapture captures[16];
|
||||
memset(captures, 0, sizeof(captures));
|
||||
int capture_count = 0;
|
||||
|
||||
struct AxisStep
|
||||
{
|
||||
const char *prompt;
|
||||
const char *channel;
|
||||
int invert_when_positive; // the asked move should read NEGATIVE
|
||||
int throttle; // full-travel lever (invert from FINAL)
|
||||
int allow_skip;
|
||||
};
|
||||
static const AxisStep axisSteps[] =
|
||||
{
|
||||
{ "AIM: push the STICK fully RIGHT", "JoystickX", 0, 0, 0 },
|
||||
{ "AIM: push the STICK fully FORWARD\n"
|
||||
" (forward aims DOWN, flight-style; add/remove the word\n"
|
||||
" invert on that line in bindings.txt to flip it later)",
|
||||
"JoystickY", 1, 0, 0 },
|
||||
{ "TURN: TWIST the stick / push the rudder RIGHT (SPACE if none)",
|
||||
"Turn", 0, 0, 1 },
|
||||
{ "THROTTLE: move the throttle lever to FULL (SPACE if none)",
|
||||
"Throttle", 0, 1, 1 },
|
||||
};
|
||||
|
||||
float baseline[BTJoyMaxDevices][BTJoyAxisCount];
|
||||
|
||||
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");
|
||||
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].throttle)
|
||||
{
|
||||
capture.invert = (final_value < 0.0f);
|
||||
capture.deadzone = 0.0f;
|
||||
sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0",
|
||||
joyAxisToken(axis), axisSteps[s].channel,
|
||||
capture.invert ? " invert" : "");
|
||||
}
|
||||
else
|
||||
{
|
||||
int negative = (delta < 0.0f);
|
||||
capture.invert = axisSteps[s].invert_when_positive
|
||||
? !negative : negative;
|
||||
capture.deadzone = 0.08f;
|
||||
sprintf(capture.line, "joyaxis %s axis %s%s",
|
||||
joyAxisToken(axis), axisSteps[s].channel,
|
||||
capture.invert ? " invert" : "");
|
||||
}
|
||||
printf(" -> device %d (%s) axis %s%s\n\n", device,
|
||||
BTJoyDevice(device) ? BTJoyDevice(device)->name : "?",
|
||||
joyAxisToken(axis), capture.invert ? " (inverted)" : "");
|
||||
Sleep(800); // let the control come back to rest
|
||||
}
|
||||
|
||||
struct ButtonStep
|
||||
{
|
||||
const char *prompt;
|
||||
int address;
|
||||
int allow_skip;
|
||||
};
|
||||
static const ButtonStep buttonSteps[] =
|
||||
{
|
||||
{ "FIRE 1: press the TRIGGER", 0x40, 0 },
|
||||
{ "FIRE 2: press your second fire button", 0x46, 1 },
|
||||
{ "FIRE 3: press your third fire button", 0x47, 1 },
|
||||
{ "FIRE 4: press your fourth fire button", 0x45, 1 },
|
||||
{ "REVERSE: press the button for reverse thrust", 0x3F, 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");
|
||||
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%X",
|
||||
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");
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// Group by device -> joydev slots in order of first use. The hat on
|
||||
// the primary (stick) device gets the pod look cluster automatically
|
||||
// (hats are standardized; no capture needed).
|
||||
//
|
||||
int slot_of_device[BTJoyMaxDevices];
|
||||
int slot_count = 0;
|
||||
for (int d = 0; d < BTJoyMaxDevices; ++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: preserve everything outside the marker pair.
|
||||
//
|
||||
static const char *beginMarker =
|
||||
"# >>> BT_JOYCONFIG generated -- do not edit between markers";
|
||||
static const char *endMarker = "# <<< BT_JOYCONFIG end";
|
||||
|
||||
static char kept[32768];
|
||||
kept[0] = '\0';
|
||||
{
|
||||
FILE *f = fopen("bindings.txt", "rt");
|
||||
if (f != NULL)
|
||||
{
|
||||
char line[512];
|
||||
int inside = 0;
|
||||
size_t used = 0;
|
||||
while (fgets(line, sizeof(line), f))
|
||||
{
|
||||
if (strstr(line, beginMarker) != NULL)
|
||||
{
|
||||
inside = 1;
|
||||
continue;
|
||||
}
|
||||
if (strstr(line, endMarker) != NULL)
|
||||
{
|
||||
inside = 0;
|
||||
continue;
|
||||
}
|
||||
if (!inside && used + strlen(line) < sizeof(kept) - 1)
|
||||
{
|
||||
strcpy(kept + used, line);
|
||||
used += strlen(line);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
FILE *f = fopen("bindings.txt", "wt");
|
||||
if (f == NULL)
|
||||
{
|
||||
printf("ERROR: cannot write bindings.txt (working directory?)\n");
|
||||
_getch();
|
||||
return 1;
|
||||
}
|
||||
fputs(kept, f);
|
||||
if (kept[0] != '\0' && kept[strlen(kept) - 1] != '\n')
|
||||
{
|
||||
fputs("\n", f);
|
||||
}
|
||||
fprintf(f, "%s\n", beginMarker);
|
||||
int hat_done = 0;
|
||||
for (int d = 0; d < BTJoyMaxDevices; ++d)
|
||||
{
|
||||
if (slot_of_device[d] < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const BTJoyDeviceState *state = BTJoyDevice(d);
|
||||
fprintf(f, "joydev %d %s\n", slot_of_device[d],
|
||||
state != 0 ? state->name : "");
|
||||
for (int i = 0; i < capture_count; ++i)
|
||||
{
|
||||
if (captures[i].used && captures[i].device == d)
|
||||
{
|
||||
fprintf(f, "%s\n", captures[i].line);
|
||||
}
|
||||
}
|
||||
if (!hat_done)
|
||||
{
|
||||
hat_done = 1;
|
||||
fprintf(f, "joyhat 0 up button 0x42\n");
|
||||
fprintf(f, "joyhat 0 down button 0x41\n");
|
||||
fprintf(f, "joyhat 0 left button 0x44\n");
|
||||
fprintf(f, "joyhat 0 right button 0x43\n");
|
||||
}
|
||||
}
|
||||
fprintf(f, "%s\n", endMarker);
|
||||
fclose(f);
|
||||
|
||||
printf("bindings.txt written (%d controls + hat looks).\n", capture_count);
|
||||
printf("The game uses them from the next launch -- have fun.\n\n");
|
||||
printf("Press any key to continue into the game.\n");
|
||||
_getch();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user