diff --git a/app/gui/PcView.qml b/app/gui/PcView.qml index e26ea563..8b646551 100644 --- a/app/gui/PcView.qml +++ b/app/gui/PcView.qml @@ -46,6 +46,12 @@ GridView { noHwDecoderDialog.open() } + var unmappedGamepads = prefs.getUnmappedGamepads() + if (unmappedGamepads) { + unmappedGamepadDialog.unmappedGamepads = unmappedGamepads + unmappedGamepadDialog.open() + } + // Don't show any highlighted item until interacting with them currentIndex = -1 } @@ -244,6 +250,20 @@ GridView { } } + MessageDialog { + id: unmappedGamepadDialog + property string unmappedGamepads : "" + modality:Qt.WindowModal + icon: StandardIcon.Warning + standardButtons: StandardButton.Ok | StandardButton.Help + text: "Moonlight detected gamepads without a proper mapping. " + + "The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" + + "Click the Help button for information on how to map your gamepads." + onHelp: { + Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"); + } + } + MessageDialog { id: pairDialog // don't allow edits to the rest of the window while open diff --git a/app/settings/streamingpreferences.cpp b/app/settings/streamingpreferences.cpp index 638b4c3a..370a7ace 100644 --- a/app/settings/streamingpreferences.cpp +++ b/app/settings/streamingpreferences.cpp @@ -99,6 +99,11 @@ bool StreamingPreferences::isWow64() #endif } +QString StreamingPreferences::getUnmappedGamepads() +{ + return SdlInputHandler::getUnmappedGamepads(); +} + int StreamingPreferences::getMaximumStreamingFrameRate() { // Never let the maximum drop below 60 FPS diff --git a/app/settings/streamingpreferences.h b/app/settings/streamingpreferences.h index 120d33e7..a0809632 100644 --- a/app/settings/streamingpreferences.h +++ b/app/settings/streamingpreferences.h @@ -27,6 +27,8 @@ public: Q_INVOKABLE QRect getNativeResolution(int displayIndex); + Q_INVOKABLE QString getUnmappedGamepads(); + void reload(); enum AudioConfig diff --git a/app/streaming/input.cpp b/app/streaming/input.cpp index 4f7eb698..abb66035 100644 --- a/app/streaming/input.cpp +++ b/app/streaming/input.cpp @@ -668,9 +668,37 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve } } +void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) +{ + SDL_assert(event->type == SDL_JOYDEVICEADDED); + + if (!SDL_IsGameController(event->which)) { + char guidStr[33]; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which), + guidStr, sizeof(guidStr)); + const char* name = SDL_JoystickNameForIndex(event->which); + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Joystick discovered with no mapping: %s %s", + name ? name : "", + guidStr); + SDL_Joystick* joy = SDL_JoystickOpen(event->which); + if (joy != nullptr) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Number of axes: %d | Number of buttons: %d | Number of hats: %d", + SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy), + SDL_JoystickNumHats(joy)); + SDL_JoystickClose(joy); + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open joystick for query: %s", + SDL_GetError()); + } + } +} + int SdlInputHandler::getAttachedGamepadMask() { - int i; int count; int mask; @@ -680,7 +708,7 @@ int SdlInputHandler::getAttachedGamepadMask() } count = mask = 0; - for (i = 0; i < SDL_NumJoysticks(); i++) { + for (int i = 0; i < SDL_NumJoysticks(); i++) { if (SDL_IsGameController(i)) { mask |= (1 << count++); } @@ -688,3 +716,58 @@ int SdlInputHandler::getAttachedGamepadMask() return mask; } + +QString SdlInputHandler::getUnmappedGamepads() +{ + QString ret; + + if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", + SDL_GetError()); + } + + for (int i = 0; i < SDL_NumJoysticks(); i++) { + if (!SDL_IsGameController(i)) { + char guidStr[33]; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i), + guidStr, sizeof(guidStr)); + const char* name = SDL_JoystickNameForIndex(i); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Unmapped joystick: %s %s", + name ? name : "", + guidStr); + SDL_Joystick* joy = SDL_JoystickOpen(i); + if (joy != nullptr) { + int numButtons = SDL_JoystickNumButtons(joy); + int numHats = SDL_JoystickNumHats(joy); + int numAxes = SDL_JoystickNumAxes(joy); + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Number of axes: %d | Number of buttons: %d | Number of hats: %d", + numAxes, numButtons, numHats); + + if (numAxes >= 4 && numButtons >= 8 && numHats <= 1) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Joystick likely to be an unmapped game controller"); + if (!ret.isEmpty()) { + ret += ", "; + } + + ret += name; + } + + SDL_JoystickClose(joy); + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open joystick for query: %s", + SDL_GetError()); + } + } + } + + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); + + return ret; +} diff --git a/app/streaming/input.h b/app/streaming/input.h index b0bd5987..081d1df6 100644 --- a/app/streaming/input.h +++ b/app/streaming/input.h @@ -39,8 +39,13 @@ public: void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event); + void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); + int getAttachedGamepadMask(); + static + QString getUnmappedGamepads(); + private: GamepadState* findStateForGamepad(SDL_JoystickID id); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 2b617ccd..6738f4e6 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -525,6 +525,11 @@ bool Session::validateLaunch() emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session."); } + // Check for unmapped gamepads + if (!SdlInputHandler::getUnmappedGamepads().isEmpty()) { + emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this."); + } + if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE && !isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection, m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264, @@ -1107,13 +1112,12 @@ void Session::exec(int displayOriginX, int displayOriginY) case SDL_CONTROLLERDEVICEREMOVED: inputHandler.handleControllerDeviceEvent(&event.cdevice); break; + case SDL_JOYDEVICEADDED: + inputHandler.handleJoystickArrivalEvent(&event.jdevice); + break; } } - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "SDL_WaitEvent() failed: %s", - SDL_GetError()); - DispatchDeferredCleanup: #ifdef Q_OS_WIN32 // HACK: See comment above