1 | /** |
2 | * Copyright (c) 2006-2023 LOVE Development Team |
3 | * |
4 | * This software is provided 'as-is', without any express or implied |
5 | * warranty. In no event will the authors be held liable for any damages |
6 | * arising from the use of this software. |
7 | * |
8 | * Permission is granted to anyone to use this software for any purpose, |
9 | * including commercial applications, and to alter it and redistribute it |
10 | * freely, subject to the following restrictions: |
11 | * |
12 | * 1. The origin of this software must not be misrepresented; you must not |
13 | * claim that you wrote the original software. If you use this software |
14 | * in a product, an acknowledgment in the product documentation would be |
15 | * appreciated but is not required. |
16 | * 2. Altered source versions must be plainly marked as such, and must not be |
17 | * misrepresented as being the original software. |
18 | * 3. This notice may not be removed or altered from any source distribution. |
19 | **/ |
20 | |
21 | #include "common/config.h" |
22 | #include "JoystickModule.h" |
23 | #include "Joystick.h" |
24 | |
25 | // SDL |
26 | #include <SDL.h> |
27 | |
28 | // C++ |
29 | #include <sstream> |
30 | #include <algorithm> |
31 | |
32 | // C |
33 | #include <cstdlib> |
34 | |
35 | namespace love |
36 | { |
37 | namespace joystick |
38 | { |
39 | namespace sdl |
40 | { |
41 | |
42 | JoystickModule::JoystickModule() |
43 | { |
44 | if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) |
45 | throw love::Exception("Could not initialize SDL joystick subsystem (%s)" , SDL_GetError()); |
46 | |
47 | // Initialize any joysticks which are already connected. |
48 | for (int i = 0; i < SDL_NumJoysticks(); i++) |
49 | addJoystick(i); |
50 | |
51 | // Start joystick event watching. Joysticks are automatically added and |
52 | // removed via love.event. |
53 | SDL_JoystickEventState(SDL_ENABLE); |
54 | SDL_GameControllerEventState(SDL_ENABLE); |
55 | } |
56 | |
57 | JoystickModule::~JoystickModule() |
58 | { |
59 | // Close any open Joysticks. |
60 | for (auto stick : joysticks) |
61 | { |
62 | stick->close(); |
63 | stick->release(); |
64 | } |
65 | |
66 | if (SDL_WasInit(SDL_INIT_HAPTIC) != 0) |
67 | SDL_QuitSubSystem(SDL_INIT_HAPTIC); |
68 | |
69 | SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER); |
70 | } |
71 | |
72 | const char *JoystickModule::getName() const |
73 | { |
74 | return "love.joystick.sdl" ; |
75 | } |
76 | |
77 | love::joystick::Joystick *JoystickModule::getJoystick(int joyindex) |
78 | { |
79 | if (joyindex < 0 || (size_t) joyindex >= activeSticks.size()) |
80 | return nullptr; |
81 | |
82 | return activeSticks[joyindex]; |
83 | } |
84 | |
85 | int JoystickModule::getIndex(const love::joystick::Joystick *joystick) |
86 | { |
87 | for (int i = 0; i < (int) activeSticks.size(); i++) |
88 | { |
89 | if (activeSticks[i] == joystick) |
90 | return i; |
91 | } |
92 | |
93 | // Joystick is not connected. |
94 | return -1; |
95 | } |
96 | |
97 | int JoystickModule::getJoystickCount() const |
98 | { |
99 | return (int) activeSticks.size(); |
100 | } |
101 | |
102 | love::joystick::Joystick *JoystickModule::getJoystickFromID(int instanceid) |
103 | { |
104 | for (auto stick : activeSticks) |
105 | { |
106 | if (stick->getInstanceID() == instanceid) |
107 | return stick; |
108 | } |
109 | |
110 | return nullptr; |
111 | } |
112 | |
113 | love::joystick::Joystick *JoystickModule::addJoystick(int deviceindex) |
114 | { |
115 | if (deviceindex < 0 || deviceindex >= SDL_NumJoysticks()) |
116 | return nullptr; |
117 | |
118 | std::string guidstr = getDeviceGUID(deviceindex); |
119 | joystick::Joystick *joystick = 0; |
120 | bool reused = false; |
121 | |
122 | for (auto stick : joysticks) |
123 | { |
124 | // Try to re-use a disconnected Joystick with the same GUID. |
125 | if (!stick->isConnected() && stick->getGUID() == guidstr) |
126 | { |
127 | joystick = stick; |
128 | reused = true; |
129 | break; |
130 | } |
131 | } |
132 | |
133 | if (!joystick) |
134 | { |
135 | joystick = new Joystick((int) joysticks.size()); |
136 | joysticks.push_back(joystick); |
137 | } |
138 | |
139 | // Make sure the Joystick object isn't in the active list already. |
140 | removeJoystick(joystick); |
141 | |
142 | if (!joystick->open(deviceindex)) |
143 | return nullptr; |
144 | |
145 | // Make sure multiple instances of the same physical joystick aren't added |
146 | // to the active list. |
147 | for (auto activestick : activeSticks) |
148 | { |
149 | if (joystick->getHandle() == activestick->getHandle()) |
150 | { |
151 | joystick->close(); |
152 | |
153 | // If we just created the stick, remove it since it's a duplicate. |
154 | if (!reused) |
155 | { |
156 | joysticks.remove(joystick); |
157 | joystick->release(); |
158 | } |
159 | |
160 | return activestick; |
161 | } |
162 | } |
163 | |
164 | if (joystick->isGamepad()) |
165 | recentGamepadGUIDs[joystick->getGUID()] = true; |
166 | |
167 | activeSticks.push_back(joystick); |
168 | return joystick; |
169 | } |
170 | |
171 | void JoystickModule::removeJoystick(love::joystick::Joystick *joystick) |
172 | { |
173 | if (!joystick) |
174 | return; |
175 | |
176 | // Close the Joystick and remove it from the active joystick list. |
177 | auto it = std::find(activeSticks.begin(), activeSticks.end(), joystick); |
178 | if (it != activeSticks.end()) |
179 | { |
180 | (*it)->close(); |
181 | activeSticks.erase(it); |
182 | } |
183 | } |
184 | |
185 | bool JoystickModule::setGamepadMapping(const std::string &guid, Joystick::GamepadInput gpinput, Joystick::JoystickInput joyinput) |
186 | { |
187 | // All SDL joystick GUID strings are 32 characters. |
188 | if (guid.length() != 32) |
189 | throw love::Exception("Invalid joystick GUID: %s" , guid.c_str()); |
190 | |
191 | SDL_JoystickGUID sdlguid = SDL_JoystickGetGUIDFromString(guid.c_str()); |
192 | std::string mapstr; |
193 | |
194 | char *sdlmapstr = SDL_GameControllerMappingForGUID(sdlguid); |
195 | if (sdlmapstr) |
196 | { |
197 | mapstr = sdlmapstr; |
198 | SDL_free(sdlmapstr); |
199 | } |
200 | else |
201 | { |
202 | std::string name = "Controller" ; |
203 | |
204 | for (love::joystick::Joystick *stick : joysticks) |
205 | { |
206 | if (stick->getGUID() == guid) |
207 | { |
208 | name = stick->getName(); |
209 | break; |
210 | } |
211 | } |
212 | |
213 | mapstr = guid + "," + name + "," ; |
214 | } |
215 | |
216 | std::stringstream joyinputstream; |
217 | Uint8 sdlhat; |
218 | |
219 | // We can't have negative int values in the bind string. |
220 | switch (joyinput.type) |
221 | { |
222 | case Joystick::INPUT_TYPE_AXIS: |
223 | if (joyinput.axis >= 0) |
224 | joyinputstream << "a" << joyinput.axis; |
225 | break; |
226 | case Joystick::INPUT_TYPE_BUTTON: |
227 | if (joyinput.button >= 0) |
228 | joyinputstream << "b" << joyinput.button; |
229 | break; |
230 | case Joystick::INPUT_TYPE_HAT: |
231 | if (joyinput.hat.index >= 0 && Joystick::getConstant(joyinput.hat.value, sdlhat)) |
232 | joyinputstream << "h" << joyinput.hat.index << "." << int(sdlhat); |
233 | break; |
234 | default: |
235 | break; |
236 | } |
237 | |
238 | std::string joyinputstr = joyinputstream.str(); |
239 | |
240 | if (joyinputstr.length() == 0) |
241 | throw love::Exception("Invalid joystick input value." ); |
242 | |
243 | // SDL's name for the gamepad input value, e.g. "guide". |
244 | std::string gpinputname = stringFromGamepadInput(gpinput); |
245 | |
246 | // We should remove any existing joystick bind for this gamepad buttton/axis |
247 | // so SDL's parser doesn't get mixed up. |
248 | removeBindFromMapString(mapstr, joyinputstr); |
249 | |
250 | // The string we'll be adding to the mapping string, e.g. "guide:b10," |
251 | std::string insertstr = gpinputname + ":" + joyinputstr + "," ; |
252 | |
253 | // We should replace any existing gamepad bind. |
254 | size_t findpos = mapstr.find("," + gpinputname + ":" ); |
255 | if (findpos != std::string::npos) |
256 | { |
257 | // The bind string ends at the next comma, or the end of the string. |
258 | size_t endpos = mapstr.find_first_of(',', findpos + 1); |
259 | if (endpos == std::string::npos) |
260 | endpos = mapstr.length() - 1; |
261 | |
262 | mapstr.replace(findpos + 1, endpos - findpos + 1, insertstr); |
263 | } |
264 | else |
265 | { |
266 | // Just append to the end if we don't need to replace anything. |
267 | mapstr += insertstr; |
268 | } |
269 | |
270 | // 1 == added, 0 == updated, -1 == error. |
271 | int status = SDL_GameControllerAddMapping(mapstr.c_str()); |
272 | |
273 | if (status != -1) |
274 | recentGamepadGUIDs[guid] = true; |
275 | |
276 | // FIXME: massive hack until missing APIs are added to SDL 2: |
277 | // https://bugzilla.libsdl.org/show_bug.cgi?id=1975 |
278 | if (status == 1) |
279 | checkGamepads(guid); |
280 | |
281 | return status >= 0; |
282 | } |
283 | |
284 | std::string JoystickModule::stringFromGamepadInput(Joystick::GamepadInput gpinput) const |
285 | { |
286 | SDL_GameControllerAxis sdlaxis; |
287 | SDL_GameControllerButton sdlbutton; |
288 | |
289 | const char *gpinputname = nullptr; |
290 | |
291 | switch (gpinput.type) |
292 | { |
293 | case Joystick::INPUT_TYPE_AXIS: |
294 | if (Joystick::getConstant(gpinput.axis, sdlaxis)) |
295 | gpinputname = SDL_GameControllerGetStringForAxis(sdlaxis); |
296 | break; |
297 | case Joystick::INPUT_TYPE_BUTTON: |
298 | if (Joystick::getConstant(gpinput.button, sdlbutton)) |
299 | gpinputname = SDL_GameControllerGetStringForButton(sdlbutton); |
300 | break; |
301 | default: |
302 | break; |
303 | } |
304 | |
305 | if (!gpinputname) |
306 | throw love::Exception("Invalid gamepad axis/button." ); |
307 | |
308 | return std::string(gpinputname); |
309 | } |
310 | |
311 | void JoystickModule::removeBindFromMapString(std::string &mapstr, const std::string &joybindstr) const |
312 | { |
313 | // Find the joystick part of the bind in the string. |
314 | size_t joybindpos = mapstr.find(joybindstr + "," ); |
315 | if (joybindpos == std::string::npos) |
316 | { |
317 | joybindpos = mapstr.rfind(joybindstr); |
318 | if (joybindpos != mapstr.length() - joybindstr.length()) |
319 | return; |
320 | } |
321 | |
322 | if (joybindpos == std::string::npos) |
323 | return; |
324 | |
325 | // Find the start of the entire bind by looking for the separator between |
326 | // the end of one section of the map string and the start of this section. |
327 | size_t bindstart = mapstr.rfind(',', joybindpos); |
328 | if (bindstart != std::string::npos && bindstart < mapstr.length() - 1) |
329 | { |
330 | // The start of the bind is directly after the separator. |
331 | bindstart++; |
332 | |
333 | size_t bindend = mapstr.find(',', bindstart + 1); |
334 | if (bindend == std::string::npos) |
335 | bindend = mapstr.length() - 1; |
336 | |
337 | // Replace it with an empty string (remove it.) |
338 | mapstr.replace(bindstart, bindend - bindstart + 1, "" ); |
339 | } |
340 | } |
341 | |
342 | void JoystickModule::checkGamepads(const std::string &guid) const |
343 | { |
344 | // FIXME: massive hack until missing APIs are added to SDL 2: |
345 | // https://bugzilla.libsdl.org/show_bug.cgi?id=1975 |
346 | |
347 | // Make sure all connected joysticks of a certain guid that are |
348 | // gamepad-capable are opened as such. |
349 | for (int d_index = 0; d_index < SDL_NumJoysticks(); d_index++) |
350 | { |
351 | if (!SDL_IsGameController(d_index)) |
352 | continue; |
353 | |
354 | if (guid.compare(getDeviceGUID(d_index)) != 0) |
355 | continue; |
356 | |
357 | for (auto stick : activeSticks) |
358 | { |
359 | if (guid.compare(stick->getGUID()) != 0) |
360 | continue; |
361 | |
362 | // Big hack time: open the index as a game controller and compare |
363 | // the underlying joystick handle to the active stick's. |
364 | SDL_GameController *controller = SDL_GameControllerOpen(d_index); |
365 | if (controller == nullptr) |
366 | continue; |
367 | |
368 | // GameController objects are reference-counted in SDL, so we don't want to |
369 | // have a joystick open when trying to re-initialize it |
370 | SDL_Joystick *sdlstick = SDL_GameControllerGetJoystick(controller); |
371 | bool open_gamepad = (sdlstick == (SDL_Joystick *) stick->getHandle()); |
372 | SDL_GameControllerClose(controller); |
373 | |
374 | // open as gamepad if necessary |
375 | if (open_gamepad) |
376 | stick->openGamepad(d_index); |
377 | } |
378 | } |
379 | } |
380 | |
381 | std::string JoystickModule::getDeviceGUID(int deviceindex) const |
382 | { |
383 | if (deviceindex < 0 || deviceindex >= SDL_NumJoysticks()) |
384 | return std::string("" ); |
385 | |
386 | // SDL_JoystickGetGUIDString uses 32 bytes plus the null terminator. |
387 | char guidstr[33] = {'\0'}; |
388 | |
389 | // SDL2's GUIDs identify *classes* of devices, instead of unique devices. |
390 | SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(deviceindex); |
391 | SDL_JoystickGetGUIDString(guid, guidstr, sizeof(guidstr)); |
392 | |
393 | return std::string(guidstr); |
394 | } |
395 | |
396 | void JoystickModule::loadGamepadMappings(const std::string &mappings) |
397 | { |
398 | // TODO: We should use SDL_GameControllerAddMappingsFromRW. We're |
399 | // duplicating its functionality for now because it was added after |
400 | // SDL 2.0.0's release, and we want runtime compat with 2.0.0 on Linux... |
401 | |
402 | std::stringstream ss(mappings); |
403 | std::string mapping; |
404 | bool success = false; |
405 | |
406 | // The mappings string contains newline-separated mappings. |
407 | while (std::getline(ss, mapping)) |
408 | { |
409 | if (mapping.empty()) |
410 | continue; |
411 | |
412 | // Lines starting with "#" are comments. |
413 | if (mapping[0] == '#') |
414 | continue; |
415 | |
416 | // Strip out and compare any "platform:XYZ," in the mapping. |
417 | size_t pstartpos = mapping.find("platform:" ); |
418 | if (pstartpos != std::string::npos) |
419 | { |
420 | pstartpos += strlen("platform:" ); |
421 | |
422 | size_t pendpos = mapping.find_first_of(',', pstartpos); |
423 | std::string platform = mapping.substr(pstartpos, pendpos - pstartpos); |
424 | |
425 | if (platform.compare(SDL_GetPlatform()) != 0) |
426 | { |
427 | // Ignore the mapping but still acknowledge that it is one. |
428 | success = true; |
429 | continue; |
430 | } |
431 | |
432 | pstartpos -= strlen("platform:" ); |
433 | mapping.erase(pstartpos, pendpos - pstartpos + 1); |
434 | } |
435 | |
436 | if (SDL_GameControllerAddMapping(mapping.c_str()) != -1) |
437 | { |
438 | success = true; |
439 | std::string guid = mapping.substr(0, mapping.find_first_of(',')); |
440 | recentGamepadGUIDs[guid] = true; |
441 | |
442 | // FIXME: massive hack until missing APIs are added to SDL 2: |
443 | // https://bugzilla.libsdl.org/show_bug.cgi?id=1975 |
444 | checkGamepads(guid); |
445 | } |
446 | } |
447 | |
448 | // Don't error when an empty string is given, since saveGamepadMappings can |
449 | // produce an empty string if there are no recently seen gamepads to save. |
450 | if (!success && !mappings.empty()) |
451 | throw love::Exception("Invalid gamepad mappings." ); |
452 | } |
453 | |
454 | std::string JoystickModule::getGamepadMappingString(const std::string &guid) const |
455 | { |
456 | SDL_JoystickGUID sdlguid = SDL_JoystickGetGUIDFromString(guid.c_str()); |
457 | |
458 | char *sdlmapping = SDL_GameControllerMappingForGUID(sdlguid); |
459 | if (sdlmapping == nullptr) |
460 | return "" ; |
461 | |
462 | std::string mapping(sdlmapping); |
463 | SDL_free(sdlmapping); |
464 | |
465 | // Matches SDL_GameControllerAddMappingsFromRW. |
466 | if (mapping.find_last_of(',') != mapping.length() - 1) |
467 | mapping += "," ; |
468 | mapping += "platform:" + std::string(SDL_GetPlatform()); |
469 | |
470 | return mapping; |
471 | } |
472 | |
473 | std::string JoystickModule::saveGamepadMappings() |
474 | { |
475 | std::string mappings; |
476 | |
477 | for (const auto &g : recentGamepadGUIDs) |
478 | { |
479 | SDL_JoystickGUID sdlguid = SDL_JoystickGetGUIDFromString(g.first.c_str()); |
480 | |
481 | char *sdlmapping = SDL_GameControllerMappingForGUID(sdlguid); |
482 | if (sdlmapping == nullptr) |
483 | continue; |
484 | |
485 | std::string mapping = sdlmapping; |
486 | SDL_free(sdlmapping); |
487 | |
488 | if (mapping.find_last_of(',') != mapping.length() - 1) |
489 | mapping += "," ; |
490 | |
491 | // Matches SDL_GameControllerAddMappingsFromRW. |
492 | mapping += "platform:" + std::string(SDL_GetPlatform()) + ",\n" ; |
493 | mappings += mapping; |
494 | } |
495 | |
496 | return mappings; |
497 | } |
498 | |
499 | } // sdl |
500 | } // joystick |
501 | } // love |
502 | |