Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get joypad's vendor ID, product ID and name on Windows for XInput devices. #98861

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions core/input/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down Expand Up @@ -1706,7 +1705,7 @@ void Input::add_joy_mapping(const String &p_mapping, bool p_update_existing) {
for (KeyValue<int, Joypad> &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);
}
}
}
Expand All @@ -1721,11 +1720,22 @@ void Input::remove_joy_mapping(const String &p_guid) {
for (KeyValue<int, Joypad> &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) {
Expand Down
2 changes: 2 additions & 0 deletions core/input/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ class Input : public Object {

Vector<JoyDeviceMapping> 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]);
Expand Down
9 changes: 6 additions & 3 deletions doc/classes/Input.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,19 @@
<return type="String" />
<param index="0" name="device" type="int" />
<description>
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.
</description>
</method>
<method name="get_joy_info" qualifiers="const">
<return type="Dictionary" />
<param index="0" name="device" type="int" />
<description>
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.
Expand Down
143 changes: 102 additions & 41 deletions platform/windows/joypad_windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
Expand All @@ -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 } };
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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<JoypadWindows *>(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);
Expand All @@ -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<JoypadWindows *>(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;
}
Expand All @@ -302,52 +359,37 @@ 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.
}
}
}

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) {
Expand Down Expand Up @@ -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];

Expand Down Expand Up @@ -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");
Expand All @@ -560,10 +605,26 @@ 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() {
if (xinput_dll) {
FreeLibrary((HMODULE)xinput_dll);
}
}

void JoypadWindows::unload_winmm() {
if (winmm_dll) {
FreeLibrary((HMODULE)winmm_dll);
}
}
Loading
Loading