| 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 | |