Integrate SDL Haptic API (#607)

* Integrate SDL Haptic API

* Close other devices

* Fixes
This commit is contained in:
Christian Semmler 2025-07-15 14:47:37 -07:00 committed by GitHub
parent 87c89885ba
commit f20fc475c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 284 additions and 96 deletions

View File

@ -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);

View File

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

View File

@ -8,6 +8,7 @@
#include "mxpresenter.h"
#include "mxqueue.h"
#include <SDL3/SDL_haptic.h>
#include <SDL3/SDL_joystick.h>
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_keycode.h>
@ -19,6 +20,7 @@
#endif
#include <map>
#include <variant>
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
std::map<SDL_FingerID, SDL_FPoint> m_touchOrigins;
std::map<SDL_FingerID, MxU32> m_touchFlags;
std::map<SDL_FingerID, std::pair<MxU32, SDL_FPoint>> m_touchLastMotion;
std::map<SDL_MouseID, std::pair<void*, SDL_Haptic*>> m_mice;
std::map<SDL_JoystickID, std::pair<SDL_Gamepad*, SDL_Haptic*>> m_joysticks;
std::map<SDL_HapticID, SDL_Haptic*> m_otherHaptics;
std::variant<SDL_MouseID_v, SDL_JoystickID_v, SDL_TouchID_v> m_lastInputMethod;
};
// TEMPLATE: LEGO1 0x10028850

View File

@ -11,6 +11,8 @@
#include "mxdebug.h"
#include "roi/legoroi.h"
#include <SDL3/SDL_log.h>
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;
}
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
if (xPos || yPos) {
break;
}
}
*p_joystickX = ((xPos + 32768) * 100) / 65535;
*p_joystickY = ((yPos + 32768) * 100) / 65535;
*p_povPosition = -1;
return SUCCESS;
}
return FAILURE;
}
// 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<SDL_JoystickID_v>(&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<SDL_HapticID> 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;
}
}

View File

@ -59,4 +59,12 @@ inline T RToD(T p_r)
return p_r * 180.0F / 3.1416F;
}
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
#endif // __LEGOUTIL_H

View File

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