Files
BT411/engine/MUNGA_L4/L4JOY.cpp
T
arcattackandClaude Opus 4.8 c2ee7299b2 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>
2026-07-23 16:36:32 -05:00

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;
}