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
35namespace love
36{
37namespace joystick
38{
39namespace sdl
40{
41
42JoystickModule::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
57JoystickModule::~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
72const char *JoystickModule::getName() const
73{
74 return "love.joystick.sdl";
75}
76
77love::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
85int 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
97int JoystickModule::getJoystickCount() const
98{
99 return (int) activeSticks.size();
100}
101
102love::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
113love::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
171void 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
185bool 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
284std::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
311void 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
342void 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
381std::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
396void 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
454std::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
473std::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