#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 #include #include #include #include //########################################################################### // 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 #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; }