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