#include "game_controller.h" #include #include "dualsense_effects.h" #include "game_controller.h" #include "game/system/hid/sdl_util.h" #include "fmt/format.h" GameController::GameController(int sdl_device_id, std::shared_ptr settings) : m_sdl_instance_id(sdl_device_id) { m_settings = settings; m_loaded = false; m_device_handle = SDL_OpenGamepad(sdl_device_id); if (!m_device_handle) { sdl_util::log_error( fmt::format("Could not open game controller with device id: {}", sdl_device_id)); return; } m_low_device_handle = SDL_GetGamepadJoystick(m_device_handle); if (!m_low_device_handle) { sdl_util::log_error( fmt::format("Could not get underlying joystick for gamecontroller: id {}", sdl_device_id)); return; } auto test = SDL_GetNumJoystickAxes(m_low_device_handle); m_sdl_instance_id = SDL_GetJoystickID(m_low_device_handle); if (!m_sdl_instance_id) { sdl_util::log_error( fmt::format("Could not determine instance id for gamecontroller: id {}", sdl_device_id)); return; } const auto controller_guid = SDL_GetJoystickGUID(m_low_device_handle); if (sdl_util::is_SDL_GUID_zero(controller_guid)) { sdl_util::log_error(fmt::format("Could not determine controller guid: id {}", sdl_device_id)); return; } char guidStr[33]; SDL_GUIDToString(controller_guid, guidStr, sizeof(guidStr)); m_guid = guidStr; // NOTE - it has been observed that this function will return `NULL` which indicates a bad // controller this seems to happen if you disconnect the controller while it's in the process of // connecting // // However, SDL_GameControllerGetAttached returns true, and `NULL` here is not an outright failure // there could be controllers that just have no name. // // So I'm letting this one go through, in most cases it had an invalid guid as well (so caught // above). auto name = SDL_GetGamepadName(m_device_handle); if (!name) { sdl_util::log_error(fmt::format("Could not get device name with id: {}", sdl_device_id)); m_device_name = ""; } else { m_device_name = name; } auto properties = SDL_GetGamepadProperties(m_device_handle); if (!properties) { sdl_util::log_error( fmt::format("Unable to retrieve properties for gamepad: id {}", sdl_device_id)); m_has_led = false; m_has_rumble = false; m_has_trigger_rumble = false; m_is_dualsense = false; } else { m_has_led = SDL_GetBooleanProperty(properties, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false); m_has_rumble = SDL_GetBooleanProperty(properties, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false); m_has_trigger_rumble = SDL_GetBooleanProperty(properties, SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN, false); m_is_dualsense = SDL_GetGamepadType(m_device_handle) == SDL_GAMEPAD_TYPE_PS5; clear_trigger_effect(dualsense_effects::TriggerEffectOption::BOTH); } if (m_settings->controller_binds.find(m_guid) == m_settings->controller_binds.end()) { m_settings->controller_binds[m_guid] = DEFAULT_CONTROLLER_BINDS; } const auto num_axes = SDL_GetNumJoystickAxes(m_low_device_handle); m_has_pressure_sensitive_buttons = SDL_GetGamepadType(m_device_handle) == SDL_GAMEPAD_TYPE_PS3 && num_axes == 16; if (m_has_pressure_sensitive_buttons) { lg::info("Detected a PS3 controller with support for pressure sensitive buttons"); } m_loaded = true; } static std::unordered_map button_to_pressure_axes = { {SDL_GAMEPAD_BUTTON_SOUTH, 6}, {SDL_GAMEPAD_BUTTON_EAST, 7}, {SDL_GAMEPAD_BUTTON_WEST, 8}, {SDL_GAMEPAD_BUTTON_NORTH, 9}, {SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, 10}, {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, 11}, {SDL_GAMEPAD_BUTTON_DPAD_UP, 12}, {SDL_GAMEPAD_BUTTON_DPAD_DOWN, 13}, {SDL_GAMEPAD_BUTTON_DPAD_LEFT, 14}, {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, 15}}; static std::unordered_map pressure_axes_to_button = { {6, SDL_GAMEPAD_BUTTON_SOUTH}, {7, SDL_GAMEPAD_BUTTON_EAST}, {8, SDL_GAMEPAD_BUTTON_WEST}, {9, SDL_GAMEPAD_BUTTON_NORTH}, {10, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER}, {11, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER}, {12, SDL_GAMEPAD_BUTTON_DPAD_UP}, {13, SDL_GAMEPAD_BUTTON_DPAD_DOWN}, {14, SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {15, SDL_GAMEPAD_BUTTON_DPAD_RIGHT}}; // Adjust the value range to 0-255 (127 being neutral) // Values come out of SDL as -32,768 to 32,767 int normalize_axes_value(int sdl_val) { return ((sdl_val + 32768) * 256) / 65536; } // Adjust the value range to 0-255 (127 being neutral) // Values come out of SDL as -32,768 to 32,767 // with 5% of the bottom range ignored for drift compensation int normalize_axes_value_drift_compensated(int sdl_val) { if (sdl_val < -32768 + 1638) { return 0; } if (sdl_val > 32767 - 1638) { return 255; } return normalize_axes_value(sdl_val); } void GameController::process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, std::optional& bind_assignment) { if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION && event.gaxis.which == m_sdl_instance_id) { // https://wiki.libsdl.org/SDL3/SDL_GamepadAxis if ((int)event.gaxis.axis <= SDL_GAMEPAD_AXIS_INVALID || event.gaxis.axis >= SDL_GAMEPAD_AXIS_COUNT) { return; } auto& binds = m_settings->controller_binds.at(m_guid); // Handle analog stick binds if (event.gaxis.axis >= SDL_GAMEPAD_AXIS_LEFTX && event.gaxis.axis <= SDL_GAMEPAD_AXIS_RIGHTY && !data->analogs_being_simulated() && binds.analog_axii.find(event.gaxis.axis) != binds.analog_axii.end()) { for (const auto& bind : binds.analog_axii.at(event.gaxis.axis)) { data->analog_data.at(bind.pad_data_index) = normalize_axes_value(m_settings->axis_scale * event.gaxis.value); } } else if (event.gaxis.axis >= SDL_GAMEPAD_AXIS_LEFT_TRIGGER && event.gaxis.axis <= SDL_GAMEPAD_AXIS_RIGHT_TRIGGER && binds.button_axii.find(event.gaxis.axis) != binds.button_axii.end()) { // Binding re-assignment if (bind_assignment) { // In the event that the user binds an analog input to the confirm binds // (ie. a trigger on X) we wait until it hits a neutral position (0) // before proceeding to rebind. // // TODO - this still isn't perfect as the data mutation below will trigger it after the bind // assignment has been set but atleast it's in a mostly working state right now if (!bind_assignment->seen_controller_confirm_neutral) { for (const auto& confirm_bind : bind_assignment->controller_confirmation_binds) { if (confirm_bind.sdl_idx == event.gaxis.axis) { if (event.gaxis.value <= 0) { bind_assignment->seen_controller_confirm_neutral = true; } return; } } } if (bind_assignment->device_type == InputDeviceType::CONTROLLER && !bind_assignment->for_analog) { binds.assign_button_bind(event.gaxis.axis, bind_assignment.value(), true); } return; } // and analog triggers for (const auto& bind : binds.button_axii.at(event.gaxis.axis)) { // ignore 5% of the bottom range for temporary drift compensation // do something better later (and for the sticks too) data->button_data.at(bind.pad_data_index) = event.gaxis.value > 1638; const auto pressure_index = data->button_index_to_pressure_index( static_cast(bind.pad_data_index)); if (pressure_index != PadData::PressureIndex::INVALID_PRESSURE) { if (m_settings->enable_pressure_sensitivity && m_has_pressure_sensitive_buttons) { data->pressure_data.at(pressure_index) = normalize_axes_value_drift_compensated( m_settings->pressure_scale * event.gaxis.value); } else { data->pressure_data.at(pressure_index) = event.gaxis.value > 1638 ? 255 : 0; } } } } } else if ((event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || event.type == SDL_EVENT_GAMEPAD_BUTTON_UP) && event.gbutton.which == m_sdl_instance_id) { auto& binds = m_settings->controller_binds.at(m_guid); // https://wiki.libsdl.org/SDL3/SDL_GamepadButton if ((int)event.gbutton.button <= SDL_GAMEPAD_BUTTON_INVALID || event.gbutton.button >= SDL_GAMEPAD_BUTTON_COUNT) { return; } // Binding re-assignment if (bind_assignment && event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) { if (bind_assignment->device_type == InputDeviceType::CONTROLLER && !bind_assignment->for_analog) { binds.assign_button_bind(event.gbutton.button, bind_assignment.value()); } return; } if (binds.buttons.find(event.gbutton.button) == binds.buttons.end()) { return; } // Iterate the binds, and apply all of them for (const auto& bind : binds.buttons.at(event.gbutton.button)) { // 1. pressed flag data->button_data.at(bind.pad_data_index) = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN; // 2. potentially get the pressure value as well const auto pressure_index = data->button_index_to_pressure_index( static_cast(bind.pad_data_index)); if (pressure_index != PadData::PressureIndex::INVALID_PRESSURE) { if (m_settings->enable_pressure_sensitivity && m_has_pressure_sensitive_buttons && button_to_pressure_axes.contains(event.gbutton.button)) { // TODO - handle error const auto pressure_val = SDL_GetJoystickAxis( m_low_device_handle, button_to_pressure_axes.at(event.gbutton.button)); data->pressure_data.at(pressure_index) = normalize_axes_value(pressure_val); } else { data->pressure_data.at(pressure_index) = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ? 255 : 0; } } } // Check for commands if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN && commands.controller_binds.find(event.gbutton.button) != commands.controller_binds.end()) { for (const auto& command : commands.controller_binds.at(event.gbutton.button)) { if (command.event_command) { command.event_command(event); } else if (command.command) { command.command(); } else { lg::warn("CommandBinding has no valid callback for controller bind"); } } } } else if (event.type == SDL_EVENT_JOYSTICK_AXIS_MOTION && m_has_pressure_sensitive_buttons) { // handle changes in pressure on axii not mapped to the gamepad (ie. PS3 analog buttons) if (m_settings->enable_pressure_sensitivity && pressure_axes_to_button.contains(event.jaxis.axis)) { auto& binds = m_settings->controller_binds.at(m_guid); // Work backwords, get the button so we can iterate the binds const auto sdl_button_index = pressure_axes_to_button.at(event.jaxis.axis); for (const auto& bind : binds.buttons.at(sdl_button_index)) { const auto pressure_index = data->button_index_to_pressure_index( static_cast(bind.pad_data_index)); if (pressure_index != PadData::PressureIndex::INVALID_PRESSURE) { data->pressure_data.at(pressure_index) = normalize_axes_value(event.jaxis.value); } } } } } void GameController::close_device() { if (m_device_handle) { clear_trigger_effect(dualsense_effects::TriggerEffectOption::BOTH); SDL_CloseGamepad(m_device_handle); m_device_handle = NULL; } } int GameController::send_rumble(const u8 low_rumble, const u8 high_rumble) { if (m_has_rumble && m_device_handle) { // https://wiki.libsdl.org/SDL3/SDL_RumbleGamepad // Arbitrary duration, since it's called every frame anyway, just so the vibration doesn't last // forever. SDL expects a value in the range of 0-0xFFFF, so the `257` is just normalizing the // 0-255 we get to that range // // NOTE - the 100ms is arbitrary, not sure how long the vibration lasted on the original ps2? if (!SDL_RumbleGamepad(m_device_handle, low_rumble * 257, high_rumble * 257, 100)) { return 1; } } return 0; } void GameController::send_trigger_rumble(const u16 left_rumble, const u16 right_rumble, const u32 duration_ms) { if (m_has_trigger_rumble && m_device_handle) { // https://wiki.libsdl.org/SDL3/SDL_RumbleGamepadTriggers if (!SDL_RumbleGamepadTriggers(m_device_handle, left_rumble, right_rumble, duration_ms)) { sdl_util::log_error("Unable to rumble gamepad's triggers!"); } } } dualsense_effects::DS5EffectsState_t prepare_effects_struct( const dualsense_effects::TriggerEffectOption option, const std::array& effect) { dualsense_effects::DS5EffectsState_t effects; SDL_zero(effects); // Modify right (0x04) and left (0x08) trigger effect if (option == dualsense_effects::TriggerEffectOption::LEFT || option == dualsense_effects::TriggerEffectOption::BOTH) { effects.ucEnableBits1 |= 0x08; SDL_memcpy(effects.rgucLeftTriggerEffect, effect.data(), sizeof(effect)); } if (option == dualsense_effects::TriggerEffectOption::RIGHT || option == dualsense_effects::TriggerEffectOption::BOTH) { effects.ucEnableBits1 |= 0x04; SDL_memcpy(effects.rgucRightTriggerEffect, effect.data(), sizeof(effect)); } return effects; } void GameController::clear_trigger_effect(dualsense_effects::TriggerEffectOption option) { if (m_is_dualsense && m_device_handle) { const auto trigger_effect = dualsense_effects::trigger_effect_off(); const auto effects = prepare_effects_struct(option, trigger_effect); if (!SDL_SendGamepadEffect(m_device_handle, &effects, sizeof(effects))) { sdl_util::log_error("Unable to clear dualsense trigger effect!"); } } } void GameController::send_trigger_effect_feedback(dualsense_effects::TriggerEffectOption option, u8 position, u8 strength) { if (m_is_dualsense && m_device_handle) { const auto trigger_effect = dualsense_effects::trigger_effect_feedback(position, strength); const auto effects = prepare_effects_struct(option, trigger_effect); if (!SDL_SendGamepadEffect(m_device_handle, &effects, sizeof(effects))) { sdl_util::log_error("Unable to send dualsense feedback trigger effect!"); } } } void GameController::send_trigger_effect_vibrate(dualsense_effects::TriggerEffectOption option, u8 position, u8 amplitude, u8 frequency) { if (m_is_dualsense && m_device_handle) { const auto trigger_effect = dualsense_effects::trigger_effect_vibration(position, amplitude, frequency); const auto effects = prepare_effects_struct(option, trigger_effect); if (!SDL_SendGamepadEffect(m_device_handle, &effects, sizeof(effects))) { sdl_util::log_error("Unable to send dualsense vibrate trigger effect!"); } } } void GameController::send_trigger_effect_weapon(dualsense_effects::TriggerEffectOption option, u8 start_position, u8 end_position, u8 strength) { if (m_is_dualsense && m_device_handle) { const auto trigger_effect = dualsense_effects::trigger_effect_weapon(start_position, end_position, strength); const auto effects = prepare_effects_struct(option, trigger_effect); if (!SDL_SendGamepadEffect(m_device_handle, &effects, sizeof(effects))) { sdl_util::log_error("Unable to send dualsense weapon trigger effect!"); } } } void GameController::set_led(const u8 red, const u8 green, const u8 blue) { if (!m_has_led) { return; } if (m_device_handle) { if (!SDL_SetGamepadLED(m_device_handle, red, green, blue)) { m_has_led = false; } } }