|
1 | | -#include <iostream> |
2 | | -#define WIN32_LEAN_AND_MEAN |
3 | | -#include <initguid.h> |
4 | | -#include <windows.h> |
5 | | -#include <dbt.h> |
6 | | -#include <hidclass.h> |
7 | | -#pragma comment(lib, "winmm.lib") |
8 | | -#include <mmsystem.h> |
9 | | - |
10 | | -#include <list> |
11 | | -#include <map> |
12 | | -#include <set> |
13 | | -#include <thread> |
| 1 | +#include <algorithm> |
| 2 | +#include <ppl.h> |
| 3 | +#include <vector> |
| 4 | +#include <concrt.h> |
| 5 | +#include <winerror.h> |
| 6 | +#include <winrt/Windows.Gaming.Input.h> |
14 | 7 |
|
15 | 8 | #include "gamepad.h" |
16 | 9 | #include "utils.h" |
| 10 | +#include <optional> |
| 11 | +#include <chrono> |
| 12 | +#include <GameInput.h> |
| 13 | +#include <iomanip> |
| 14 | +#include <sstream> |
| 15 | +#pragma comment(lib, "GameInput.lib") |
17 | 16 |
|
18 | 17 | Gamepads gamepads; |
19 | 18 |
|
20 | | -std::list<Event> Gamepads::diff_states(Gamepad* gamepad, |
21 | | - const JOYINFOEX& old, |
22 | | - const JOYINFOEX& current) { |
| 19 | +using namespace concurrency; |
| 20 | +using namespace winrt; |
| 21 | +using namespace Windows::Gaming; |
| 22 | +using namespace std::chrono_literals; |
| 23 | + |
| 24 | +static concurrency::critical_section m_lock{}; |
| 25 | + |
| 26 | +static IGameInput* g_gameInput = nullptr; |
| 27 | +static IGameInputDevice* g_gamepad = nullptr; |
| 28 | + |
| 29 | +std::string AppLocalDeviceIdToString(const APP_LOCAL_DEVICE_ID& id) { |
| 30 | + std::ostringstream oss; |
| 31 | + oss << std::hex << std::setfill('0'); |
| 32 | + for (size_t i = 0; i < APP_LOCAL_DEVICE_ID_SIZE; ++i) { |
| 33 | + oss << std::setw(2) << static_cast<int>(id.value[i]); |
| 34 | + } |
| 35 | + return oss.str(); |
| 36 | +} |
| 37 | + |
| 38 | +std::list<Event> diff_states(const GameInputDeviceInfo& device_info, |
| 39 | + const GameInputGamepadState& old, |
| 40 | + const GameInputGamepadState& current) { |
23 | 41 | std::time_t now = std::time(nullptr); |
24 | 42 | int time = static_cast<int>(now); |
25 | 43 |
|
26 | 44 | std::list<Event> events; |
27 | | - if (old.dwXpos != current.dwXpos) { |
| 45 | + if (old.leftThumbstickX != current.leftThumbstickX) { |
28 | 46 | events.push_back( |
29 | | - {time, "analog", "dwXpos", static_cast<int>(current.dwXpos)}); |
| 47 | + {time, "analog", "dwXpos", current.leftThumbstickX}); |
30 | 48 | } |
31 | | - if (old.dwYpos != current.dwYpos) { |
| 49 | + if (old.leftThumbstickY != current.leftThumbstickY) { |
32 | 50 | events.push_back( |
33 | | - {time, "analog", "dwYpos", static_cast<int>(current.dwYpos)}); |
| 51 | + {time, "analog", "dwYpos", current.leftThumbstickY}); |
34 | 52 | } |
35 | | - if (old.dwZpos != current.dwZpos) { |
| 53 | + if (old.rightThumbstickX != current.rightThumbstickX) { |
36 | 54 | events.push_back( |
37 | | - {time, "analog", "dwZpos", static_cast<int>(current.dwZpos)}); |
| 55 | + {time, "analog", "dwZpos", current.rightThumbstickX}); |
38 | 56 | } |
39 | | - if (old.dwRpos != current.dwRpos) { |
| 57 | + if (old.rightThumbstickY != current.rightThumbstickY) { |
40 | 58 | events.push_back( |
41 | | - {time, "analog", "dwRpos", static_cast<int>(current.dwRpos)}); |
| 59 | + {time, "analog", "dwRpos", current.rightThumbstickY}); |
42 | 60 | } |
43 | | - if (old.dwUpos != current.dwUpos) { |
| 61 | + if (old.leftTrigger != current.leftTrigger) { |
44 | 62 | events.push_back( |
45 | | - {time, "analog", "dwUpos", static_cast<int>(current.dwUpos)}); |
| 63 | + {time, "analog", "dwUpos", current.leftTrigger}); |
46 | 64 | } |
47 | | - if (old.dwVpos != current.dwVpos) { |
| 65 | + if (old.rightTrigger != current.rightTrigger) { |
48 | 66 | events.push_back( |
49 | | - {time, "analog", "dwVpos", static_cast<int>(current.dwVpos)}); |
50 | | - } |
51 | | - if (old.dwPOV != current.dwPOV) { |
52 | | - events.push_back({time, "analog", "pov", static_cast<int>(current.dwPOV)}); |
| 67 | + {time, "analog", "dwVpos", current.rightTrigger}); |
53 | 68 | } |
54 | | - if (old.dwButtons != current.dwButtons) { |
55 | | - for (int i = 0; i < gamepad->num_buttons; ++i) { |
56 | | - bool was_pressed = old.dwButtons & (1 << i); |
57 | | - bool is_pressed = current.dwButtons & (1 << i); |
| 69 | + if (old.buttons != current.buttons) { |
| 70 | + for (uint32_t i = 0; i < device_info.controllerButtonCount; ++i) { |
| 71 | + bool was_pressed = old.buttons & (1 << i); |
| 72 | + bool is_pressed = current.buttons & (1 << i); |
58 | 73 | if (was_pressed != is_pressed) { |
| 74 | + double value = is_pressed ? 1.0 : 0.0; |
59 | 75 | events.push_back( |
60 | | - {time, "button", "button-" + std::to_string(i), is_pressed}); |
| 76 | + {time, "button", "button-" + std::to_string(i), value}); |
61 | 77 | } |
62 | 78 | } |
63 | 79 | } |
64 | 80 | return events; |
65 | 81 | } |
66 | 82 |
|
67 | | -bool Gamepads::are_states_different(const JOYINFOEX& a, const JOYINFOEX& b) { |
68 | | - return a.dwXpos != b.dwXpos || a.dwYpos != b.dwYpos || a.dwZpos != b.dwZpos || |
69 | | - a.dwRpos != b.dwRpos || a.dwUpos != b.dwUpos || a.dwVpos != b.dwVpos || |
70 | | - a.dwButtons != b.dwButtons || a.dwPOV != b.dwPOV; |
| 83 | +bool are_states_different(const GameInputGamepadState& a, const GameInputGamepadState& b) { |
| 84 | + return a.leftThumbstickX != b.leftThumbstickX || |
| 85 | + a.leftThumbstickY != b.leftThumbstickY || |
| 86 | + a.leftTrigger != b.leftTrigger || |
| 87 | + a.rightThumbstickX != b.rightThumbstickX || |
| 88 | + a.rightThumbstickY != b.rightThumbstickY || |
| 89 | + a.rightTrigger != b.rightTrigger || |
| 90 | + a.buttons != b.buttons; |
71 | 91 | } |
72 | 92 |
|
73 | | -void Gamepads::read_gamepad(Gamepad* gamepad) { |
74 | | - JOYINFOEX state; |
75 | | - state.dwSize = sizeof(JOYINFOEX); |
76 | | - state.dwFlags = JOY_RETURNALL; |
77 | | - |
78 | | - int joy_id = gamepad->joy_id; |
| 93 | +void OnDeviceEvent( |
| 94 | + GameInputCallbackToken callbackToken, |
| 95 | + void* context, |
| 96 | + IGameInputReading* reading, |
| 97 | + bool hasOverrunOccurred |
| 98 | +) { |
| 99 | + //auto* self = static_cast<Gamepads*>(context); |
| 100 | + std::cout << "Gamepad event" << std::endl; |
| 101 | +} |
79 | 102 |
|
80 | | - std::cout << "Listening to gamepad " << joy_id << std::endl; |
| 103 | +void Gamepads::init() |
| 104 | +{ |
| 105 | + GameInputCreate(&g_gameInput); |
81 | 106 |
|
82 | | - while (gamepad->alive) { |
83 | | - JOYINFOEX previous_state = state; |
84 | | - MMRESULT result = joyGetPosEx(joy_id, &state); |
85 | | - if (result == JOYERR_NOERROR) { |
86 | | - if (are_states_different(previous_state, state)) { |
87 | | - std::list<Event> events = diff_states(gamepad, previous_state, state); |
88 | | - for (auto joy_event : events) { |
89 | | - if (event_emitter.has_value()) { |
90 | | - (*event_emitter)(gamepad, joy_event); |
| 107 | + if (g_gameInput != nullptr) { |
| 108 | + // Register listener for gamepad events |
| 109 | + if (g_gameInput != nullptr) { |
| 110 | + g_gameInput->RegisterDeviceCallback( |
| 111 | + nullptr, // All devices |
| 112 | + GameInputKindGamepad, |
| 113 | + GameInputDeviceConnected, |
| 114 | + GameInputAsyncEnumeration, |
| 115 | + static_cast<void*>(this), |
| 116 | + []( |
| 117 | + _In_ GameInputCallbackToken callbackToken, |
| 118 | + _In_ void * context, |
| 119 | + _In_ IGameInputDevice * device, |
| 120 | + _In_ uint64_t timestamp, |
| 121 | + _In_ GameInputDeviceStatus currentStatus, |
| 122 | + _In_ GameInputDeviceStatus previousStatus |
| 123 | + ) { |
| 124 | + auto* self = static_cast<Gamepads*>(context); |
| 125 | + if (currentStatus & GameInputDeviceConnected) { |
| 126 | + self->on_gamepad_connected(device); |
| 127 | + } else { |
| 128 | + self->on_gamepad_disconnected(device); |
91 | 129 | } |
92 | | - } |
| 130 | + }, |
| 131 | + this->deviceCallbackToken |
| 132 | + ); |
| 133 | + } |
| 134 | + |
| 135 | + /* |
| 136 | + // Currently doesn't produce any data, but perhaps in future, it can be used instead of read_thread. |
| 137 | + g_gameInput->RegisterReadingCallback( |
| 138 | + nullptr, // Any device, |
| 139 | + GameInputKindGamepad, |
| 140 | + 0.0, |
| 141 | + static_cast<void*>(this), |
| 142 | + OnDeviceEvent, |
| 143 | + this->readingCallbackToken |
| 144 | + ); |
| 145 | + */ |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +void Gamepads::stop() |
| 150 | +{ |
| 151 | + if (g_gamepad) g_gamepad->Release(); |
| 152 | + if (g_gameInput) { |
| 153 | + g_gameInput->UnregisterCallback(*this->deviceCallbackToken, 5000); |
| 154 | + //g_gameInput->UnregisterCallback(*this->readingCallbackToken, 5000); |
| 155 | + g_gameInput->Release(); |
| 156 | + } |
| 157 | + |
| 158 | + // Stop/cleanup threads |
| 159 | + for (auto gp : this->gamepads) { |
| 160 | + if (!gp->stop_thead) { |
| 161 | + if (gp->alive) { |
| 162 | + gp->stop_thead = true; |
| 163 | + } else { |
| 164 | + // Cleanup data of threads that exited due to error state. |
| 165 | + delete gp; |
93 | 166 | } |
94 | | - } else { |
95 | | - std::cout << "Fail to listen to gamepad " << joy_id << std::endl; |
96 | | - gamepad->alive = false; |
97 | | - gamepads.erase(joy_id); |
98 | 167 | } |
99 | 168 | } |
| 169 | + this->gamepads.clear(); |
| 170 | +} |
| 171 | + |
| 172 | +std::list<GamepadData*> Gamepads::get_gamepads() { |
| 173 | + return this->gamepads; |
100 | 174 | } |
101 | 175 |
|
102 | | -void Gamepads::connect_gamepad(UINT joy_id, std::string name, int num_buttons) { |
103 | | - gamepads[joy_id] = {joy_id, name, num_buttons, true}; |
| 176 | +void Gamepads::on_gamepad_connected(IGameInputDevice * device) |
| 177 | +{ |
| 178 | + auto info = device->GetDeviceInfo(); |
| 179 | + if (info == nullptr) { |
| 180 | + std::cerr << "Gamepad connected but failed to read info" << std::endl; |
| 181 | + return; |
| 182 | + } |
| 183 | + auto gp = new GamepadData(); |
| 184 | + gp->id = AppLocalDeviceIdToString(info->deviceId); |
| 185 | + gp->name = info->displayName != nullptr && info->displayName->data != nullptr ? info->displayName->data : ""; |
| 186 | + gp->num_buttons = info->controllerButtonCount; |
| 187 | + gp->stop_thead = false; |
| 188 | + gp->alive = true; |
| 189 | + this->gamepads.push_back(gp); |
| 190 | + |
| 191 | + std::cout << "Gamepad connected: " << gp->id << " : " << gp->name << std::endl; |
| 192 | + |
104 | 193 | std::thread read_thread( |
105 | | - [this, joy_id]() { read_gamepad(&gamepads[joy_id]); }); |
| 194 | + [this, gp, device]() { this->read_gamepad(gp, device); }); |
106 | 195 | read_thread.detach(); |
107 | 196 | } |
108 | 197 |
|
109 | | -void Gamepads::update_gamepads() { |
110 | | - std::cout << "Updating gamepads..." << std::endl; |
111 | | - UINT max_joysticks = joyGetNumDevs(); |
112 | | - JOYCAPSW joy_caps; |
113 | | - for (UINT joy_id = 0; joy_id < max_joysticks; ++joy_id) { |
114 | | - MMRESULT result = joyGetDevCapsW(joy_id, &joy_caps, sizeof(JOYCAPSW)); |
115 | | - if (result == JOYERR_NOERROR) { |
116 | | - std::string name = to_string(joy_caps.szPname); |
117 | | - int num_buttons = static_cast<int>(joy_caps.wNumButtons); |
118 | | - std::optional<Gamepad> gamepad = gamepads[joy_id]; |
119 | | - if (gamepad) { |
120 | | - if (gamepad->name != name) { |
121 | | - std::cout << "Updated gamepad " << joy_id << std::endl; |
122 | | - gamepad->alive = false; |
123 | | - gamepads.erase(joy_id); |
124 | | - |
125 | | - connect_gamepad(joy_id, name, num_buttons); |
126 | | - } |
127 | | - } else { |
128 | | - std::cout << "New gamepad connected " << joy_id << std::endl; |
129 | | - connect_gamepad(joy_id, name, num_buttons); |
130 | | - } |
| 198 | +void Gamepads::on_gamepad_disconnected(IGameInputDevice * device) |
| 199 | +{ |
| 200 | + auto info = device->GetDeviceInfo(); |
| 201 | + if (info == nullptr) { |
| 202 | + std::cerr << "Gamepad disconnected but failed to read info" << std::endl; |
| 203 | + return; |
| 204 | + } |
| 205 | + std::string removeId = AppLocalDeviceIdToString(info->deviceId); |
| 206 | + std::cout << "Gamepad disconnected: " << removeId << std::endl; |
| 207 | + GamepadData* removeGp = nullptr; |
| 208 | + for (auto gp : this->gamepads) { |
| 209 | + if (gp->id == removeId) { |
| 210 | + gp->stop_thead = true; |
| 211 | + removeGp = gp; |
| 212 | + break; |
131 | 213 | } |
132 | 214 | } |
| 215 | + // Remove the gamepad from list. The thread will free up memory. |
| 216 | + if (removeGp != nullptr) { |
| 217 | + this->gamepads.remove(removeGp); |
| 218 | + } |
133 | 219 | } |
134 | 220 |
|
135 | | -std::set<std::wstring> connected_devices; |
136 | | - |
137 | | -std::optional<LRESULT> CALLBACK GamepadListenerProc(HWND hwnd, |
138 | | - UINT uMsg, |
139 | | - WPARAM wParam, |
140 | | - LPARAM lParam) { |
141 | | - switch (uMsg) { |
142 | | - case WM_DEVICECHANGE: { |
143 | | - if (lParam != NULL) { |
144 | | - PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam; |
145 | | - if (pHdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { |
146 | | - PDEV_BROADCAST_DEVICEINTERFACE pDevInterface = |
147 | | - (PDEV_BROADCAST_DEVICEINTERFACE)pHdr; |
148 | | - if (IsEqualGUID(pDevInterface->dbcc_classguid, |
149 | | - GUID_DEVINTERFACE_HID)) { |
150 | | - std::wstring device_path = pDevInterface->dbcc_name; |
151 | | - bool is_connected = |
152 | | - connected_devices.find(device_path) != connected_devices.end(); |
153 | | - if (!is_connected && wParam == DBT_DEVICEARRIVAL) { |
154 | | - connected_devices.insert(device_path); |
155 | | - gamepads.update_gamepads(); |
156 | | - } else if (is_connected && wParam == DBT_DEVICEREMOVECOMPLETE) { |
157 | | - connected_devices.erase(device_path); |
158 | | - gamepads.update_gamepads(); |
| 221 | + |
| 222 | +void Gamepads::read_gamepad(GamepadData* gamepad, IGameInputDevice* device) { |
| 223 | + auto info = device->GetDeviceInfo(); |
| 224 | + |
| 225 | + GameInputGamepadState previous_state; |
| 226 | + while (info != nullptr && !gamepad->stop_thead && g_gameInput != nullptr) { |
| 227 | + IGameInputReading* reading; |
| 228 | + GameInputGamepadState state; |
| 229 | + g_gameInput->GetCurrentReading(GameInputKindGamepad, device, &reading); |
| 230 | + if (reading != nullptr) { |
| 231 | + if(reading->GetGamepadState(&state)) { |
| 232 | + if (are_states_different(previous_state, state)) { |
| 233 | + auto events = diff_states(*info, state, previous_state); |
| 234 | + for (auto event : events) { |
| 235 | + if (event_emitter.has_value()) { |
| 236 | + (*event_emitter)(gamepad, event); |
159 | 237 | } |
160 | 238 | } |
161 | 239 | } |
| 240 | + previous_state = state; |
| 241 | + reading->Release(); |
162 | 242 | } |
163 | | - return 0; |
164 | | - } |
165 | | - case WM_DESTROY: { |
166 | | - PostQuitMessage(0); |
167 | | - return 0; |
168 | 243 | } |
| 244 | + |
| 245 | + Sleep(1); |
| 246 | + } |
| 247 | + |
| 248 | + if (gamepad->stop_thead) { |
| 249 | + std::cout << "Gamepad thread exit (via signal) " << gamepad->id << std::endl; |
| 250 | + delete gamepad; |
| 251 | + } else { |
| 252 | + std::cout << "Gamepad thread exit (due to error state) " << gamepad->id << std::endl; |
| 253 | + gamepad->alive = false; |
169 | 254 | } |
170 | | - return std::nullopt; |
171 | 255 | } |
0 commit comments