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>
990 lines
23 KiB
C++
990 lines
23 KiB
C++
#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;
|
|
}
|