From 01a2726c5919a64d012123003f2569e25750708a Mon Sep 17 00:00:00 2001 From: MJacred Date: Sun, 15 Dec 2024 23:59:11 +0100 Subject: [PATCH] Get joypad's vendor ID, product ID, and name on Windows --- core/input/input.cpp | 18 +++- core/input/input.h | 2 + doc/classes/Input.xml | 9 +- platform/windows/joypad_windows.cpp | 143 ++++++++++++++++++++-------- platform/windows/joypad_windows.h | 19 +++- 5 files changed, 138 insertions(+), 53 deletions(-) diff --git a/core/input/input.cpp b/core/input/input.cpp index 5314e9f02d1c..c50e8b0163cc 100644 --- a/core/input/input.cpp +++ b/core/input/input.cpp @@ -609,10 +609,9 @@ void Input::joy_connection_changed(int p_idx, bool p_connected, const String &p_ for (int i = 0; i < map_db.size(); i++) { if (js.uid == map_db[i].uid) { mapping = i; - js.name = map_db[i].name; } } - js.mapping = mapping; + _set_joypad_mapping(js, mapping); } else { js.connected = false; for (int i = 0; i < (int)JoyButton::MAX; i++) { @@ -1706,7 +1705,7 @@ void Input::add_joy_mapping(const String &p_mapping, bool p_update_existing) { for (KeyValue &E : joy_names) { Joypad &joy = E.value; if (joy.uid == uid) { - joy.mapping = map_db.size() - 1; + _set_joypad_mapping(joy, map_db.size() - 1); } } } @@ -1721,11 +1720,22 @@ void Input::remove_joy_mapping(const String &p_guid) { for (KeyValue &E : joy_names) { Joypad &joy = E.value; if (joy.uid == p_guid) { - joy.mapping = -1; + _set_joypad_mapping(joy, -1); } } } +void Input::_set_joypad_mapping(Joypad &p_js, int p_map_index) { + if (p_map_index != fallback_mapping && p_map_index >= 0 && p_map_index < map_db.size() && p_js.uid != "__XINPUT_DEVICE__") { + // Prefer the joypad name defined in the mapping. + // Exceptions: + // * On Windows for XInput devices the mapping would change the joypad's name to a collective name. + // * A fallback mapping is not allowed to override the joypad's name. + p_js.name = map_db[p_map_index].name; + } + p_js.mapping = p_map_index; +} + void Input::set_fallback_mapping(const String &p_guid) { for (int i = 0; i < map_db.size(); i++) { if (map_db[i].uid == p_guid) { diff --git a/core/input/input.h b/core/input/input.h index 005ddcca4fdb..81722d013e6d 100644 --- a/core/input/input.h +++ b/core/input/input.h @@ -245,6 +245,8 @@ class Input : public Object { Vector map_db; + void _set_joypad_mapping(Joypad &p_js, int p_map_index); + JoyEvent _get_mapped_button_event(const JoyDeviceMapping &mapping, JoyButton p_button); JoyEvent _get_mapped_axis_event(const JoyDeviceMapping &mapping, JoyAxis p_axis, float p_value, JoyAxisRange &r_range); void _get_mapped_hat_events(const JoyDeviceMapping &mapping, HatDir p_hat, JoyEvent r_events[(size_t)HatDir::MAX]); diff --git a/doc/classes/Input.xml b/doc/classes/Input.xml index 6fe5b7a80229..3a8bbb246a49 100644 --- a/doc/classes/Input.xml +++ b/doc/classes/Input.xml @@ -118,7 +118,8 @@ - Returns an SDL2-compatible device GUID on platforms that use gamepad remapping, e.g. [code]030000004c050000c405000000010000[/code]. Returns [code]"Default Gamepad"[/code] otherwise. Godot uses the [url=https://github.com/gabomdq/SDL_GameControllerDB]SDL2 game controller database[/url] to determine gamepad names and mappings based on this GUID. + Returns an SDL2-compatible device GUID on platforms that use gamepad remapping, e.g. [code]030000004c050000c405000000010000[/code]. Returns an empty string if it cannot be found. Godot uses the [url=https://github.com/gabomdq/SDL_GameControllerDB]SDL2 game controller database[/url] to determine gamepad names and mappings based on this GUID. + On Windows, all XInput joypad GUIDs will be overridden by Godot to [code]__XINPUT_DEVICE__[/code], because their mappings are the same. @@ -126,8 +127,10 @@ Returns a dictionary with extra platform-specific information about the device, e.g. the raw gamepad name from the OS or the Steam Input index. - On Windows the dictionary contains the following fields: - [code]xinput_index[/code]: The index of the controller in the XInput system. + On Windows, the dictionary contains the following fields: + [code]xinput_index[/code]: The index of the controller in the XInput system. Undefined for DirectInput devices. + [code]vendor_id[/code]: The USB vendor ID of the device. + [code]product_id[/code]: The USB product ID of the device. On Linux: [code]raw_name[/code]: The name of the controller as it came from the OS, before getting renamed by the godot controller database. [code]vendor_id[/code]: The USB vendor ID of the device. diff --git a/platform/windows/joypad_windows.cpp b/platform/windows/joypad_windows.cpp index 0f55cda5d744..7a4cc08f81d7 100644 --- a/platform/windows/joypad_windows.cpp +++ b/platform/windows/joypad_windows.cpp @@ -46,17 +46,23 @@ DWORD WINAPI _xinput_set_state(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration) return ERROR_DEVICE_NOT_CONNECTED; } +MMRESULT WINAPI _winmm_get_joycaps(UINT uJoyID, LPJOYCAPSW pjc, UINT cbjc) { + return MMSYSERR_NODRIVER; +} + JoypadWindows::JoypadWindows() { } JoypadWindows::JoypadWindows(HWND *hwnd) { input = Input::get_singleton(); hWnd = hwnd; - joypad_count = 0; + x_joypad_probe_count = 0; + d_joypad_count = 0; dinput = nullptr; xinput_dll = nullptr; xinput_get_state = nullptr; xinput_set_state = nullptr; + winmm_get_joycaps = nullptr; load_xinput(); @@ -79,14 +85,15 @@ JoypadWindows::JoypadWindows(HWND *hwnd) { } JoypadWindows::~JoypadWindows() { - close_joypad(); + close_d_joypad(); if (dinput) { dinput->Release(); } + unload_winmm(); unload_xinput(); } -bool JoypadWindows::have_device(const GUID &p_guid) { +bool JoypadWindows::is_d_joypad_known(const GUID &p_guid) { for (int i = 0; i < JOYPADS_MAX; i++) { if (d_joypads[i].guid == p_guid) { d_joypads[i].confirmed = true; @@ -97,7 +104,7 @@ bool JoypadWindows::have_device(const GUID &p_guid) { } // adapted from SDL2, works a lot better than the MSDN version -bool JoypadWindows::is_xinput_device(const GUID *p_guid) { +bool JoypadWindows::is_xinput_joypad(const GUID *p_guid) { static GUID IID_ValveStreamingGamepad = { MAKELONG(0x28DE, 0x11FF), 0x28DE, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } }; static GUID IID_X360WiredGamepad = { MAKELONG(0x045E, 0x02A1), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } }; static GUID IID_X360WirelessGamepad = { MAKELONG(0x045E, 0x028E), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } }; @@ -159,12 +166,56 @@ bool JoypadWindows::is_xinput_device(const GUID *p_guid) { return false; } +void JoypadWindows::probe_xinput_joypad(const String &name) { + if (x_joypad_probe_count >= XUSER_MAX_COUNT) { + return; + } + int i = x_joypad_probe_count; + x_joypad_probe_count++; + + ZeroMemory(&x_joypads[i].state, sizeof(XINPUT_STATE)); + + DWORD dwResult = xinput_get_state(i, &x_joypads[i].state); + if (dwResult == ERROR_SUCCESS) { + int id = input->get_unused_joy_id(); + if (id != -1 && !x_joypads[i].attached) { + x_joypads[i].attached = true; + x_joypads[i].id = id; + x_joypads[i].ff_timestamp = 0; + x_joypads[i].ff_end_timestamp = 0; + x_joypads[i].vibrating = false; + attached_joypads[id] = true; + Dictionary joypad_info; + String joypad_name; + + joypad_info["xinput_index"] = (int)i; + + JOYCAPSW jc; + memset(&jc, 0, sizeof(JOYCAPSW)); + MMRESULT jcResult = winmm_get_joycaps((UINT)id, &jc, sizeof(JOYCAPSW)); + if (jcResult == JOYERR_NOERROR) { + joypad_info["vendor_id"] = itos(jc.wMid); + joypad_info["product_id"] = itos(jc.wPid); + if (!name.is_empty()) { + joypad_name = name.trim_prefix("Controller (").trim_suffix(")"); + } + } + + input->joy_connection_changed(id, true, joypad_name, "__XINPUT_DEVICE__", joypad_info); + } + } else if (x_joypads[i].attached) { + x_joypads[i].attached = false; + attached_joypads[x_joypads[i].id] = false; + input->joy_connection_changed(x_joypads[i].id, false, ""); + } +} + bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) { ERR_FAIL_NULL_V_MSG(dinput, false, "DirectInput not initialized. Rebooting your PC may solve this issue."); HRESULT hr; int num = input->get_unused_joy_id(); - if (have_device(instance->guidInstance) || num == -1) { + if (is_d_joypad_known(instance->guidInstance) || num == -1) { return false; } @@ -193,6 +244,10 @@ bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) { WORD version = 0; sprintf_s(uid, "%04x%04x%04x%04x%04x%04x%04x%04x", type, 0, vendor, 0, product, 0, version, 0); + Dictionary joypad_info; + joypad_info["vendor_id"] = itos(vendor); + joypad_info["product_id"] = itos(product); + id_to_change = num; slider_count = 0; @@ -202,16 +257,17 @@ bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) { joy->joy_axis.sort(); joy->guid = instance->guidInstance; - input->joy_connection_changed(num, true, instance->tszProductName, uid); + const String &name = String(instance->tszProductName).trim_prefix("Controller (").trim_suffix(")"); + input->joy_connection_changed(num, true, name, uid, joypad_info); joy->attached = true; joy->id = num; attached_joypads[num] = true; joy->confirmed = true; - joypad_count++; + d_joypad_count++; return true; } -void JoypadWindows::setup_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_joy_id) { +void JoypadWindows::setup_d_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_joy_id) { if (ob->dwType & DIDFT_AXIS) { HRESULT res; DIPROPRANGE prop_range; @@ -270,7 +326,8 @@ void JoypadWindows::setup_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_ BOOL CALLBACK JoypadWindows::enumCallback(const DIDEVICEINSTANCE *p_instance, void *p_context) { JoypadWindows *self = static_cast(p_context); - if (self->is_xinput_device(&p_instance->guidProduct)) { + if (self->is_xinput_joypad(&p_instance->guidProduct)) { + self->probe_xinput_joypad(p_instance->tszProductName); return DIENUM_CONTINUE; } self->setup_dinput_joypad(p_instance); @@ -279,15 +336,15 @@ BOOL CALLBACK JoypadWindows::enumCallback(const DIDEVICEINSTANCE *p_instance, vo BOOL CALLBACK JoypadWindows::objectsCallback(const DIDEVICEOBJECTINSTANCE *p_instance, void *p_context) { JoypadWindows *self = static_cast(p_context); - self->setup_joypad_object(p_instance, self->id_to_change); + self->setup_d_joypad_object(p_instance, self->id_to_change); return DIENUM_CONTINUE; } -void JoypadWindows::close_joypad(int id) { +void JoypadWindows::close_d_joypad(int id) { if (id == -1) { for (int i = 0; i < JOYPADS_MAX; i++) { - close_joypad(i); + close_d_joypad(i); } return; } @@ -302,45 +359,29 @@ void JoypadWindows::close_joypad(int id) { attached_joypads[d_joypads[id].id] = false; d_joypads[id].guid.Data1 = d_joypads[id].guid.Data2 = d_joypads[id].guid.Data3 = 0; input->joy_connection_changed(d_joypads[id].id, false, ""); - joypad_count--; + d_joypad_count--; } void JoypadWindows::probe_joypads() { ERR_FAIL_NULL_MSG(dinput, "DirectInput not initialized. Rebooting your PC may solve this issue."); - DWORD dwResult; - for (DWORD i = 0; i < XUSER_MAX_COUNT; i++) { - ZeroMemory(&x_joypads[i].state, sizeof(XINPUT_STATE)); - - dwResult = xinput_get_state(i, &x_joypads[i].state); - if (dwResult == ERROR_SUCCESS) { - int id = input->get_unused_joy_id(); - if (id != -1 && !x_joypads[i].attached) { - x_joypads[i].attached = true; - x_joypads[i].id = id; - x_joypads[i].ff_timestamp = 0; - x_joypads[i].ff_end_timestamp = 0; - x_joypads[i].vibrating = false; - attached_joypads[id] = true; - Dictionary joypad_info; - joypad_info["xinput_index"] = (int)i; - input->joy_connection_changed(id, true, "XInput Gamepad", "__XINPUT_DEVICE__", joypad_info); - } - } else if (x_joypads[i].attached) { - x_joypads[i].attached = false; - attached_joypads[x_joypads[i].id] = false; - input->joy_connection_changed(x_joypads[i].id, false, ""); - } - } - for (int i = 0; i < joypad_count; i++) { - d_joypads[i].confirmed = false; + for (int i = 0; i < d_joypad_count; i++) { + d_joypads[i].confirmed = false; // Flag DirectInput devices for re-checking their availability. } + x_joypad_probe_count = 0; + // Probe _all attached_ joypad devices. dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, enumCallback, this, DIEDFL_ATTACHEDONLY); - for (int i = 0; i < joypad_count; i++) { + for (int i = x_joypad_probe_count; i < XUSER_MAX_COUNT; i++) { + // Handle disconnect of XInput devices. + // And act as a fallback, just in case DirectInput could not find the device. + probe_xinput_joypad(); + } + + for (int i = 0; i < d_joypad_count; i++) { if (!d_joypads[i].confirmed) { - close_joypad(i); + close_d_joypad(i); // Any DirectInput device not found during probing is considered as disconnected. } } } @@ -348,6 +389,7 @@ void JoypadWindows::probe_joypads() { void JoypadWindows::process_joypads() { HRESULT hr; + // Handle XInput joypads. for (int i = 0; i < XUSER_MAX_COUNT; i++) { xinput_gamepad &joy = x_joypads[i]; if (!joy.attached) { @@ -388,6 +430,7 @@ void JoypadWindows::process_joypads() { } } + // Handle DirectIndput joypads. for (int i = 0; i < JOYPADS_MAX; i++) { dinput_gamepad *joy = &d_joypads[i]; @@ -535,7 +578,9 @@ void JoypadWindows::joypad_vibration_stop_xinput(int p_device, uint64_t p_timest void JoypadWindows::load_xinput() { xinput_get_state = &_xinput_get_state; xinput_set_state = &_xinput_set_state; + winmm_get_joycaps = &_winmm_get_joycaps; bool legacy_xinput = false; + xinput_dll = LoadLibrary("XInput1_4.dll"); if (!xinput_dll) { xinput_dll = LoadLibrary("XInput1_3.dll"); @@ -560,6 +605,16 @@ void JoypadWindows::load_xinput() { } xinput_get_state = func; xinput_set_state = set_func; + + winmm_dll = LoadLibrary("Winmm.dll"); + if (winmm_dll) { + joyGetDevCaps_t caps_func = (joyGetDevCaps_t)GetProcAddress((HMODULE)winmm_dll, "joyGetDevCapsW"); + if (caps_func) { + winmm_get_joycaps = caps_func; + } else { + unload_winmm(); + } + } } void JoypadWindows::unload_xinput() { @@ -567,3 +622,9 @@ void JoypadWindows::unload_xinput() { FreeLibrary((HMODULE)xinput_dll); } } + +void JoypadWindows::unload_winmm() { + if (winmm_dll) { + FreeLibrary((HMODULE)winmm_dll); + } +} diff --git a/platform/windows/joypad_windows.h b/platform/windows/joypad_windows.h index 87c7af76577d..b56f80cb570b 100644 --- a/platform/windows/joypad_windows.h +++ b/platform/windows/joypad_windows.h @@ -37,6 +37,8 @@ #include #include +#include + #ifndef SAFE_RELEASE // when Windows Media Device M? is not present #define SAFE_RELEASE(x) \ if (x != nullptr) { \ @@ -107,14 +109,18 @@ class JoypadWindows { typedef DWORD(WINAPI *XInputGetState_t)(DWORD dwUserIndex, XINPUT_STATE *pState); typedef DWORD(WINAPI *XInputSetState_t)(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration); + typedef MMRESULT(WINAPI *joyGetDevCaps_t)(UINT uJoyID, LPJOYCAPSW pjc, UINT cbjc); + HWND *hWnd = nullptr; HANDLE xinput_dll; + HANDLE winmm_dll; LPDIRECTINPUT8 dinput; Input *input = nullptr; int id_to_change; int slider_count; - int joypad_count; + int x_joypad_probe_count; // XInput equivalent to dinput_gamepad.confirmed. + int d_joypad_count; bool attached_joypads[JOYPADS_MAX]; dinput_gamepad d_joypads[JOYPADS_MAX]; xinput_gamepad x_joypads[XUSER_MAX_COUNT]; @@ -122,22 +128,25 @@ class JoypadWindows { static BOOL CALLBACK enumCallback(const DIDEVICEINSTANCE *p_instance, void *p_context); static BOOL CALLBACK objectsCallback(const DIDEVICEOBJECTINSTANCE *instance, void *context); - void setup_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_joy_id); - void close_joypad(int id = -1); + void setup_d_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_joy_id); + void close_d_joypad(int id = -1); void load_xinput(); void unload_xinput(); + void unload_winmm(); void post_hat(int p_device, DWORD p_dpad); - bool have_device(const GUID &p_guid); - bool is_xinput_device(const GUID *p_guid); + bool is_d_joypad_known(const GUID &p_guid); + bool is_xinput_joypad(const GUID *p_guid); bool setup_dinput_joypad(const DIDEVICEINSTANCE *instance); + void probe_xinput_joypad(const String &name = ""); // Handles connect, disconnect & re-connect for XInput joypads. void joypad_vibration_start_xinput(int p_device, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp); void joypad_vibration_stop_xinput(int p_device, uint64_t p_timestamp); float axis_correct(int p_val, bool p_xinput = false, bool p_trigger = false, bool p_negate = false) const; XInputGetState_t xinput_get_state; XInputSetState_t xinput_set_state; + joyGetDevCaps_t winmm_get_joycaps; // Only for reading info on XInput joypads. }; #endif // JOYPAD_WINDOWS_H