diff --git a/MUNGA_L4/L4JOY.cpp b/MUNGA_L4/L4JOY.cpp new file mode 100644 index 0000000..353fd5c --- /dev/null +++ b/MUNGA_L4/L4JOY.cpp @@ -0,0 +1,1052 @@ +#include "mungal4.h" +#pragma hdrstop + +//######################################################################## +// L4JOY - the generic-joystick reader. Design and device model in +// L4JOY.h. Consumed by the PadRIO poll through the joydev / joyaxis / +// joybutton / joyhat bindings (L4PADBINDINGS / L4PADRIO). +//######################################################################## + +#include "l4joy.h" + +#define DIRECTINPUT_VERSION 0x0800 +#include +#include +#include +#include +#include + +//######################################################################## +// State +//######################################################################## + +namespace +{ + struct JoyDevice + { + IDirectInputDevice8A *device; + RPJoyDeviceState state; + // + // Per-axis calibrated range. DIPROP_RANGE is set to +-32767 when + // the device is opened, but a driver may refuse, so normalization + // uses what the device actually reports. + // + LONG axisMin[joyAxisCount]; + LONG axisMax[joyAxisCount]; + }; + + IDirectInput8A *gDirectInput = NULL; + JoyDevice gDevices[joyMaxDevices]; + int gDeviceCount = 0; + int gInitialized = 0; + unsigned long gLastProbeTick = 0; + + int JoyLogEnabled() + { + static int log = -1; + if (log < 0) + { + const char *value = getenv("RP412JOYLOG"); + log = (value != NULL && *value != '0') ? 1 : 0; + } + return log; + } + + //################################################################### + // XInput-device exclusion. + // + // The documented WMI-free method: every XInput-capable device's + // RawInput device path carries the "IG_" marker. Collect the VID/PID + // of each such path once per enumeration pass and skip any + // DirectInput device whose guidProduct matches - DI packs VID in the + // low word and PID in the high word of guidProduct.Data1. + // + // Without this an Xbox pad arrives through both APIs and every + // button counts twice. + //################################################################### + + enum { xinputVidPidMax = 16 }; + unsigned long gXInputVidPid[xinputVidPidMax]; + int gXInputVidPidCount = 0; + + void CollectXInputVidPids() + { + gXInputVidPidCount = 0; + + UINT device_count = 0; + if (GetRawInputDeviceList(NULL, &device_count, + sizeof(RAWINPUTDEVICELIST)) != 0 || device_count == 0) + { + return; + } + RAWINPUTDEVICELIST *list = (RAWINPUTDEVICELIST *) + malloc(device_count * sizeof(RAWINPUTDEVICELIST)); + if (list == NULL) + { + return; + } + device_count = GetRawInputDeviceList(list, &device_count, + sizeof(RAWINPUTDEVICELIST)); + if (device_count == (UINT) -1) + { + free(list); + return; + } + + for (UINT i = 0; i < device_count; ++i) + { + if (list[i].dwType != RIM_TYPEHID) + { + continue; + } + char path[256]; + UINT size = sizeof(path); + if (GetRawInputDeviceInfoA(list[i].hDevice, RIDI_DEVICENAME, + path, &size) == (UINT) -1) + { + continue; + } + path[sizeof(path) - 1] = '\0'; + if (strstr(path, "IG_") == NULL && strstr(path, "ig_") == NULL) + { + continue; + } + // + // Parse "...VID_045E&PID_028E..." - case varies by driver. + // + const char *v = strstr(path, "VID_"); + if (v == NULL) v = strstr(path, "vid_"); + const char *p = strstr(path, "PID_"); + if (p == NULL) p = strstr(path, "pid_"); + if (v == NULL || p == NULL) + { + continue; + } + unsigned vid = (unsigned) strtoul(v + 4, NULL, 16); + unsigned pid = (unsigned) strtoul(p + 4, NULL, 16); + if (gXInputVidPidCount < xinputVidPidMax) + { + gXInputVidPid[gXInputVidPidCount++] = + ((unsigned long) pid << 16) | vid; + } + } + free(list); + } + + int IsXInputProduct(const GUID &guid_product) + { + for (int i = 0; i < gXInputVidPidCount; ++i) + { + if (gXInputVidPid[i] == (unsigned long) guid_product.Data1) + { + return 1; + } + } + return 0; + } + + //################################################################### + // Device open + //################################################################### + + // + // A window handle of THIS process for SetCooperativeLevel. BACKGROUND + // + NONEXCLUSIVE matches the XInput model: the device stays readable + // while the game runs, and PadRIO's own rules remain the arbiter of + // what acts on it. + // + BOOL CALLBACK FindProcessWindowCallback(HWND hwnd, LPARAM lparam) + { + DWORD process_id = 0; + GetWindowThreadProcessId(hwnd, &process_id); + if (process_id == GetCurrentProcessId()) + { + *(HWND *) lparam = hwnd; + return FALSE; + } + return TRUE; + } + + HWND FindProcessWindow() + { + HWND hwnd = NULL; + EnumWindows(FindProcessWindowCallback, (LPARAM) &hwnd); + return (hwnd != NULL) ? hwnd : GetDesktopWindow(); + } + + BOOL CALLBACK SetAxisRangeCallback( + LPCDIDEVICEOBJECTINSTANCEA object, LPVOID context) + { + IDirectInputDevice8A *device = (IDirectInputDevice8A *) context; + + DIPROPRANGE range; + range.diph.dwSize = sizeof(DIPROPRANGE); + range.diph.dwHeaderSize = sizeof(DIPROPHEADER); + range.diph.dwHow = DIPH_BYID; + range.diph.dwObj = object->dwType; + range.lMin = -32767; + range.lMax = 32767; + device->SetProperty(DIPROP_RANGE, &range.diph); // best effort + return DIENUM_CONTINUE; + } + + BOOL CALLBACK EnumDevicesCallback( + const DIDEVICEINSTANCEA *instance, VOID *context) + { + HWND owner = *(HWND *) context; + + if (gDeviceCount >= joyMaxDevices) + { + return DIENUM_STOP; + } + if (IsXInputProduct(instance->guidProduct)) + { + if (JoyLogEnabled()) + { + DEBUG_STREAM << "Joy: skipping XInput-class device \"" + << instance->tszProductName << "\"\n" << std::flush; + } + return DIENUM_CONTINUE; + } + + IDirectInputDevice8A *device = NULL; + if (FAILED(gDirectInput->CreateDevice(instance->guidInstance, + &device, NULL)) || device == NULL) + { + return DIENUM_CONTINUE; + } + if (FAILED(device->SetDataFormat(&c_dfDIJoystick2))) + { + device->Release(); + return DIENUM_CONTINUE; + } + // + // Best effort: some environments reject a cooperative level on + // the desktop window, and the default still polls. + // + device->SetCooperativeLevel(owner, + DISCL_BACKGROUND | DISCL_NONEXCLUSIVE); + device->EnumObjects(SetAxisRangeCallback, device, DIDFT_AXIS); + device->Acquire(); + + JoyDevice &slot = gDevices[gDeviceCount]; + memset(&slot, 0, sizeof(slot)); + slot.device = device; + slot.state.attached = 1; + strncpy(slot.state.name, instance->tszProductName, + sizeof(slot.state.name) - 1); + for (int a = 0; a < joyAxisCount; ++a) + { + slot.axisMin[a] = -32767; + slot.axisMax[a] = 32767; + } + for (int h = 0; h < joyHatCount; ++h) + { + slot.state.hat[h] = -1; + } + ++gDeviceCount; + + DEBUG_STREAM << "Joy: device " << (gDeviceCount - 1) << ": \"" + << slot.state.name << "\" attached\n" << std::flush; + return DIENUM_CONTINUE; + } + + void ReleaseAllDevices() + { + for (int i = 0; i < gDeviceCount; ++i) + { + if (gDevices[i].device != NULL) + { + gDevices[i].device->Unacquire(); + gDevices[i].device->Release(); + gDevices[i].device = NULL; + } + gDevices[i].state.attached = 0; + } + gDeviceCount = 0; + } + + void Enumerate() + { + ReleaseAllDevices(); + CollectXInputVidPids(); + + HWND owner = FindProcessWindow(); + gDirectInput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumDevicesCallback, + &owner, DIEDFL_ATTACHEDONLY); + } + + float NormalizeAxis(LONG value, LONG range_min, LONG range_max) + { + if (range_max <= range_min) + { + return 0.0f; + } + float normalized = + ((float)(value - range_min) / (float)(range_max - range_min)) + * 2.0f - 1.0f; + if (normalized < -1.0f) normalized = -1.0f; + if (normalized > 1.0f) normalized = 1.0f; + return normalized; + } +} + +//######################################################################## +// Public surface +//######################################################################## + +int + RPJoyInit(void) +{ + if (gInitialized) + { + return gDeviceCount; + } + gInitialized = 1; + + if (FAILED(DirectInput8Create(GetModuleHandleA(NULL), DIRECTINPUT_VERSION, + IID_IDirectInput8A, (void **) &gDirectInput, NULL)) + || gDirectInput == NULL) + { + gDirectInput = NULL; + DEBUG_STREAM << "Joy: DirectInput8Create failed - generic joysticks " + << "unavailable\n" << std::flush; + return 0; + } + Enumerate(); + return gDeviceCount; +} + +void + RPJoyShutdown(void) +{ + ReleaseAllDevices(); + if (gDirectInput != NULL) + { + gDirectInput->Release(); + gDirectInput = NULL; + } + gInitialized = 0; +} + +void + RPJoyPoll(void) +{ + if (!gInitialized) + { + RPJoyInit(); + } + if (gDirectInput == NULL) + { + return; + } + + // + // Hot-plug: when nothing is attached, re-enumerate on the same ~3 s + // cadence PadRIO uses to look for a pad. Enumeration is far too heavy + // to run every frame. + // + int any_attached = 0; + for (int i = 0; i < gDeviceCount; ++i) + { + if (gDevices[i].state.attached) + { + any_attached = 1; + break; + } + } + if (!any_attached) + { + unsigned long now = GetTickCount(); + if (gLastProbeTick != 0 && now - gLastProbeTick < 3000) + { + return; + } + gLastProbeTick = now; + Enumerate(); + } + + for (int i = 0; i < gDeviceCount; ++i) + { + JoyDevice &slot = gDevices[i]; + if (slot.device == NULL || !slot.state.attached) + { + continue; + } + + HRESULT result = slot.device->Poll(); + if (FAILED(result)) + { + result = slot.device->Acquire(); + if (SUCCEEDED(result)) + { + result = slot.device->Poll(); + } + } + DIJOYSTATE2 joystate; + if (SUCCEEDED(result) || result == DI_NOEFFECT) + { + result = slot.device->GetDeviceState(sizeof(joystate), &joystate); + } + if (FAILED(result)) + { + // + // Unplugged, or the driver died. Mark it detached and zero + // the state: the binding layer then sees everything released + // rather than holding whatever was pressed at the moment the + // device went away. + // + DEBUG_STREAM << "Joy: device " << i << " (\"" << slot.state.name + << "\") lost\n" << std::flush; + slot.state.attached = 0; + memset(slot.state.axis, 0, sizeof(slot.state.axis)); + slot.state.buttons = 0; + for (int h = 0; h < joyHatCount; ++h) + { + slot.state.hat[h] = -1; + } + continue; + } + + LONG raw[joyAxisCount]; + raw[0] = joystate.lX; + raw[1] = joystate.lY; + raw[2] = joystate.lZ; + raw[3] = joystate.lRx; + raw[4] = joystate.lRy; + raw[5] = joystate.lRz; + raw[6] = joystate.rglSlider[0]; + raw[7] = joystate.rglSlider[1]; + for (int a = 0; a < joyAxisCount; ++a) + { + slot.state.axis[a] = + NormalizeAxis(raw[a], slot.axisMin[a], slot.axisMax[a]); + } + + slot.state.buttons = 0; + for (int b = 0; b < joyButtonCount; ++b) + { + if (joystate.rgbButtons[b] & 0x80) + { + slot.state.buttons |= (1u << b); + } + } + + for (int h = 0; h < joyHatCount; ++h) + { + DWORD pov = joystate.rgdwPOV[h]; + // + // Centered reads as 0xFFFF in the low word per the DI + // contract; some drivers return the full 0xFFFFFFFF. + // + slot.state.hat[h] = (LOWORD(pov) == 0xFFFF) ? -1 : (int) pov; + } + } +} + +int + RPJoyDeviceCount(void) +{ + return gDeviceCount; +} + +const RPJoyDeviceState * + RPJoyDevice(int index) +{ + if (index < 0 || index >= gDeviceCount || !gDevices[index].state.attached) + { + return NULL; + } + return &gDevices[index].state; +} + +int + RPJoyFindDevice(const char *name_substring) +{ + if (name_substring == NULL || *name_substring == '\0') + { + return -1; + } + char want[64]; + strncpy(want, name_substring, sizeof(want) - 1); + want[sizeof(want) - 1] = '\0'; + _strlwr(want); + + for (int i = 0; i < gDeviceCount; ++i) + { + if (!gDevices[i].state.attached) + { + continue; + } + char have[64]; + strncpy(have, gDevices[i].state.name, sizeof(have) - 1); + have[sizeof(have) - 1] = '\0'; + _strlwr(have); + if (strstr(have, want) != NULL) + { + return i; + } + } + return -1; +} + +//######################################################################## +//########################### RP412JOYCONFIG ############################# +//######################################################################## +// +// The interactive capture wizard. +// +// Console UI - the game is a GUI app, so it allocates one. It detects +// which device and axis the player moves for each pod control, derives +// the sign convention from the DIRECTION of the move, then writes the +// joystick section of bindings.txt between marker lines. Everything +// outside the markers is preserved byte for byte, so a player's own +// keyboard and pad edits survive re-running it. +// +// Deriving the sign from the move is the point: a stick that reads +// positive when pushed right and one that reads negative are equally +// common, and no amount of documentation gets a player to work out which +// they own. +// +//######################################################################## + +#include +#include "l4padbindings.h" + +namespace +{ + struct WizardCapture + { + int used; + int device; + int axis; // axis index, -1 for buttons + int button; // button index, -1 for axes + int invert; + char line[128]; + }; + + const char *JoyAxisToken(int axis) + { + static const char *names[joyAxisCount] = + { "X", "Y", "Z", "RX", "RY", "RZ", "SL0", "SL1" }; + return (axis >= 0 && axis < joyAxisCount) ? names[axis] : "?"; + } + + void WizardBaseline(float baseline[joyMaxDevices][joyAxisCount]) + { + // + // ~600 ms of samples gives the at-rest position of every axis. A + // HOTAS throttle rests wherever its lever was left, so an + // assumed zero would read as a huge deflection. + // + for (int pass = 0; pass < 20; ++pass) + { + RPJoyPoll(); + Sleep(30); + } + for (int d = 0; d < joyMaxDevices; ++d) + { + const RPJoyDeviceState *state = RPJoyDevice(d); + for (int a = 0; a < joyAxisCount; ++a) + { + baseline[d][a] = (state != NULL) ? state->axis[a] : 0.0f; + } + } + } + + // + // Wait for a decisive axis move. Returns 1 with the outputs filled, + // 0 on skip (SPACE), -1 on abort (ESC) or timeout. + // + int WizardCaptureAxis( + const float baseline[joyMaxDevices][joyAxisCount], + const WizardCapture *taken, int taken_count, + int allow_skip, + int *out_device, int *out_axis, float *out_delta, float *out_final) + { + unsigned long deadline = GetTickCount() + 30000; + unsigned long hold_since = 0; + int candidate_device = -1, candidate_axis = -1; + + while (GetTickCount() < deadline) + { + while (_kbhit()) + { + int key = _getch(); + if (key == 27) + { + return -1; + } + if (key == ' ' && allow_skip) + { + return 0; + } + } + RPJoyPoll(); + + int best_device = -1, best_axis = -1; + float best_magnitude = 0.0f, best_delta = 0.0f; + for (int d = 0; d < joyMaxDevices; ++d) + { + const RPJoyDeviceState *state = RPJoyDevice(d); + if (state == NULL) + { + continue; + } + for (int a = 0; a < joyAxisCount; ++a) + { + // + // An axis already claimed cannot be claimed again - + // otherwise one twitchy axis wins every prompt. + // + int already = 0; + for (int t = 0; t < taken_count; ++t) + { + if (taken[t].used && taken[t].axis >= 0 && + taken[t].device == d && taken[t].axis == a) + { + already = 1; + } + } + if (already) + { + continue; + } + float delta = state->axis[a] - baseline[d][a]; + float magnitude = (delta < 0.0f) ? -delta : delta; + if (magnitude > best_magnitude) + { + best_magnitude = magnitude; + best_delta = delta; + best_device = d; + best_axis = a; + } + } + } + + // + // Nearly half of full travel, held for a quarter second: big + // enough that a resting hand or a noisy pot cannot trip it, + // and the hold means a knocked stick passing through does + // not either. + // + if (best_magnitude > 0.45f) + { + if (candidate_device != best_device || candidate_axis != best_axis) + { + candidate_device = best_device; + candidate_axis = best_axis; + hold_since = GetTickCount(); + } + else if (GetTickCount() - hold_since > 250) + { + const RPJoyDeviceState *state = RPJoyDevice(best_device); + *out_device = best_device; + *out_axis = best_axis; + *out_delta = best_delta; + *out_final = (state != NULL) + ? state->axis[best_axis] : best_delta; + return 1; + } + } + else + { + candidate_device = candidate_axis = -1; + } + Sleep(15); + } + return -1; + } + + // + // Wait for a fresh button press. Returns 1 with the outputs filled, + // 0 on skip, -1 on abort or timeout. + // + int WizardCaptureButton(int allow_skip, int *out_device, int *out_button) + { + // + // Baseline the buttons already held, so a trigger squeezed since + // the last prompt does not answer this one by itself. + // + unsigned baseline[joyMaxDevices]; + RPJoyPoll(); + for (int d = 0; d < joyMaxDevices; ++d) + { + const RPJoyDeviceState *state = RPJoyDevice(d); + baseline[d] = (state != NULL) ? state->buttons : 0; + } + + unsigned long deadline = GetTickCount() + 30000; + while (GetTickCount() < deadline) + { + while (_kbhit()) + { + int key = _getch(); + if (key == 27) + { + return -1; + } + if (key == ' ' && allow_skip) + { + return 0; + } + } + RPJoyPoll(); + for (int d = 0; d < joyMaxDevices; ++d) + { + const RPJoyDeviceState *state = RPJoyDevice(d); + if (state == NULL) + { + continue; + } + unsigned fresh = state->buttons & ~baseline[d]; + if (fresh != 0) + { + int button = 0; + while ((fresh & 1u) == 0) + { + fresh >>= 1; + ++button; + } + *out_device = d; + *out_button = button; + return 1; + } + baseline[d] &= state->buttons; // releases refresh it + } + Sleep(15); + } + return -1; + } +} + +int + RPJoyConfigWizard(void) +{ + // + // The game is a GUI-subsystem app and has no console of its own. + // + if (GetConsoleWindow() == NULL) + { + AllocConsole(); + } + FILE *io; + freopen_s(&io, "CONOUT$", "w", stdout); + freopen_s(&io, "CONIN$", "r", stdin); + + printf("\n=== Red Planet joystick setup (RP412JOYCONFIG) ===\n\n"); + + // + // Make sure bindings.txt exists before we start: the wizard only + // writes its own section, and the keyboard and pad rows come from + // the default write. + // + { + PadBindingProfile ensure_default; + PadBindings_Load(&ensure_default); + } + + if (RPJoyInit() == 0) + { + printf("No generic (non-Xbox) game devices found.\n"); + printf("Plug in the stick, throttle or pedals and run joyconfig again.\n"); + printf("(Xbox-class controllers already work - no setup needed.)\n\n"); + printf("Press any key to exit.\n"); + _getch(); + return 1; + } + + // + // Let the devices settle and report where every axis is sitting. + // Worth printing rather than assuming: a driver that refuses the + // +-32767 range we ask for reports its own, and an axis then rests + // hard over instead of near zero. Seeing "X +1.00" on an untouched + // stick is the difference between a five-minute fix and a bug report + // that says "it configured itself". + // + { + float settle[joyMaxDevices][joyAxisCount]; + WizardBaseline(settle); + + printf("Detected devices (axes at rest):\n"); + for (int d = 0; d < RPJoyDeviceCount(); ++d) + { + const RPJoyDeviceState *state = RPJoyDevice(d); + if (state == NULL) + { + continue; + } + printf(" [%d] %s\n ", d, state->name); + for (int a = 0; a < joyAxisCount; ++a) + { + printf("%s %+.2f ", JoyAxisToken(a), settle[d][a]); + } + printf("\n"); + } + } + printf("\nFor each prompt, MOVE the control you want, or press SPACE to\n" + "skip it, ESC to abort. Keep everything else still.\n\n"); + + WizardCapture captures[16]; + memset(captures, 0, sizeof(captures)); + int capture_count = 0; + + // + // The pod's analog channels. wants_negative says the asked-for move + // should read NEGATIVE in the pod's sign convention, which is what + // decides whether the captured axis gets an invert: + // + // JoystickX left +1, right -1 + // JoystickY forward -1, back +1 + // Pedals right +1, left -1 (the composite that decomposes + // into the pod's two pedals) + // + struct AxisStep + { + const char *prompt; + const char *channel; + int wants_negative; + int lever; // full-travel lever: sign from where + // it ENDS, not which way it moved + int allow_skip; + }; + static const AxisStep axisSteps[] = + { + { "STEER: push the STICK / turn the WHEEL fully RIGHT", + "JoystickX", 1, 0, 0 }, + { "PITCH: push the STICK fully FORWARD\n" + " (add or remove the word invert on that line in\n" + " bindings.txt to flip it later)", + "JoystickY", 1, 0, 0 }, + { "PEDALS: twist the stick / press the RIGHT rudder pedal\n" + " (SPACE if you have neither)", + "Pedals", 0, 0, 1 }, + { "THROTTLE: move the throttle lever to FULL (SPACE if none)", + "Throttle", 0, 1, 1 } + }; + + float baseline[joyMaxDevices][joyAxisCount]; + + for (int s = 0; s < (int)(sizeof(axisSteps) / sizeof(axisSteps[0])); ++s) + { + printf("%s ...\n", axisSteps[s].prompt); + WizardBaseline(baseline); + int device, axis; + float delta, final_value; + int got = WizardCaptureAxis(baseline, captures, capture_count, + axisSteps[s].allow_skip, &device, &axis, &delta, &final_value); + if (got < 0) + { + printf("\nAborted - nothing written.\n"); + printf("Press any key to continue into the game.\n"); + _getch(); + return 1; + } + if (got == 0) + { + printf(" skipped.\n\n"); + continue; + } + WizardCapture &capture = captures[capture_count++]; + capture.used = 1; + capture.device = device; + capture.axis = axis; + capture.button = -1; + + if (axisSteps[s].lever) + { + // + // A lever has no rest position to move away from, so the + // sign comes from where it finished: full-forward reading + // negative means the axis runs backwards for us. + // + capture.invert = (final_value < 0.0f); + sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0", + JoyAxisToken(axis), axisSteps[s].channel, + capture.invert ? " invert" : ""); + } + else + { + int went_negative = (delta < 0.0f); + capture.invert = axisSteps[s].wants_negative + ? !went_negative : went_negative; + sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0.08", + JoyAxisToken(axis), axisSteps[s].channel, + capture.invert ? " invert" : ""); + } + // + // The move is reported, not just the axis: a capture nobody made + // shows up here as a small delta, and a player who wonders why + // the wrong control answered can see what the wizard saw. + // + printf(" -> device %d (%s) axis %s%s [moved %+.2f, now %+.2f]\n\n", + device, + (RPJoyDevice(device) != NULL) ? RPJoyDevice(device)->name : "?", + JoyAxisToken(axis), capture.invert ? " (inverted)" : "", + delta, final_value); + Sleep(800); // let the control come back to rest + } + + // + // The pod's stick-head buttons, at their RIO addresses. + // + struct ButtonStep + { + const char *prompt; + int address; + int allow_skip; + }; + static const ButtonStep buttonSteps[] = + { + { "MAIN: press the TRIGGER", 0x40, 0 }, + { "MIDDLE: press your second fire button", 0x46, 1 }, + { "UPPER: press your third fire button", 0x47, 1 }, + { "PINKY: press your fourth fire button", 0x45, 1 }, + { "REVERSE: press the button for reverse thrust", 0x3F, 1 }, + { "PANIC: press the button for the panic stop", 0x3D, 1 } + }; + + for (int s = 0; s < (int)(sizeof(buttonSteps) / sizeof(buttonSteps[0])); ++s) + { + printf("%s ... %s\n", buttonSteps[s].prompt, + buttonSteps[s].allow_skip ? "(SPACE to skip)" : ""); + int device, button; + int got = WizardCaptureButton(buttonSteps[s].allow_skip, + &device, &button); + if (got < 0) + { + printf("\nAborted - nothing written.\n"); + printf("Press any key to continue into the game.\n"); + _getch(); + return 1; + } + if (got == 0) + { + printf(" skipped.\n\n"); + continue; + } + WizardCapture &capture = captures[capture_count++]; + capture.used = 1; + capture.device = device; + capture.axis = -1; + capture.button = button; + sprintf(capture.line, "joybutton %d button 0x%02X", + button, buttonSteps[s].address); + printf(" -> device %d button %d\n\n", device, button); + Sleep(400); + } + + if (capture_count == 0) + { + printf("Nothing captured - nothing written.\n"); + printf("Press any key to continue into the game.\n"); + _getch(); + return 1; + } + + // + // Group by device into joydev slots, in order of first use. The hat + // on the first device gets the pod's look cluster automatically - + // hats are standardized, so there is nothing to ask. + // + int slot_of_device[joyMaxDevices]; + int slot_count = 0; + for (int d = 0; d < joyMaxDevices; ++d) + { + slot_of_device[d] = -1; + } + for (int i = 0; i < capture_count; ++i) + { + if (captures[i].used && slot_of_device[captures[i].device] < 0) + { + slot_of_device[captures[i].device] = slot_count++; + } + } + + // + // Rewrite bindings.txt, preserving everything outside the markers. + // + static const char *beginMarker = + "# >>> RP412JOYCONFIG generated - do not edit between the markers"; + static const char *endMarker = "# <<< RP412JOYCONFIG end"; + + static char kept[65536]; + kept[0] = '\0'; + { + FILE *in = fopen("bindings.txt", "rt"); + if (in != NULL) + { + char line[512]; + int inside = 0; + size_t used = 0; + while (fgets(line, sizeof(line), in) != NULL) + { + if (strstr(line, "RP412JOYCONFIG generated") != NULL) + { + inside = 1; + continue; + } + if (strstr(line, "RP412JOYCONFIG end") != NULL) + { + inside = 0; + continue; + } + if (!inside && used + strlen(line) < sizeof(kept) - 1) + { + strcpy(kept + used, line); + used += strlen(line); + } + } + fclose(in); + } + } + + FILE *out = fopen("bindings.txt", "wt"); + if (out == NULL) + { + printf("ERROR: cannot write bindings.txt (wrong working directory?)\n"); + printf("Press any key to continue into the game.\n"); + _getch(); + return 1; + } + fputs(kept, out); + if (kept[0] != '\0' && kept[strlen(kept) - 1] != '\n') + { + fputs("\n", out); + } + fprintf(out, "%s\n", beginMarker); + int hat_done = 0; + for (int d = 0; d < joyMaxDevices; ++d) + { + if (slot_of_device[d] < 0) + { + continue; + } + const RPJoyDeviceState *state = RPJoyDevice(d); + fprintf(out, "joydev %d %s\n", slot_of_device[d], + (state != NULL) ? state->name : ""); + for (int i = 0; i < capture_count; ++i) + { + if (captures[i].used && captures[i].device == d) + { + fprintf(out, "%s\n", captures[i].line); + } + } + if (!hat_done) + { + hat_done = 1; + fprintf(out, "joyhat 0 up button 0x42\n"); + fprintf(out, "joyhat 0 down button 0x41\n"); + fprintf(out, "joyhat 0 left button 0x44\n"); + fprintf(out, "joyhat 0 right button 0x43\n"); + } + } + fprintf(out, "%s\n", endMarker); + fclose(out); + + printf("bindings.txt written (%d controls + hat looks).\n", capture_count); + printf("The game uses them from here on - have fun.\n\n"); + printf("Press any key to continue into the game.\n"); + _getch(); + return 0; +} diff --git a/MUNGA_L4/L4JOY.h b/MUNGA_L4/L4JOY.h new file mode 100644 index 0000000..66d14f4 --- /dev/null +++ b/MUNGA_L4/L4JOY.h @@ -0,0 +1,85 @@ +//===========================================================================// +// File: l4joy.h // +// Project: MUNGA Brick: generic joystick reader // +// Contents: DirectInput 8 sticks, HOTAS throttles and pedals // +//---------------------------------------------------------------------------// +// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. // +// PROPRIETARY AND CONFIDENTIAL // +//===========================================================================// + +#pragma once + +//######################################################################## +// +// L4JOY - the generic-joystick reader. +// +// PadRIO reads XInput, which covers Xbox-class pads and nothing else. +// This layer adds every OTHER game device Windows knows - flight sticks, +// HOTAS throttles, twist grips, rudder pedals, wheels - through +// DirectInput 8, the standard generic-HID game API. It exposes up to +// joyMaxDevices attached devices as normalized state blocks; the PadRIO +// poll maps them onto the pod's control channels through the joydev / +// joyaxis / joybutton / joyhat rows of bindings.txt (L4PADBINDINGS.h), +// the same binding machinery the pad and keyboard already use. +// +// XInput-class devices are EXCLUDED here, or they would double-feed +// through both APIs and every input would count twice. A DirectInput +// device whose VID/PID also appears in a RawInput device path containing +// the "IG_" marker is an XInput device - the documented detection that +// does not drag in WMI. +// +// This is distinct from the legacy L4DINPUT.cpp DIJoystick, the 1995-era +// single-device `Joystick` engine interface reachable only through the +// old L4CONTROLS=DIJOYSTICK profile. That path is untouched. +// +// RP412JOYCONFIG=1 runs the interactive setup wizard at boot: it asks +// the player to move each control, works out which device and axis moved +// and which way, and writes the joystick section of bindings.txt. +// RP412JOYLOG=1 logs device attach/detach. +// +// Ported from BT411, whose glass cockpit needed the same thing. +// +//######################################################################## + +enum +{ + joyMaxDevices = 4, + joyAxisCount = 8, // X Y Z RX RY RZ SL0 SL1 (DIJOYSTATE2 order) + joyButtonCount = 32, // buttons exposed to bindings (DI carries 128) + joyHatCount = 4 +}; + +struct RPJoyDeviceState +{ + int attached; + float axis[joyAxisCount]; // normalized -1..1, raw: deadzones + // are the binding layer's business + unsigned buttons; // bit n = button n held + int hat[joyHatCount]; // POV in centidegrees, -1 = centered + char name[64]; // product name ("T.16000M", ...) +}; + +// +// Lifecycle. Init is lazy-safe (Poll calls it) and returns the attached +// non-XInput device count. Re-enumeration for hot-plug happens inside +// Poll on a ~3 s cadence whenever nothing is attached. +// +int RPJoyInit(void); +void RPJoyShutdown(void); +void RPJoyPoll(void); + +int RPJoyDeviceCount(void); +const RPJoyDeviceState *RPJoyDevice(int index); // NULL out of range/detached + +// +// Case-insensitive product-name substring match to a device index, -1 for +// no match. This is what a named joydev slot resolves through. +// +int RPJoyFindDevice(const char *name_substring); + +// +// The RP412JOYCONFIG capture wizard (console UI; called from RPL4.CPP +// before the front end). Returns 0 if it wrote a config, non-zero on +// abort or no device. +// +int RPJoyConfigWizard(void); diff --git a/MUNGA_L4/L4PADBINDINGS.cpp b/MUNGA_L4/L4PADBINDINGS.cpp index 80886c6..4b3399d 100644 --- a/MUNGA_L4/L4PADBINDINGS.cpp +++ b/MUNGA_L4/L4PADBINDINGS.cpp @@ -2,6 +2,7 @@ #pragma hdrstop #include "l4padbindings.h" +#include "l4joy.h" // joyButtonCount / joyHatCount, the parse limits #include #include @@ -79,6 +80,20 @@ namespace { "Throttle", BindAxisThrottle }, { "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal }, { "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX }, + { "Pedals", BindAxisPedals }, + }; + + // DirectInput's axis order, which is what the joy* rows name + const NameValue kJoyAxisNames[] = + { + { "X", BindJoyAxisX }, { "Y", BindJoyAxisY }, { "Z", BindJoyAxisZ }, + { "RX", BindJoyAxisRX }, { "RY", BindJoyAxisRY }, { "RZ", BindJoyAxisRZ }, + { "SL0", BindJoyAxisSL0 }, { "SL1", BindJoyAxisSL1 }, + }; + + const NameValue kJoyHatNames[] = + { + { "up", 0 }, { "right", 1 }, { "down", 2 }, { "left", 3 }, }; Logical NameEquals(const char *a, const char *b) @@ -185,10 +200,83 @@ namespace } //--------------------------------------------------------------- - // One line of the profile grammar + // Shared tail of the two axis-source rows: [invert] [deadzone ] + // [rate ], in any order. //--------------------------------------------------------------- - Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile) + Logical ParseAxisOptions( + char *tokens[], int token_count, int first, + Logical *invert, Scalar *deadzone, Scalar *rate) { + for (int i = first; i < token_count; ++i) + { + if (NameEquals(tokens[i], "invert")) + { + *invert = True; + } + else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count) + { + if (!ParseNumber(tokens[++i], deadzone)) + { + return False; + } + } + else if (NameEquals(tokens[i], "rate") && i + 1 < token_count) + { + if (!ParseNumber(tokens[++i], rate)) + { + return False; + } + } + else + { + return False; + } + } + return True; + } + + //--------------------------------------------------------------- + // One line of the profile grammar. joy_slot carries the joydev + // state forward from line to line - joy rows attach to the slot + // most recently declared. + //--------------------------------------------------------------- + Logical ParseLine( + char *tokens[], int token_count, PadBindingProfile *profile, + int *joy_slot) + { + // + // joydev is the one row that can be two tokens long ("joydev 1"), + // so it is answered before the four-token floor below. + // + if (NameEquals(tokens[0], "joydev") && token_count >= 2) + { + int slot = -1; + if (sscanf(tokens[1], "%d", &slot) != 1 || + slot < 0 || slot >= BindJoyDeviceSlots) + { + return False; + } + *joy_slot = slot; + // + // The rest of the line is a product-name substring, rejoined + // with single spaces ("Saitek Pro Flight" is four tokens). + // + profile->joyDeviceMatch[slot][0] = '\0'; + for (int i = 2; i < token_count; ++i) + { + if (i > 2) + { + strncat(profile->joyDeviceMatch[slot], " ", + sizeof(profile->joyDeviceMatch[slot]) - + strlen(profile->joyDeviceMatch[slot]) - 1); + } + strncat(profile->joyDeviceMatch[slot], tokens[i], + sizeof(profile->joyDeviceMatch[slot]) - + strlen(profile->joyDeviceMatch[slot]) - 1); + } + return True; + } + if (token_count < 4) { return False; @@ -271,31 +359,75 @@ namespace memset(binding, 0, sizeof(*binding)); binding->source = source; binding->axis = axis; - for (int i = 4; i < token_count; ++i) + return ParseAxisOptions(tokens, token_count, 4, + &binding->invert, &binding->deadzone, &binding->rate); + } + + //--------------------------------------------------------------- + // Generic joystick rows, all attaching to the current joydev slot + //--------------------------------------------------------------- + if (NameEquals(tokens[0], "joyaxis") && NameEquals(tokens[2], "axis")) + { + int source = LookupTable(kJoyAxisNames, + sizeof(kJoyAxisNames) / sizeof(kJoyAxisNames[0]), tokens[1]); + int axis = LookupTable(kRioAxisNames, + sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]); + if (source < 0 || axis < 0 || + profile->joyAxisCount >= PadBindingProfile::maxJoyAxes) { - if (NameEquals(tokens[i], "invert")) - { - binding->invert = True; - } - else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count) - { - if (!ParseNumber(tokens[++i], &binding->deadzone)) - { - return False; - } - } - else if (NameEquals(tokens[i], "rate") && i + 1 < token_count) - { - if (!ParseNumber(tokens[++i], &binding->rate)) - { - return False; - } - } - else - { - return False; - } + return False; } + PadJoyAxisBinding *binding = &profile->joyAxes[profile->joyAxisCount++]; + memset(binding, 0, sizeof(*binding)); + binding->device = *joy_slot; + binding->source = source; + binding->axis = axis; + return ParseAxisOptions(tokens, token_count, 4, + &binding->invert, &binding->deadzone, &binding->rate); + } + + if (NameEquals(tokens[0], "joybutton") && NameEquals(tokens[2], "button")) + { + int button = -1; + int address; + if (sscanf(tokens[1], "%d", &button) != 1 || + button < 0 || button >= joyButtonCount || + !ParseAddress(tokens[3], &address) || + profile->joyButtonCount >= PadBindingProfile::maxJoyButtons) + { + return False; + } + Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle")); + PadJoyButtonBinding *binding = + &profile->joyButtons[profile->joyButtonCount++]; + memset(binding, 0, sizeof(*binding)); + binding->device = *joy_slot; + binding->button = button; + binding->address = address; + binding->toggle = toggle; + return True; + } + + if (NameEquals(tokens[0], "joyhat") && token_count >= 5 && + NameEquals(tokens[3], "button")) + { + int hat = -1; + int direction = LookupTable(kJoyHatNames, + sizeof(kJoyHatNames) / sizeof(kJoyHatNames[0]), tokens[2]); + int address; + if (sscanf(tokens[1], "%d", &hat) != 1 || + hat < 0 || hat >= joyHatCount || direction < 0 || + !ParseAddress(tokens[4], &address) || + profile->joyHatCount >= PadBindingProfile::maxJoyHats) + { + return False; + } + PadJoyHatBinding *binding = &profile->joyHats[profile->joyHatCount++]; + memset(binding, 0, sizeof(*binding)); + binding->device = *joy_slot; + binding->hat = hat; + binding->direction = direction; + binding->address = address; return True; } @@ -320,10 +452,17 @@ namespace "# key axis rate \n" "# pad