2025-05-28 14:22:20 +05:00
|
|
|
/**************************************************************************/
|
|
|
|
/* joypad_sdl.cpp */
|
|
|
|
/**************************************************************************/
|
|
|
|
/* This file is part of: */
|
|
|
|
/* GODOT ENGINE */
|
|
|
|
/* https://godotengine.org */
|
|
|
|
/**************************************************************************/
|
|
|
|
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
|
|
|
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
|
|
|
/* */
|
|
|
|
/* Permission is hereby granted, free of charge, to any person obtaining */
|
|
|
|
/* a copy of this software and associated documentation files (the */
|
|
|
|
/* "Software"), to deal in the Software without restriction, including */
|
|
|
|
/* without limitation the rights to use, copy, modify, merge, publish, */
|
|
|
|
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
|
|
|
/* permit persons to whom the Software is furnished to do so, subject to */
|
|
|
|
/* the following conditions: */
|
|
|
|
/* */
|
|
|
|
/* The above copyright notice and this permission notice shall be */
|
|
|
|
/* included in all copies or substantial portions of the Software. */
|
|
|
|
/* */
|
|
|
|
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
|
|
|
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
|
|
|
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
|
|
|
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
|
|
|
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
|
|
|
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
|
|
|
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
|
|
|
/**************************************************************************/
|
|
|
|
|
|
|
|
#include "joypad_sdl.h"
|
|
|
|
|
|
|
|
#ifdef SDL_ENABLED
|
|
|
|
|
|
|
|
#include "core/input/default_controller_mappings.h"
|
|
|
|
#include "core/os/time.h"
|
|
|
|
#include "core/variant/dictionary.h"
|
|
|
|
|
|
|
|
#include <iterator>
|
|
|
|
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <SDL3/SDL_error.h>
|
|
|
|
#include <SDL3/SDL_events.h>
|
|
|
|
#include <SDL3/SDL_gamepad.h>
|
|
|
|
#include <SDL3/SDL_iostream.h>
|
|
|
|
#include <SDL3/SDL_joystick.h>
|
|
|
|
|
|
|
|
JoypadSDL *JoypadSDL::singleton = nullptr;
|
|
|
|
|
|
|
|
// Macro to skip the SDL joystick event handling if the device is an SDL gamepad, because
|
|
|
|
// there are separate events for SDL gamepads
|
|
|
|
#define SKIP_EVENT_FOR_GAMEPAD \
|
|
|
|
if (SDL_IsGamepad(sdl_event.jdevice.which)) { \
|
|
|
|
continue; \
|
|
|
|
}
|
|
|
|
|
|
|
|
JoypadSDL::JoypadSDL() {
|
|
|
|
singleton = this;
|
|
|
|
}
|
|
|
|
|
2025-08-21 13:21:48 +05:00
|
|
|
#ifdef WINDOWS_ENABLED
|
|
|
|
extern "C" {
|
|
|
|
HWND SDL_HelperWindow;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Required for DInput joypads to work
|
|
|
|
// TODO: remove this workaround when we update to newer version of SDL
|
|
|
|
JoypadSDL::JoypadSDL(HWND p_helper_window) :
|
|
|
|
JoypadSDL() {
|
|
|
|
SDL_HelperWindow = p_helper_window;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2025-05-28 14:22:20 +05:00
|
|
|
JoypadSDL::~JoypadSDL() {
|
|
|
|
// Process any remaining input events
|
|
|
|
process_events();
|
|
|
|
for (int i = 0; i < Input::JOYPADS_MAX; i++) {
|
|
|
|
if (joypads[i].attached) {
|
|
|
|
close_joypad(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
SDL_Quit();
|
|
|
|
singleton = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
JoypadSDL *JoypadSDL::get_singleton() {
|
|
|
|
return singleton;
|
|
|
|
}
|
|
|
|
|
|
|
|
Error JoypadSDL::initialize() {
|
|
|
|
SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1");
|
|
|
|
SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
|
|
|
|
ERR_FAIL_COND_V_MSG(!SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD), FAILED, SDL_GetError());
|
|
|
|
|
|
|
|
// Add Godot's mapping database from memory
|
|
|
|
int i = 0;
|
|
|
|
while (DefaultControllerMappings::mappings[i]) {
|
|
|
|
String mapping_string = DefaultControllerMappings::mappings[i++];
|
|
|
|
CharString data = mapping_string.utf8();
|
|
|
|
SDL_IOStream *rw = SDL_IOFromMem((void *)data.ptr(), data.size());
|
|
|
|
SDL_AddGamepadMappingsFromIO(rw, 1);
|
|
|
|
}
|
|
|
|
|
2025-08-18 23:55:20 +05:00
|
|
|
// Make sure that we handle already connected joypads when the driver is initialized.
|
|
|
|
process_events();
|
|
|
|
|
2025-05-28 14:22:20 +05:00
|
|
|
print_verbose("SDL: Init OK!");
|
|
|
|
return OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
void JoypadSDL::process_events() {
|
|
|
|
// Update rumble first for it to be applied when we handle SDL events
|
|
|
|
for (int i = 0; i < Input::JOYPADS_MAX; i++) {
|
|
|
|
Joypad &joy = joypads[i];
|
|
|
|
if (joy.attached && joy.supports_force_feedback) {
|
|
|
|
uint64_t timestamp = Input::get_singleton()->get_joy_vibration_timestamp(i);
|
|
|
|
|
|
|
|
// Update the joypad rumble only if there was a new vibration request
|
|
|
|
if (timestamp > joy.ff_effect_timestamp) {
|
|
|
|
joy.ff_effect_timestamp = timestamp;
|
|
|
|
|
|
|
|
SDL_Joystick *sdl_joy = SDL_GetJoystickFromID(joypads[i].sdl_instance_idx);
|
|
|
|
Vector2 strength = Input::get_singleton()->get_joy_vibration_strength(i);
|
|
|
|
|
2025-10-03 11:08:39 +05:00
|
|
|
/*
|
|
|
|
If the vibration was requested to start, SDL_RumbleJoystick will start it.
|
|
|
|
If the vibration was requested to stop, strength and duration will be 0, so SDL will stop the rumble.
|
|
|
|
|
|
|
|
Here strength.y goes first and then strength.x, because Input.get_joy_vibration_strength().x
|
|
|
|
is vibration's weak magnitude (high frequency rumble), and .y is strong magnitude (low frequency rumble),
|
|
|
|
SDL_RumbleJoystick takes low frequency rumble first and then high frequency rumble.
|
|
|
|
*/
|
2025-05-28 14:22:20 +05:00
|
|
|
SDL_RumbleJoystick(
|
|
|
|
sdl_joy,
|
|
|
|
// Rumble strength goes from 0 to 0xFFFF
|
|
|
|
strength.y * UINT16_MAX,
|
2025-10-03 11:08:39 +05:00
|
|
|
strength.x * UINT16_MAX,
|
2025-05-28 14:22:20 +05:00
|
|
|
Input::get_singleton()->get_joy_vibration_duration(i) * 1000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
SDL_Event sdl_event;
|
|
|
|
while (SDL_PollEvent(&sdl_event)) {
|
|
|
|
// A new joypad was attached
|
|
|
|
if (sdl_event.type == SDL_EVENT_JOYSTICK_ADDED) {
|
|
|
|
int joy_id = Input::get_singleton()->get_unused_joy_id();
|
|
|
|
if (joy_id == -1) {
|
|
|
|
// There is no space for more joypads...
|
|
|
|
print_error("A new joypad was attached but couldn't allocate a new id for it because joypad limit was reached.");
|
|
|
|
} else {
|
|
|
|
SDL_Joystick *joy = nullptr;
|
2025-08-29 14:22:56 +05:00
|
|
|
SDL_Gamepad *gamepad = nullptr;
|
2025-05-28 14:22:20 +05:00
|
|
|
String device_name;
|
|
|
|
|
|
|
|
// Gamepads must be opened with SDL_OpenGamepad to get their special remapped events
|
|
|
|
if (SDL_IsGamepad(sdl_event.jdevice.which)) {
|
2025-08-29 14:22:56 +05:00
|
|
|
gamepad = SDL_OpenGamepad(sdl_event.jdevice.which);
|
2025-05-28 14:22:20 +05:00
|
|
|
|
|
|
|
ERR_CONTINUE_MSG(!gamepad,
|
|
|
|
vformat("Error opening gamepad at index %d: %s", sdl_event.jdevice.which, SDL_GetError()));
|
|
|
|
|
|
|
|
device_name = SDL_GetGamepadName(gamepad);
|
|
|
|
joy = SDL_GetGamepadJoystick(gamepad);
|
|
|
|
|
|
|
|
print_verbose(vformat("SDL: Gamepad %s connected", SDL_GetGamepadName(gamepad)));
|
|
|
|
} else {
|
|
|
|
joy = SDL_OpenJoystick(sdl_event.jdevice.which);
|
|
|
|
ERR_CONTINUE_MSG(!joy,
|
|
|
|
vformat("Error opening joystick at index %d: %s", sdl_event.jdevice.which, SDL_GetError()));
|
|
|
|
|
|
|
|
device_name = SDL_GetJoystickName(joy);
|
|
|
|
|
|
|
|
print_verbose(vformat("SDL: Joystick %s connected", SDL_GetJoystickName(joy)));
|
|
|
|
}
|
|
|
|
|
|
|
|
const int MAX_GUID_SIZE = 64;
|
|
|
|
char guid[MAX_GUID_SIZE] = {};
|
|
|
|
|
|
|
|
SDL_GUIDToString(SDL_GetJoystickGUID(joy), guid, MAX_GUID_SIZE);
|
|
|
|
SDL_PropertiesID propertiesID = SDL_GetJoystickProperties(joy);
|
|
|
|
|
|
|
|
joypads[joy_id].attached = true;
|
|
|
|
joypads[joy_id].sdl_instance_idx = sdl_event.jdevice.which;
|
|
|
|
joypads[joy_id].supports_force_feedback = SDL_GetBooleanProperty(propertiesID, SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, false);
|
|
|
|
joypads[joy_id].guid = StringName(String(guid));
|
|
|
|
|
|
|
|
sdl_instance_id_to_joypad_id.insert(sdl_event.jdevice.which, joy_id);
|
|
|
|
|
|
|
|
Dictionary joypad_info;
|
2025-08-29 14:22:56 +05:00
|
|
|
joypad_info["mapping_handled"] = true; // Skip Godot's mapping system because SDL already handles the joypad's mapping.
|
|
|
|
joypad_info["raw_name"] = String(SDL_GetJoystickName(joy));
|
|
|
|
joypad_info["vendor_id"] = itos(SDL_GetJoystickVendor(joy));
|
|
|
|
joypad_info["product_id"] = itos(SDL_GetJoystickProduct(joy));
|
|
|
|
|
|
|
|
const uint64_t steam_handle = SDL_GetGamepadSteamHandle(gamepad);
|
|
|
|
if (steam_handle != 0) {
|
|
|
|
joypad_info["steam_input_index"] = itos(steam_handle);
|
|
|
|
}
|
|
|
|
|
|
|
|
const int player_index = SDL_GetJoystickPlayerIndex(joy);
|
|
|
|
if (player_index >= 0) {
|
|
|
|
// For XInput controllers SDL_GetJoystickPlayerIndex returns the XInput user index.
|
|
|
|
joypad_info["xinput_index"] = itos(player_index);
|
|
|
|
}
|
2025-05-28 14:22:20 +05:00
|
|
|
|
|
|
|
Input::get_singleton()->joy_connection_changed(
|
|
|
|
joy_id,
|
|
|
|
true,
|
|
|
|
device_name,
|
|
|
|
joypads[joy_id].guid,
|
|
|
|
joypad_info);
|
|
|
|
}
|
|
|
|
// An event for an attached joypad
|
|
|
|
} else if (sdl_event.type >= SDL_EVENT_JOYSTICK_AXIS_MOTION && sdl_event.type < SDL_EVENT_FINGER_DOWN && sdl_instance_id_to_joypad_id.has(sdl_event.jdevice.which)) {
|
|
|
|
int joy_id = sdl_instance_id_to_joypad_id.get(sdl_event.jdevice.which);
|
|
|
|
|
|
|
|
switch (sdl_event.type) {
|
|
|
|
case SDL_EVENT_JOYSTICK_REMOVED:
|
|
|
|
Input::get_singleton()->joy_connection_changed(joy_id, false, "");
|
|
|
|
close_joypad(joy_id);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SDL_EVENT_JOYSTICK_AXIS_MOTION:
|
|
|
|
SKIP_EVENT_FOR_GAMEPAD;
|
|
|
|
|
|
|
|
Input::get_singleton()->joy_axis(
|
|
|
|
joy_id,
|
|
|
|
static_cast<JoyAxis>(sdl_event.jaxis.axis), // Godot joy axis constants are already intentionally the same as SDL's
|
|
|
|
((sdl_event.jaxis.value - SDL_JOYSTICK_AXIS_MIN) / (float)(SDL_JOYSTICK_AXIS_MAX - SDL_JOYSTICK_AXIS_MIN) - 0.5f) * 2.0f);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SDL_EVENT_JOYSTICK_BUTTON_UP:
|
|
|
|
case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
|
|
|
|
SKIP_EVENT_FOR_GAMEPAD;
|
|
|
|
|
2025-10-03 11:35:09 +05:00
|
|
|
// Some devices report pressing buttons with indices like 232+, 241+, etc. that are not valid,
|
|
|
|
// so we ignore them here.
|
|
|
|
if (sdl_event.jbutton.button >= (int)JoyButton::MAX) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2025-05-28 14:22:20 +05:00
|
|
|
Input::get_singleton()->joy_button(
|
|
|
|
joy_id,
|
|
|
|
static_cast<JoyButton>(sdl_event.jbutton.button), // Godot button constants are intentionally the same as SDL's, so we can just straight up use them
|
|
|
|
sdl_event.jbutton.down);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SDL_EVENT_JOYSTICK_HAT_MOTION:
|
|
|
|
SKIP_EVENT_FOR_GAMEPAD;
|
|
|
|
|
|
|
|
Input::get_singleton()->joy_hat(
|
|
|
|
joy_id,
|
|
|
|
(HatMask)sdl_event.jhat.value // Godot hat masks are identical to SDL hat masks, so we can just use them as-is.
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
|
|
|
|
float axis_value;
|
|
|
|
|
|
|
|
if (sdl_event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || sdl_event.gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
|
|
|
// Gamepad triggers go from 0 to SDL_JOYSTICK_AXIS_MAX
|
|
|
|
axis_value = sdl_event.gaxis.value / (float)SDL_JOYSTICK_AXIS_MAX;
|
|
|
|
} else {
|
|
|
|
// Other axis go from SDL_JOYSTICK_AXIS_MIN to SDL_JOYSTICK_AXIS_MAX
|
|
|
|
axis_value =
|
|
|
|
((sdl_event.gaxis.value - SDL_JOYSTICK_AXIS_MIN) / (float)(SDL_JOYSTICK_AXIS_MAX - SDL_JOYSTICK_AXIS_MIN) - 0.5f) * 2.0f;
|
|
|
|
}
|
|
|
|
|
|
|
|
Input::get_singleton()->joy_axis(
|
|
|
|
joy_id,
|
|
|
|
static_cast<JoyAxis>(sdl_event.gaxis.axis), // Godot joy axis constants are already intentionally the same as SDL's
|
|
|
|
axis_value);
|
|
|
|
} break;
|
|
|
|
|
|
|
|
// Do note SDL gamepads do not have separate events for the dpad
|
|
|
|
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
|
|
|
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
|
|
|
Input::get_singleton()->joy_button(
|
|
|
|
joy_id,
|
|
|
|
static_cast<JoyButton>(sdl_event.gbutton.button), // Godot button constants are intentionally the same as SDL's, so we can just straight up use them
|
|
|
|
sdl_event.gbutton.down);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void JoypadSDL::close_joypad(int p_pad_idx) {
|
|
|
|
int sdl_instance_idx = joypads[p_pad_idx].sdl_instance_idx;
|
|
|
|
|
|
|
|
joypads[p_pad_idx].attached = false;
|
|
|
|
sdl_instance_id_to_joypad_id.erase(sdl_instance_idx);
|
|
|
|
|
|
|
|
if (SDL_IsGamepad(sdl_instance_idx)) {
|
|
|
|
SDL_Gamepad *gamepad = SDL_GetGamepadFromID(sdl_instance_idx);
|
|
|
|
SDL_CloseGamepad(gamepad);
|
|
|
|
} else {
|
|
|
|
SDL_Joystick *joy = SDL_GetJoystickFromID(sdl_instance_idx);
|
|
|
|
SDL_CloseJoystick(joy);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // SDL_ENABLED
|