#include "controls/devices/joystick.h" #include #ifdef USE_SDL3 #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "controls/controller_motion.h" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/stubs.h" namespace devilution { std::vector Joystick::joysticks_; StaticVector Joystick::ToControllerButtonEvents(const SDL_Event &event) { switch (event.type) { case SDL_EVENT_JOYSTICK_BUTTON_DOWN: case SDL_EVENT_JOYSTICK_BUTTON_UP: { #ifdef USE_SDL3 const bool up = !event.jbutton.down; #else const bool up = (event.jbutton.state == SDL_RELEASED); #endif #if defined(JOY_BUTTON_A) || defined(JOY_BUTTON_B) || defined(JOY_BUTTON_X) || defined(JOY_BUTTON_Y) \ || defined(JOY_BUTTON_LEFTSTICK) || defined(JOY_BUTTON_RIGHTSTICK) || defined(JOY_BUTTON_LEFTSHOULDER) || defined(JOY_BUTTON_RIGHTSHOULDER) \ || defined(JOY_BUTTON_TRIGGERLEFT) || defined(JOY_BUTTON_TRIGGERRIGHT) || defined(JOY_BUTTON_START) || defined(JOY_BUTTON_BACK) \ || defined(JOY_BUTTON_DPAD_LEFT) || defined(JOY_BUTTON_DPAD_UP) || defined(JOY_BUTTON_DPAD_RIGHT) || defined(JOY_BUTTON_DPAD_DOWN) switch (event.jbutton.button) { #ifdef JOY_BUTTON_A case JOY_BUTTON_A: return { ControllerButtonEvent { ControllerButton_BUTTON_A, up } }; #endif #ifdef JOY_BUTTON_B case JOY_BUTTON_B: return { ControllerButtonEvent { ControllerButton_BUTTON_B, up } }; #endif #ifdef JOY_BUTTON_X case JOY_BUTTON_X: return { ControllerButtonEvent { ControllerButton_BUTTON_X, up } }; #endif #ifdef JOY_BUTTON_Y case JOY_BUTTON_Y: return { ControllerButtonEvent { ControllerButton_BUTTON_Y, up } }; #endif #ifdef JOY_BUTTON_LEFTSTICK case JOY_BUTTON_LEFTSTICK: return { ControllerButtonEvent { ControllerButton_BUTTON_LEFTSTICK, up } }; #endif #ifdef JOY_BUTTON_RIGHTSTICK case JOY_BUTTON_RIGHTSTICK: return { ControllerButtonEvent { ControllerButton_BUTTON_RIGHTSTICK, up } }; #endif #ifdef JOY_BUTTON_LEFTSHOULDER case JOY_BUTTON_LEFTSHOULDER: return { ControllerButtonEvent { ControllerButton_BUTTON_LEFTSHOULDER, up } }; #endif #ifdef JOY_BUTTON_RIGHTSHOULDER case JOY_BUTTON_RIGHTSHOULDER: return { ControllerButtonEvent { ControllerButton_BUTTON_RIGHTSHOULDER, up } }; #endif #ifdef JOY_BUTTON_TRIGGERLEFT case JOY_BUTTON_TRIGGERLEFT: return { ControllerButtonEvent { ControllerButton_AXIS_TRIGGERLEFT, up } }; #endif #ifdef JOY_BUTTON_TRIGGERRIGHT case JOY_BUTTON_TRIGGERRIGHT: return { ControllerButtonEvent { ControllerButton_AXIS_TRIGGERRIGHT, up } }; #endif #ifdef JOY_BUTTON_START case JOY_BUTTON_START: return { ControllerButtonEvent { ControllerButton_BUTTON_START, up } }; #endif #ifdef JOY_BUTTON_BACK case JOY_BUTTON_BACK: return { ControllerButtonEvent { ControllerButton_BUTTON_BACK, up } }; #endif #ifdef JOY_BUTTON_DPAD_LEFT case JOY_BUTTON_DPAD_LEFT: return { ControllerButtonEvent { ControllerButton_BUTTON_DPAD_LEFT, up } }; #endif #ifdef JOY_BUTTON_DPAD_UP case JOY_BUTTON_DPAD_UP: return { ControllerButtonEvent { ControllerButton_BUTTON_DPAD_UP, up } }; #endif #ifdef JOY_BUTTON_DPAD_RIGHT case JOY_BUTTON_DPAD_RIGHT: return { ControllerButtonEvent { ControllerButton_BUTTON_DPAD_RIGHT, up } }; #endif #ifdef JOY_BUTTON_DPAD_DOWN case JOY_BUTTON_DPAD_DOWN: return { ControllerButtonEvent { ControllerButton_BUTTON_DPAD_DOWN, up } }; #endif default: return { ControllerButtonEvent { ControllerButton_IGNORE, up } }; } #else return { ControllerButtonEvent { ControllerButton_IGNORE, up } }; #endif } #ifdef USE_SDL3 case SDL_EVENT_JOYSTICK_HAT_MOTION: #else case SDL_JOYHATMOTION: #endif { Joystick *joystick = Get(event); if (joystick == nullptr) return { ControllerButtonEvent { ControllerButton_IGNORE, false } }; joystick->UpdateHatState(event.jhat); return joystick->GetHatEvents(); } #ifdef USE_SDL3 case SDL_EVENT_JOYSTICK_AXIS_MOTION: case SDL_EVENT_JOYSTICK_BALL_MOTION: #else case SDL_JOYAXISMOTION: case SDL_JOYBALLMOTION: #endif // ProcessAxisMotion() requires a ControllerButtonEvent parameter // so provide one here using ControllerButton_NONE return { ControllerButtonEvent { ControllerButton_NONE, false } }; default: return {}; } } StaticVector Joystick::GetHatEvents() { StaticVector hatEvents; if (hatState_[0].didStateChange) hatEvents.emplace_back(ControllerButton_BUTTON_DPAD_UP, !hatState_[0].pressed); if (hatState_[1].didStateChange) hatEvents.emplace_back(ControllerButton_BUTTON_DPAD_DOWN, !hatState_[1].pressed); if (hatState_[2].didStateChange) hatEvents.emplace_back(ControllerButton_BUTTON_DPAD_LEFT, !hatState_[2].pressed); if (hatState_[3].didStateChange) hatEvents.emplace_back(ControllerButton_BUTTON_DPAD_RIGHT, !hatState_[3].pressed); if (hatEvents.size() == 0) hatEvents.emplace_back(ControllerButton_IGNORE, false); return hatEvents; } void Joystick::UpdateHatState(const SDL_JoyHatEvent &event) { if (lockHatState_) return; #if defined(JOY_HAT_DPAD_UP_HAT) && defined(JOY_HAT_DPAD_UP) if (event.hat == JOY_HAT_DPAD_UP_HAT) { HatState &hatState = hatState_[0]; bool pressed = (event.value & JOY_HAT_DPAD_UP) != 0; hatState.didStateChange = (pressed != hatState.pressed); hatState.pressed = pressed; } #endif #if defined(JOY_HAT_DPAD_DOWN_HAT) && defined(JOY_HAT_DPAD_DOWN) if (event.hat == JOY_HAT_DPAD_DOWN_HAT) { HatState &hatState = hatState_[1]; bool pressed = (event.value & JOY_HAT_DPAD_DOWN) != 0; hatState.didStateChange = (pressed != hatState.pressed); hatState.pressed = pressed; } #endif #if defined(JOY_HAT_DPAD_LEFT_HAT) && defined(JOY_HAT_DPAD_LEFT) if (event.hat == JOY_HAT_DPAD_LEFT_HAT) { HatState &hatState = hatState_[2]; bool pressed = (event.value & JOY_HAT_DPAD_LEFT) != 0; hatState.didStateChange = (pressed != hatState.pressed); hatState.pressed = pressed; } #endif #if defined(JOY_HAT_DPAD_RIGHT_HAT) && defined(JOY_HAT_DPAD_RIGHT) if (event.hat == JOY_HAT_DPAD_RIGHT_HAT) { HatState &hatState = hatState_[3]; bool pressed = (event.value & JOY_HAT_DPAD_RIGHT) != 0; hatState.didStateChange = (pressed != hatState.pressed); hatState.pressed = pressed; } #endif lockHatState_ = true; } void Joystick::UnlockHatState() { lockHatState_ = false; for (HatState &hatState : hatState_) hatState.didStateChange = false; } int Joystick::ToSdlJoyButton(ControllerButton button) { #if defined(JOY_BUTTON_A) || defined(JOY_BUTTON_B) || defined(JOY_BUTTON_X) || defined(JOY_BUTTON_Y) \ || defined(JOY_BUTTON_BACK) || defined(JOY_BUTTON_START) || defined(JOY_BUTTON_LEFTSTICK) || defined(JOY_BUTTON_RIGHTSTICK) \ || defined(JOY_BUTTON_LEFTSHOULDER) || defined(JOY_BUTTON_RIGHTSHOULDER) || defined(JOY_BUTTON_TRIGGERLEFT) || defined(JOY_BUTTON_TRIGGERRIGHT) \ || defined(JOY_BUTTON_DPAD_LEFT) || defined(JOY_BUTTON_DPAD_UP) || defined(JOY_BUTTON_DPAD_RIGHT) || defined(JOY_BUTTON_DPAD_DOWN) switch (button) { #ifdef JOY_BUTTON_A case ControllerButton_BUTTON_A: return JOY_BUTTON_A; #endif #ifdef JOY_BUTTON_B case ControllerButton_BUTTON_B: return JOY_BUTTON_B; #endif #ifdef JOY_BUTTON_X case ControllerButton_BUTTON_X: return JOY_BUTTON_X; #endif #ifdef JOY_BUTTON_Y case ControllerButton_BUTTON_Y: return JOY_BUTTON_Y; #endif #ifdef JOY_BUTTON_BACK case ControllerButton_BUTTON_BACK: return JOY_BUTTON_BACK; #endif #ifdef JOY_BUTTON_START case ControllerButton_BUTTON_START: return JOY_BUTTON_START; #endif #ifdef JOY_BUTTON_LEFTSTICK case ControllerButton_BUTTON_LEFTSTICK: return JOY_BUTTON_LEFTSTICK; #endif #ifdef JOY_BUTTON_RIGHTSTICK case ControllerButton_BUTTON_RIGHTSTICK: return JOY_BUTTON_RIGHTSTICK; #endif #ifdef JOY_BUTTON_LEFTSHOULDER case ControllerButton_BUTTON_LEFTSHOULDER: return JOY_BUTTON_LEFTSHOULDER; #endif #ifdef JOY_BUTTON_RIGHTSHOULDER case ControllerButton_BUTTON_RIGHTSHOULDER: return JOY_BUTTON_RIGHTSHOULDER; #endif #ifdef JOY_BUTTON_TRIGGERLEFT case ControllerButton_AXIS_TRIGGERLEFT: return JOY_BUTTON_TRIGGERLEFT; #endif #ifdef JOY_BUTTON_TRIGGERRIGHT case ControllerButton_AXIS_TRIGGERRIGHT: return JOY_BUTTON_TRIGGERRIGHT; #endif #ifdef JOY_BUTTON_DPAD_UP case ControllerButton_BUTTON_DPAD_UP: return JOY_BUTTON_DPAD_UP; #endif #ifdef JOY_BUTTON_DPAD_DOWN case ControllerButton_BUTTON_DPAD_DOWN: return JOY_BUTTON_DPAD_DOWN; #endif #ifdef JOY_BUTTON_DPAD_LEFT case ControllerButton_BUTTON_DPAD_LEFT: return JOY_BUTTON_DPAD_LEFT; #endif #ifdef JOY_BUTTON_DPAD_RIGHT case ControllerButton_BUTTON_DPAD_RIGHT: return JOY_BUTTON_DPAD_RIGHT; #endif default: return -1; } #else return -1; #endif } // NOLINTNEXTLINE(readability-convert-member-functions-to-static): Not static if joystick mappings are defined. bool Joystick::IsHatButtonPressed(ControllerButton button) const { #if (defined(JOY_HAT_DPAD_UP_HAT) && defined(JOY_HAT_DPAD_UP)) || (defined(JOY_HAT_DPAD_DOWN_HAT) && defined(JOY_HAT_DPAD_DOWN)) || (defined(JOY_HAT_DPAD_LEFT_HAT) && defined(JOY_HAT_DPAD_LEFT)) || (defined(JOY_HAT_DPAD_RIGHT_HAT) && defined(JOY_HAT_DPAD_RIGHT)) switch (button) { #if defined(JOY_HAT_DPAD_UP_HAT) && defined(JOY_HAT_DPAD_UP) case ControllerButton_BUTTON_DPAD_UP: return (SDL_JoystickGetHat(sdl_joystick_, JOY_HAT_DPAD_UP_HAT) & JOY_HAT_DPAD_UP) != 0; #endif #if defined(JOY_HAT_DPAD_DOWN_HAT) && defined(JOY_HAT_DPAD_DOWN) case ControllerButton_BUTTON_DPAD_DOWN: return (SDL_JoystickGetHat(sdl_joystick_, JOY_HAT_DPAD_DOWN_HAT) & JOY_HAT_DPAD_DOWN) != 0; #endif #if defined(JOY_HAT_DPAD_LEFT_HAT) && defined(JOY_HAT_DPAD_LEFT) case ControllerButton_BUTTON_DPAD_LEFT: return (SDL_JoystickGetHat(sdl_joystick_, JOY_HAT_DPAD_LEFT_HAT) & JOY_HAT_DPAD_LEFT) != 0; #endif #if defined(JOY_HAT_DPAD_RIGHT_HAT) && defined(JOY_HAT_DPAD_RIGHT) case ControllerButton_BUTTON_DPAD_RIGHT: return (SDL_JoystickGetHat(sdl_joystick_, JOY_HAT_DPAD_RIGHT_HAT) & JOY_HAT_DPAD_RIGHT) != 0; #endif default: return false; } #else return false; #endif } bool Joystick::IsPressed(ControllerButton button) const { if (sdl_joystick_ == nullptr) return false; if (IsHatButtonPressed(button)) return true; const int joyButton = ToSdlJoyButton(button); if (joyButton == -1) return false; #ifdef USE_SDL3 const int numButtons = SDL_GetNumJoystickButtons(sdl_joystick_); return joyButton < numButtons && SDL_GetJoystickButton(sdl_joystick_, joyButton); #else const int numButtons = SDL_JoystickNumButtons(sdl_joystick_); return joyButton < numButtons && SDL_JoystickGetButton(sdl_joystick_, joyButton) != 0; #endif } bool Joystick::ProcessAxisMotion(const SDL_Event &event) { if (event.type != SDL_EVENT_JOYSTICK_AXIS_MOTION) return false; #if defined(JOY_AXIS_LEFTX) || defined(JOY_AXIS_LEFTY) || defined(JOY_AXIS_RIGHTX) || defined(JOY_AXIS_RIGHTY) switch (event.jaxis.axis) { #ifdef JOY_AXIS_LEFTX case JOY_AXIS_LEFTX: leftStickXUnscaled = event.jaxis.value; leftStickNeedsScaling = true; return true; #endif #ifdef JOY_AXIS_LEFTY case JOY_AXIS_LEFTY: leftStickYUnscaled = -event.jaxis.value; leftStickNeedsScaling = true; return true; #endif #ifdef JOY_AXIS_RIGHTX case JOY_AXIS_RIGHTX: rightStickXUnscaled = event.jaxis.value; rightStickNeedsScaling = true; return true; #endif #ifdef JOY_AXIS_RIGHTY case JOY_AXIS_RIGHTY: rightStickYUnscaled = -event.jaxis.value; rightStickNeedsScaling = true; return true; #endif default: return false; } #else return false; #endif } #ifdef USE_SDL3 void Joystick::Add(SDL_JoystickID id) { Joystick result; const char *name = SDL_GetJoystickNameForID(id); if (name == nullptr) { LogWarn("Error getting name for joystick {}", id, SDL_GetError()); SDL_ClearError(); } else { Log("Adding joystick {}: {}", id, name); } result.sdl_joystick_ = SDL_OpenJoystick(id); if (result.sdl_joystick_ == nullptr) { LogError("{}", SDL_GetError()); SDL_ClearError(); return; } result.instance_id_ = id; joysticks_.push_back(result); } #else void Joystick::Add(int deviceIndex) { if (SDL_NumJoysticks() <= deviceIndex) return; Joystick result; Log("Adding joystick {}: {}", deviceIndex, SDL_JoystickNameForIndex(deviceIndex)); result.sdl_joystick_ = SDL_JoystickOpen(deviceIndex); if (result.sdl_joystick_ == nullptr) { LogError("{}", SDL_GetError()); SDL_ClearError(); return; } #ifndef USE_SDL1 result.instance_id_ = SDL_JoystickInstanceID(result.sdl_joystick_); #endif joysticks_.push_back(result); } #endif void Joystick::Remove(SDL_JoystickID instanceId) { #ifndef USE_SDL1 Log("Removing joystick (instance id: {})", instanceId); for (std::size_t i = 0; i < joysticks_.size(); ++i) { const Joystick &joystick = joysticks_[i]; if (joystick.instance_id_ != instanceId) continue; joysticks_.erase(joysticks_.begin() + i); return; } Log("Joystick not found with instance id: {}", instanceId); #endif } const std::vector &Joystick::All() { return joysticks_; } Joystick *Joystick::Get(SDL_JoystickID instanceId) { for (auto &joystick : joysticks_) { if (joystick.instance_id_ == instanceId) return &joystick; } return nullptr; } Joystick *Joystick::Get(const SDL_Event &event) { switch (event.type) { #ifndef USE_SDL1 case SDL_EVENT_JOYSTICK_AXIS_MOTION: return Get(event.jaxis.which); case SDL_EVENT_JOYSTICK_BALL_MOTION: return Get(event.jball.which); case SDL_EVENT_JOYSTICK_HAT_MOTION: return Get(event.jhat.which); case SDL_EVENT_JOYSTICK_BUTTON_DOWN: case SDL_EVENT_JOYSTICK_BUTTON_UP: return Get(event.jbutton.which); default: return nullptr; #else case SDL_JOYAXISMOTION: case SDL_JOYBALLMOTION: case SDL_JOYHATMOTION: case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: return joysticks_.empty() ? nullptr : &joysticks_[0]; default: return nullptr; #endif } } bool Joystick::IsPressedOnAnyJoystick(ControllerButton button) { for (auto &joystick : joysticks_) if (joystick.IsPressed(button)) return true; return false; } } // namespace devilution