From f20fc475c7abf80dcfcf15ddaff2b3de4859a034 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Tue, 15 Jul 2025 14:47:37 -0700 Subject: [PATCH] Integrate SDL Haptic API (#607) * Integrate SDL Haptic API * Close other devices * Fixes --- ISLE/isleapp.cpp | 54 +++- ISLE/isleapp.h | 2 - .../lego/legoomni/include/legoinputmanager.h | 29 +- .../legoomni/src/input/legoinputmanager.cpp | 284 +++++++++++++----- LEGO1/lego/sources/misc/legoutil.h | 8 + tools/ncc/skip.yml | 3 + 6 files changed, 284 insertions(+), 96 deletions(-) diff --git a/ISLE/isleapp.cpp b/ISLE/isleapp.cpp index 867f1744..5a071664 100644 --- a/ISLE/isleapp.cpp +++ b/ISLE/isleapp.cpp @@ -137,8 +137,6 @@ IsleApp::IsleApp() m_drawCursor = FALSE; m_use3dSound = TRUE; m_useMusic = TRUE; - m_useJoystick = TRUE; - m_joystickIndex = 0; m_wideViewAngle = TRUE; m_islandQuality = 2; m_islandTexture = 1; @@ -297,7 +295,7 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) { + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC)) { char buffer[256]; SDL_snprintf( buffer, @@ -340,6 +338,23 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) // Get reference to window *appstate = g_isle->GetWindowHandle(); + // Currently, SDL doesn't send SDL_EVENT_MOUSE_ADDED at startup (unlike for gamepads) + // This will probably be fixed in the future: https://github.com/libsdl-org/SDL/issues/12815 + { + int count; + SDL_MouseID* mice = SDL_GetMice(&count); + + if (mice) { + for (int i = 0; i < count; i++) { + if (InputManager()) { + InputManager()->AddMouse(mice[i]); + } + } + + SDL_free(mice); + } + } + #ifdef __EMSCRIPTEN__ SDL_AddEventWatch( [](void* userdata, SDL_Event* event) -> bool { @@ -423,6 +438,10 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) return SDL_APP_CONTINUE; } + if (InputManager()) { + InputManager()->UpdateLastInputMethod(event); + } + switch (event->type) { case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_MOUSE_MOTION: @@ -482,13 +501,26 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) } break; } - case SDL_EVENT_GAMEPAD_ADDED: - case SDL_EVENT_GAMEPAD_REMOVED: { + case SDL_EVENT_MOUSE_ADDED: if (InputManager()) { - InputManager()->GetJoystick(); + InputManager()->AddMouse(event->mdevice.which); + } + break; + case SDL_EVENT_MOUSE_REMOVED: + if (InputManager()) { + InputManager()->RemoveMouse(event->mdevice.which); + } + break; + case SDL_EVENT_GAMEPAD_ADDED: + if (InputManager()) { + InputManager()->AddJoystick(event->jdevice.which); + } + break; + case SDL_EVENT_GAMEPAD_REMOVED: + if (InputManager()) { + InputManager()->RemoveJoystick(event->jdevice.which); } break; - } case SDL_EVENT_GAMEPAD_BUTTON_DOWN: { switch (event->gbutton.button) { case SDL_GAMEPAD_BUTTON_DPAD_UP: @@ -903,10 +935,6 @@ MxResult IsleApp::SetupWindow() MxTransitionManager::configureMxTransitionManager(m_transitionType); RealtimeView::SetUserMaxLOD(m_maxLod); if (LegoOmni::GetInstance()) { - if (LegoOmni::GetInstance()->GetInputManager()) { - LegoOmni::GetInstance()->GetInputManager()->SetUseJoystick(m_useJoystick); - LegoOmni::GetInstance()->GetInputManager()->SetJoystickIndex(m_joystickIndex); - } if (LegoOmni::GetInstance()->GetVideoManager()) { LegoOmni::GetInstance()->GetVideoManager()->SetCursorBitmap(m_cursorCurrentBitmap); } @@ -998,8 +1026,6 @@ bool IsleApp::LoadConfig() iniparser_set(dict, "isle:3DSound", m_use3dSound ? "true" : "false"); iniparser_set(dict, "isle:Music", m_useMusic ? "true" : "false"); - iniparser_set(dict, "isle:UseJoystick", m_useJoystick ? "true" : "false"); - iniparser_set(dict, "isle:JoystickIndex", SDL_itoa(m_joystickIndex, buf, 10)); SDL_snprintf(buf, sizeof(buf), "%f", m_cursorSensitivity); iniparser_set(dict, "isle:Cursor Sensitivity", buf); @@ -1058,8 +1084,6 @@ bool IsleApp::LoadConfig() m_wideViewAngle = iniparser_getboolean(dict, "isle:Wide View Angle", m_wideViewAngle); m_use3dSound = iniparser_getboolean(dict, "isle:3DSound", m_use3dSound); m_useMusic = iniparser_getboolean(dict, "isle:Music", m_useMusic); - m_useJoystick = iniparser_getboolean(dict, "isle:UseJoystick", m_useJoystick); - m_joystickIndex = iniparser_getint(dict, "isle:JoystickIndex", m_joystickIndex); m_cursorSensitivity = iniparser_getdouble(dict, "isle:Cursor Sensitivity", m_cursorSensitivity); MxS32 backBuffersInVRAM = iniparser_getboolean(dict, "isle:Back Buffers in Video RAM", -1); diff --git a/ISLE/isleapp.h b/ISLE/isleapp.h index 4bec7dd6..cfe11bf0 100644 --- a/ISLE/isleapp.h +++ b/ISLE/isleapp.h @@ -80,8 +80,6 @@ class IsleApp { MxS32 m_hasLightSupport; // 0x24 MxS32 m_use3dSound; // 0x28 MxS32 m_useMusic; // 0x2c - MxS32 m_useJoystick; // 0x30 - MxS32 m_joystickIndex; // 0x34 MxS32 m_wideViewAngle; // 0x38 MxS32 m_islandQuality; // 0x3c MxS32 m_islandTexture; // 0x40 diff --git a/LEGO1/lego/legoomni/include/legoinputmanager.h b/LEGO1/lego/legoomni/include/legoinputmanager.h index ebb3b352..b86cd137 100644 --- a/LEGO1/lego/legoomni/include/legoinputmanager.h +++ b/LEGO1/lego/legoomni/include/legoinputmanager.h @@ -8,6 +8,7 @@ #include "mxpresenter.h" #include "mxqueue.h" +#include #include #include #include @@ -19,6 +20,7 @@ #endif #include +#include class LegoCameraController; class LegoControlManager; @@ -129,8 +131,6 @@ class LegoInputManager : public MxPresenter { void SetUnknown88(MxBool p_unk0x88) { m_unk0x88 = p_unk0x88; } void SetUnknown335(MxBool p_unk0x335) { m_unk0x335 = p_unk0x335; } void SetUnknown336(MxBool p_unk0x336) { m_unk0x336 = p_unk0x336; } - void SetUseJoystick(MxBool p_useJoystick) { m_useJoystick = p_useJoystick; } - void SetJoystickIndex(MxS32 p_joystickIndex) { m_joystickIndex = p_joystickIndex; } // FUNCTION: BETA10 0x1002e290 void DisableInputProcessing() @@ -153,13 +153,26 @@ class LegoInputManager : public MxPresenter { void GetKeyboardState(); MxResult GetNavigationKeyStates(MxU32& p_keyFlags); MxResult GetNavigationTouchStates(MxU32& p_keyFlags); + LEGO1_EXPORT void AddMouse(SDL_MouseID p_mouseID); + LEGO1_EXPORT void RemoveMouse(SDL_MouseID p_mouseID); + LEGO1_EXPORT void AddJoystick(SDL_JoystickID p_joystickID); + LEGO1_EXPORT void RemoveJoystick(SDL_JoystickID p_joystickID); LEGO1_EXPORT MxBool HandleTouchEvent(SDL_Event* p_event, TouchScheme p_touchScheme); LEGO1_EXPORT MxBool HandleRumbleEvent(); + LEGO1_EXPORT void UpdateLastInputMethod(SDL_Event* p_event); // SYNTHETIC: LEGO1 0x1005b8d0 // LegoInputManager::`scalar deleting destructor' private: + // clang-format off + enum class SDL_MouseID_v : SDL_MouseID {}; + enum class SDL_JoystickID_v : SDL_JoystickID {}; + enum class SDL_TouchID_v : SDL_TouchID {}; + // clang-format on + + void InitializeHaptics(); + MxCriticalSection m_criticalSection; // 0x58 LegoNotifyList* m_keyboardNotifyList; // 0x5c LegoCameraController* m_camera; // 0x60 @@ -176,16 +189,16 @@ class LegoInputManager : public MxPresenter { MxBool m_unk0x88; // 0x88 const bool* m_keyboardState; MxBool m_unk0x195; // 0x195 - SDL_JoystickID* m_joyids; - SDL_Gamepad* m_joystick; - MxS32 m_joystickIndex; // 0x19c - MxBool m_useJoystick; // 0x334 - MxBool m_unk0x335; // 0x335 - MxBool m_unk0x336; // 0x336 + MxBool m_unk0x335; // 0x335 + MxBool m_unk0x336; // 0x336 std::map m_touchOrigins; std::map m_touchFlags; std::map> m_touchLastMotion; + std::map> m_mice; + std::map> m_joysticks; + std::map m_otherHaptics; + std::variant m_lastInputMethod; }; // TEMPLATE: LEGO1 0x10028850 diff --git a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp index 7c80e78e..432b4fe7 100644 --- a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp +++ b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp @@ -11,6 +11,8 @@ #include "mxdebug.h" #include "roi/legoroi.h" +#include + DECOMP_SIZE_ASSERT(LegoInputManager, 0x338) DECOMP_SIZE_ASSERT(LegoNotifyList, 0x18) DECOMP_SIZE_ASSERT(LegoNotifyListCursor, 0x10) @@ -40,10 +42,6 @@ LegoInputManager::LegoInputManager() m_unk0x81 = FALSE; m_unk0x88 = FALSE; m_unk0x195 = 0; - m_joyids = NULL; - m_joystickIndex = -1; - m_joystick = NULL; - m_useJoystick = FALSE; m_unk0x335 = FALSE; m_unk0x336 = FALSE; m_unk0x74 = 0x19; @@ -71,8 +69,6 @@ MxResult LegoInputManager::Create(HWND p_hwnd) m_eventQueue = new LegoEventQueue; } - GetJoystick(); - if (!m_keyboardNotifyList || !m_eventQueue) { Destroy(); result = FAILURE; @@ -98,7 +94,23 @@ void LegoInputManager::Destroy() delete m_controlManager; } - SDL_free(m_joyids); + for (const auto& [id, joystick] : m_joysticks) { + if (joystick.second) { + SDL_CloseHaptic(joystick.second); + } + + SDL_CloseGamepad(joystick.first); + } + + for (const auto& [id, mouse] : m_mice) { + if (mouse.second) { + SDL_CloseHaptic(mouse.second); + } + } + + for (const auto& [id, haptic] : m_otherHaptics) { + SDL_CloseHaptic(haptic); + } } // FUNCTION: LEGO1 0x1005c0f0 @@ -145,74 +157,33 @@ MxResult LegoInputManager::GetNavigationKeyStates(MxU32& p_keyFlags) return SUCCESS; } -// FUNCTION: LEGO1 0x1005c240 -MxResult LegoInputManager::GetJoystick() -{ - if (m_joystick != NULL && SDL_GamepadConnected(m_joystick) == TRUE) { - return SUCCESS; - } - - MxS32 numJoysticks = 0; - if (m_joyids != NULL) { - SDL_free(m_joyids); - m_joyids = NULL; - } - m_joyids = SDL_GetGamepads(&numJoysticks); - - if (m_useJoystick != FALSE && numJoysticks != 0) { - MxS32 joyid = m_joystickIndex; - if (joyid >= 0) { - m_joystick = SDL_OpenGamepad(m_joyids[joyid]); - if (m_joystick != NULL) { - return SUCCESS; - } - } - - for (joyid = 0; joyid < numJoysticks; joyid++) { - m_joystick = SDL_OpenGamepad(m_joyids[joyid]); - if (m_joystick != NULL) { - return SUCCESS; - } - } - } - - return FAILURE; -} - // FUNCTION: LEGO1 0x1005c320 MxResult LegoInputManager::GetJoystickState(MxU32* p_joystickX, MxU32* p_joystickY, MxU32* p_povPosition) { - if (m_useJoystick != FALSE) { - if (GetJoystick() == -1) { - if (m_joystick != NULL) { - // GetJoystick() failed but handle to joystick is still open, close it - SDL_CloseGamepad(m_joystick); - m_joystick = NULL; - } + if (m_joysticks.empty()) { + return FAILURE; + } - return FAILURE; - } - - MxS16 xPos = SDL_GetGamepadAxis(m_joystick, SDL_GAMEPAD_AXIS_LEFTX); - MxS16 yPos = SDL_GetGamepadAxis(m_joystick, SDL_GAMEPAD_AXIS_LEFTY); + MxS16 xPos, yPos = 0; + for (const auto& [id, joystick] : m_joysticks) { + xPos = SDL_GetGamepadAxis(joystick.first, SDL_GAMEPAD_AXIS_LEFTX); + yPos = SDL_GetGamepadAxis(joystick.first, SDL_GAMEPAD_AXIS_LEFTY); if (xPos > -8000 && xPos < 8000) { - // Ignore small axis values xPos = 0; } if (yPos > -8000 && yPos < 8000) { - // Ignore small axis values yPos = 0; } - // normalize values acquired from joystick axes - *p_joystickX = ((xPos + 32768) * 100) / 65535; - *p_joystickY = ((yPos + 32768) * 100) / 65535; - *p_povPosition = -1; - - return SUCCESS; + if (xPos || yPos) { + break; + } } - return FAILURE; + *p_joystickX = ((xPos + 32768) * 100) / 65535; + *p_joystickY = ((yPos + 32768) * 100) / 65535; + *p_povPosition = -1; + return SUCCESS; } // FUNCTION: LEGO1 0x1005c470 @@ -546,6 +517,74 @@ MxResult LegoInputManager::GetNavigationTouchStates(MxU32& p_keyStates) return SUCCESS; } +void LegoInputManager::AddMouse(SDL_MouseID p_mouseID) +{ + if (m_mice.count(p_mouseID)) { + return; + } + + // Currently no way to get an individual haptic device for a mouse. + SDL_Haptic* haptic = SDL_OpenHapticFromMouse(); + if (haptic) { + if (!SDL_InitHapticRumble(haptic)) { + SDL_CloseHaptic(haptic); + haptic = nullptr; + } + } + + m_mice[p_mouseID] = {nullptr, haptic}; +} + +void LegoInputManager::RemoveMouse(SDL_MouseID p_mouseID) +{ + if (!m_mice.count(p_mouseID)) { + return; + } + + if (m_mice[p_mouseID].second) { + SDL_CloseHaptic(m_mice[p_mouseID].second); + } + + m_mice.erase(p_mouseID); +} + +void LegoInputManager::AddJoystick(SDL_JoystickID p_joystickID) +{ + if (m_joysticks.count(p_joystickID)) { + return; + } + + SDL_Gamepad* joystick = SDL_OpenGamepad(p_joystickID); + if (joystick) { + SDL_Haptic* haptic = SDL_OpenHapticFromJoystick(SDL_GetGamepadJoystick(joystick)); + if (haptic) { + if (!SDL_InitHapticRumble(haptic)) { + SDL_CloseHaptic(haptic); + haptic = nullptr; + } + } + + m_joysticks[p_joystickID] = {joystick, haptic}; + } + else { + SDL_Log("Failed to open gamepad: %s", SDL_GetError()); + } +} + +void LegoInputManager::RemoveJoystick(SDL_JoystickID p_joystickID) +{ + if (!m_joysticks.count(p_joystickID)) { + return; + } + + if (m_joysticks[p_joystickID].second) { + SDL_CloseHaptic(m_joysticks[p_joystickID].second); + } + + SDL_CloseGamepad(m_joysticks[p_joystickID].first); + m_joysticks.erase(p_joystickID); +} + MxBool LegoInputManager::HandleTouchEvent(SDL_Event* p_event, TouchScheme p_touchScheme) { const SDL_TouchFingerEvent& event = p_event->tfinger; @@ -629,15 +668,118 @@ MxBool LegoInputManager::HandleTouchEvent(SDL_Event* p_event, TouchScheme p_touc MxBool LegoInputManager::HandleRumbleEvent() { - if (m_joystick != NULL && SDL_GamepadConnected(m_joystick) == TRUE) { - const Uint16 frequency = 65535 / 2; - const Uint32 durationMs = 700; - SDL_RumbleGamepad(m_joystick, frequency, frequency, durationMs); - } - else { - return FALSE; + static bool g_hapticsInitialized = false; + + if (!g_hapticsInitialized) { + InitializeHaptics(); + g_hapticsInitialized = true; } - // Add support for SDL Haptic API - return TRUE; + SDL_Haptic* haptic = nullptr; + std::visit( + overloaded{ + [&haptic, this](SDL_MouseID_v p_id) { + if (m_mice.count((SDL_MouseID) p_id)) { + haptic = m_mice[(SDL_MouseID) p_id].second; + } + }, + [&haptic, this](SDL_JoystickID_v p_id) { + if (m_joysticks.count((SDL_JoystickID) p_id)) { + haptic = m_joysticks[(SDL_JoystickID) p_id].second; + } + }, + [&haptic, this](SDL_TouchID_v p_id) { + // We can't currently correlate Touch devices with Haptic devices + if (!m_otherHaptics.empty()) { + haptic = m_otherHaptics.begin()->second; + } + } + }, + m_lastInputMethod + ); + + const float strength = 0.5f; + const Uint32 durationMs = 700; + + if (haptic) { + return SDL_PlayHapticRumble(haptic, strength, durationMs); + } + + // A joystick isn't necessarily a haptic device; try basic rumble instead + if (const SDL_JoystickID_v* joystick = std::get_if(&m_lastInputMethod)) { + if (m_joysticks.count((SDL_JoystickID) *joystick)) { + return SDL_RumbleGamepad( + m_joysticks[(SDL_JoystickID) *joystick].first, + strength * 65535, + strength * 65535, + durationMs + ); + } + } + + return FALSE; +} + +void LegoInputManager::InitializeHaptics() +{ + // We don't get added/removed events for haptic devices that are not joysticks or mice, + // so we initialize "otherHaptics" once at this point. + std::vector existingHaptics; + + for (const auto& [id, mouse] : m_mice) { + if (mouse.second) { + existingHaptics.push_back(SDL_GetHapticID(mouse.second)); + } + } + + for (const auto& [id, joystick] : m_joysticks) { + if (joystick.second) { + existingHaptics.push_back(SDL_GetHapticID(joystick.second)); + } + } + + int count; + SDL_HapticID* haptics = SDL_GetHaptics(&count); + if (haptics) { + for (int i = 0; i < count; i++) { + if (std::find(existingHaptics.begin(), existingHaptics.end(), haptics[i]) == existingHaptics.end()) { + SDL_Haptic* haptic = SDL_OpenHaptic(haptics[i]); + if (haptic) { + if (SDL_InitHapticRumble(haptic)) { + m_otherHaptics[haptics[i]] = haptic; + } + else { + SDL_CloseHaptic(haptic); + } + } + } + } + + SDL_free(haptics); + } +} + +void LegoInputManager::UpdateLastInputMethod(SDL_Event* p_event) +{ + switch (p_event->type) { + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + m_lastInputMethod = SDL_MouseID_v{p_event->button.which}; + break; + case SDL_EVENT_MOUSE_MOTION: + m_lastInputMethod = SDL_MouseID_v{p_event->motion.which}; + break; + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + m_lastInputMethod = SDL_JoystickID_v{p_event->gbutton.which}; + break; + case SDL_EVENT_GAMEPAD_AXIS_MOTION: + m_lastInputMethod = SDL_JoystickID_v{p_event->gaxis.which}; + break; + case SDL_EVENT_FINGER_MOTION: + case SDL_EVENT_FINGER_DOWN: + case SDL_EVENT_FINGER_UP: + m_lastInputMethod = SDL_TouchID_v{p_event->tfinger.touchID}; + break; + } } diff --git a/LEGO1/lego/sources/misc/legoutil.h b/LEGO1/lego/sources/misc/legoutil.h index 871a9b72..78f52d0f 100644 --- a/LEGO1/lego/sources/misc/legoutil.h +++ b/LEGO1/lego/sources/misc/legoutil.h @@ -59,4 +59,12 @@ inline T RToD(T p_r) return p_r * 180.0F / 3.1416F; } +template +struct overloaded : Ts... { + using Ts::operator()...; +}; + +template +overloaded(Ts...) -> overloaded; + #endif // __LEGOUTIL_H diff --git a/tools/ncc/skip.yml b/tools/ncc/skip.yml index c5ceef72..2bae0ab4 100644 --- a/tools/ncc/skip.yml +++ b/tools/ncc/skip.yml @@ -74,3 +74,6 @@ cksize: "Re-defined Windows name" fccType: "Re-defined Windows name" dwDataOffset: "Re-defined Windows name" fccType: "Re-defined Windows name" +SDL_MouseID_v: "SDL-based name" +SDL_JoystickID_v: "SDL-based name" +SDL_TouchID_v: "SDL-based name" \ No newline at end of file