/** * Copyright (c) 2006-2014 LOVE Development Team * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. **/ #include "common/config.h" #include "JoystickModule.h" #include "Joystick.h" // SDL #include // C++ #include #include // C #include namespace love { namespace joystick { namespace sdl { JoystickModule::JoystickModule() { if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) throw love::Exception("%s", SDL_GetError()); // Initialize any joysticks which are already connected. for (int i = 0; i < SDL_NumJoysticks(); i++) addJoystick(i); // Start joystick event watching. Joysticks are automatically added and // removed via love.event. SDL_JoystickEventState(SDL_ENABLE); SDL_GameControllerEventState(SDL_ENABLE); } JoystickModule::~JoystickModule() { // Close any open Joysticks. for (auto stick : joysticks) { stick->close(); stick->release(); } if (SDL_WasInit(SDL_INIT_HAPTIC) != 0) SDL_QuitSubSystem(SDL_INIT_HAPTIC); SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER); } const char *JoystickModule::getName() const { return "love.joystick.sdl"; } love::joystick::Joystick *JoystickModule::getJoystick(int joyindex) { if (joyindex < 0 || (size_t) joyindex >= activeSticks.size()) return nullptr; return activeSticks[joyindex]; } int JoystickModule::getIndex(const love::joystick::Joystick *joystick) { for (size_t i = 0; i < activeSticks.size(); i++) { if (activeSticks[i] == joystick) return i; } // Joystick is not connected. return -1; } int JoystickModule::getJoystickCount() const { return (int) activeSticks.size(); } love::joystick::Joystick *JoystickModule::getJoystickFromID(int instanceid) { for (auto stick : activeSticks) { if (stick->getInstanceID() == instanceid) return stick; } return nullptr; } love::joystick::Joystick *JoystickModule::addJoystick(int deviceindex) { if (deviceindex < 0 || deviceindex >= SDL_NumJoysticks()) return nullptr; std::string guidstr = getDeviceGUID(deviceindex); joystick::Joystick *joystick = 0; bool reused = false; for (auto stick : joysticks) { // Try to re-use a disconnected Joystick with the same GUID. if (!stick->isConnected() && stick->getGUID() == guidstr) { joystick = stick; reused = true; break; } } if (!joystick) { joystick = new Joystick(joysticks.size()); joysticks.push_back(joystick); } // Make sure the Joystick object isn't in the active list already. removeJoystick(joystick); if (!joystick->open(deviceindex)) return nullptr; // Make sure multiple instances of the same physical joystick aren't added // to the active list. for (auto activestick : activeSticks) { if (joystick->getHandle() == activestick->getHandle()) { joystick->close(); // If we just created the stick, remove it since it's a duplicate. if (!reused) { joysticks.remove(joystick); joystick->release(); } return activestick; } } activeSticks.push_back(joystick); return joystick; } void JoystickModule::removeJoystick(love::joystick::Joystick *joystick) { if (!joystick) return; // Close the Joystick and remove it from the active joystick list. auto it = std::find(activeSticks.begin(), activeSticks.end(), joystick); if (it != activeSticks.end()) { (*it)->close(); activeSticks.erase(it); } } bool JoystickModule::setGamepadMapping(const std::string &guid, Joystick::GamepadInput gpinput, Joystick::JoystickInput joyinput) { // All SDL joystick GUID strings are 32 characters. if (guid.length() != 32) throw love::Exception("Invalid joystick GUID: %s", guid.c_str()); SDL_JoystickGUID sdlguid = SDL_JoystickGetGUIDFromString(guid.c_str()); std::string mapstr; char *sdlmapstr = SDL_GameControllerMappingForGUID(sdlguid); if (sdlmapstr) { mapstr = sdlmapstr; SDL_free(sdlmapstr); } else { // Use a generic name if we have to create a new mapping string. mapstr = guid + ",Controller,"; } std::stringstream joyinputstream; Uint8 sdlhat; // We can't have negative int values in the bind string. switch (joyinput.type) { case Joystick::INPUT_TYPE_AXIS: if (joyinput.axis >= 0) joyinputstream << "a" << joyinput.axis; break; case Joystick::INPUT_TYPE_BUTTON: if (joyinput.button >= 0) joyinputstream << "b" << joyinput.button; break; case Joystick::INPUT_TYPE_HAT: if (joyinput.hat.value >= 0 && Joystick::getConstant(joyinput.hat.value, sdlhat)) joyinputstream << "h" << joyinput.hat.value << "." << int(sdlhat); break; default: break; } std::string joyinputstr = joyinputstream.str(); if (joyinputstr.length() == 0) throw love::Exception("Invalid joystick input value."); // SDL's name for the gamepad input value, e.g. "guide". std::string gpinputname = stringFromGamepadInput(gpinput); // We should remove any existing joystick bind for this gamepad buttton/axis // so SDL's parser doesn't get mixed up. removeBindFromMapString(mapstr, joyinputstr); // The string we'll be adding to the mapping string, e.g. "guide:b10," std::string insertstr = gpinputname + ":" + joyinputstr + ","; // We should replace any existing gamepad bind. size_t findpos = mapstr.find(gpinputname + ":"); if (findpos != std::string::npos) { // The bind string ends at the next comma, or the end of the string. size_t endpos = mapstr.find_first_of(',', findpos); if (endpos == std::string::npos) endpos = mapstr.length() - 1; mapstr.replace(findpos, endpos - findpos + 1, insertstr); } else { // Just append to the end if we don't need to replace anything. mapstr += insertstr; } // 1 == added, 0 == updated, -1 == error. int status = SDL_GameControllerAddMapping(mapstr.c_str()); // FIXME: massive hack until missing APIs are added to SDL 2: // https://bugzilla.libsdl.org/show_bug.cgi?id=1975 if (status == 1) checkGamepads(guid); return status >= 0; } Joystick::JoystickInput JoystickModule::getGamepadMapping(const std::string &guid, Joystick::GamepadInput gpinput) { // All SDL joystick GUID strings are 32 characters. if (guid.length() != 32) throw love::Exception("Invalid joystick GUID: %s", guid.c_str()); Joystick::JoystickInput jinput; jinput.type = Joystick::INPUT_TYPE_MAX_ENUM; SDL_JoystickGUID sdlguid = SDL_JoystickGetGUIDFromString(guid.c_str()); std::string mapstr; char *sdlmapstr = SDL_GameControllerMappingForGUID(sdlguid); if (!sdlmapstr) return jinput; mapstr = sdlmapstr; SDL_free(sdlmapstr); std::string gpbindname = stringFromGamepadInput(gpinput); size_t findpos = mapstr.find(std::string(",") + gpbindname + ":"); if (findpos == std::string::npos) return jinput; size_t endpos = mapstr.find_first_of(',', findpos); if (endpos == std::string::npos) { // Assume end-of-string if we can't find the next comma. endpos = mapstr.length() - 1; } if (endpos >= mapstr.length()) return jinput; // Something went wrong. // Strip out the trailing comma from our search position, if it exists. if (mapstr[endpos] == ',') endpos--; // New start position: comma + gamepadinputlength + ":". findpos += 1 + gpbindname.length() + 1; std::string jbindstr = mapstr.substr(findpos, endpos - findpos + 1); jinput = JoystickInputFromString(jbindstr); return jinput; } std::string JoystickModule::stringFromGamepadInput(Joystick::GamepadInput gpinput) const { SDL_GameControllerAxis sdlaxis; SDL_GameControllerButton sdlbutton; const char *gpinputname = nullptr; switch (gpinput.type) { case Joystick::INPUT_TYPE_AXIS: if (Joystick::getConstant(gpinput.axis, sdlaxis)) gpinputname = SDL_GameControllerGetStringForAxis(sdlaxis); break; case Joystick::INPUT_TYPE_BUTTON: if (Joystick::getConstant(gpinput.button, sdlbutton)) gpinputname = SDL_GameControllerGetStringForButton(sdlbutton); break; default: break; } if (!gpinputname) throw love::Exception("Invalid gamepad axis/button."); return std::string(gpinputname); } Joystick::JoystickInput JoystickModule::JoystickInputFromString(const std::string &str) const { Joystick::JoystickInput jinput; jinput.type = Joystick::INPUT_TYPE_MAX_ENUM; // Return an invalid value rather than throwing an exception. if (str.length() < 2) return jinput; // The input type will always be the first character in the string. char inputtype = str[0]; std::string bindvalues = str.substr(1); Uint8 sdlhat; switch (inputtype) { case 'a': jinput.type = Joystick::INPUT_TYPE_AXIS; jinput.axis = atoi(bindvalues.c_str()); break; case 'b': jinput.type = Joystick::INPUT_TYPE_BUTTON; jinput.button = atoi(bindvalues.c_str()); break; case 'h': // Hat string syntax is "index.value". if (bindvalues.length() < 3) break; jinput.type = Joystick::INPUT_TYPE_HAT; jinput.hat.index = atoi(bindvalues.substr(0, 1).c_str()); sdlhat = (Uint8) atoi(bindvalues.substr(2, 1).c_str()); if (!Joystick::getConstant(sdlhat, jinput.hat.value)) { // Return an invalid value if we can't find the hat constant. jinput.type = Joystick::INPUT_TYPE_MAX_ENUM; return jinput; } break; default: break; } return jinput; } void JoystickModule::removeBindFromMapString(std::string &mapstr, const std::string &joybindstr) const { // Find the joystick part of the bind in the string. size_t joybindpos = mapstr.find(joybindstr + ","); if (joybindpos == std::string::npos) { joybindpos = mapstr.rfind(joybindstr); if (joybindpos != mapstr.length() - joybindstr.length()) return; } if (joybindpos == std::string::npos) return; // Find the start of the entire bind. size_t bindstart = mapstr.rfind(',', joybindpos); if (bindstart != std::string::npos && bindstart < mapstr.length() - 1) { size_t bindend = mapstr.find(',', bindstart + 1); if (bindend == std::string::npos) bindend = mapstr.length() - 1; // Replace it with an empty string (remove it.) mapstr.replace(bindstart, bindend - bindstart + 1, ""); } } void JoystickModule::checkGamepads(const std::string &guid) const { // FIXME: massive hack until missing APIs are added to SDL 2: // https://bugzilla.libsdl.org/show_bug.cgi?id=1975 // Make sure all connected joysticks of a certain guid that are // gamepad-capable are opened as such. for (int d_index = 0; d_index < SDL_NumJoysticks(); d_index++) { if (!SDL_IsGameController(d_index)) continue; if (guid.compare(getDeviceGUID(d_index)) != 0) continue; for (auto stick : activeSticks) { if (stick->isGamepad() || guid.compare(stick->getGUID()) != 0) continue; // Big hack time: open the index as a game controller and compare // the underlying joystick handle to the active stick's. SDL_GameController *controller = SDL_GameControllerOpen(d_index); if (controller == nullptr) continue; SDL_Joystick *sdlstick = SDL_GameControllerGetJoystick(controller); if (sdlstick == (SDL_Joystick *) stick->getHandle()) stick->openGamepad(d_index); // GameController objects are reference-counted in SDL. SDL_GameControllerClose(controller); } } } std::string JoystickModule::getDeviceGUID(int deviceindex) const { if (deviceindex < 0 || deviceindex >= SDL_NumJoysticks()) return std::string(""); // SDL_JoystickGetGUIDString uses 32 bytes plus the null terminator. char guidstr[33] = {'\0'}; // SDL2's GUIDs identify *classes* of devices, instead of unique devices. SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(deviceindex); SDL_JoystickGetGUIDString(guid, guidstr, sizeof(guidstr)); return std::string(guidstr); } void JoystickModule::loadGamepadMappings(const std::string &mappings) { // TODO: We should use SDL_GameControllerAddMappingsFromRW. We're // duplicating its functionality for now because it was added after // SDL 2.0.0's release, and we want runtime compat with 2.0.0 on Linux... std::stringstream ss(mappings); std::string mapping; bool success = false; // The mappings string contains newline-separated mappings. while (std::getline(ss, mapping)) { if (mapping.empty()) continue; // Strip out and compare any "platform:XYZ," in the mapping. size_t pstartpos = mapping.find("platform:"); if (pstartpos != std::string::npos) { pstartpos += strlen("platform:"); size_t pendpos = mapping.find_first_of(',', pstartpos); std::string platform = mapping.substr(pstartpos, pendpos - pstartpos); if (platform.compare(SDL_GetPlatform()) != 0) continue; pstartpos -= strlen("platform:"); mapping.erase(pstartpos, pendpos - pstartpos + 1); } success = success || (SDL_GameControllerAddMapping(mapping.c_str()) != -1); } if (!success) throw love::Exception("Invalid gamepad mappings."); } std::string JoystickModule::saveGamepadMapping(const std::string &pguid) { SDL_JoystickGUID sdlguid = SDL_JoystickGetGUIDFromString(pguid.c_str()); std::string mapping; char *sdlmapping = SDL_GameControllerMappingForGUID(sdlguid); if (sdlmapping == nullptr) throw love::Exception("The specified Joystick GUID string has no gamepad mapping."); mapping = sdlmapping; SDL_free(sdlmapping); if (mapping.find_last_of(',') != mapping.size() - 1) mapping += ","; // Matches SDL_GameControllerAddMappingsFromRW. mapping += "platform:" + std::string(SDL_GetPlatform()) + ","; return mapping; } } // sdl } // joystick } // love