From 7c627a8c832b9e3d454a28adc3f8faf856feb308 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 23 Nov 2024 17:53:05 +1000 Subject: [PATCH] Controller: Add JogCon This is probably wrong, but I have no way of testing it with an actual force feedback wheel. PRs welcome to improve it further. --- src/core/CMakeLists.txt | 2 + src/core/controller.cpp | 5 + src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 2 + src/core/jogcon.cpp | 638 ++++++++++++++++++++++++++++++++ src/core/jogcon.h | 150 ++++++++ src/core/types.h | 1 + src/util/imgui_glyph_ranges.inl | 2 +- 8 files changed, 801 insertions(+), 1 deletion(-) create mode 100644 src/core/jogcon.cpp create mode 100644 src/core/jogcon.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 8ae032d3f..1b4d025ce 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -81,6 +81,8 @@ add_library(core imgui_overlays.h interrupt_controller.cpp interrupt_controller.h + jogcon.cpp + jogcon.h justifier.cpp justifier.h mdec.cpp diff --git a/src/core/controller.cpp b/src/core/controller.cpp index 4608f2528..d6d568ce0 100644 --- a/src/core/controller.cpp +++ b/src/core/controller.cpp @@ -8,6 +8,7 @@ #include "game_database.h" #include "guncon.h" #include "host.h" +#include "jogcon.h" #include "justifier.h" #include "negcon.h" #include "negcon_rumble.h" @@ -38,6 +39,7 @@ static const Controller::ControllerInfo* s_controller_info[] = { &Justifier::INFO, &DigitalController::INFO_POPN, &DigitalController::INFO_DDGO, + &JogCon::INFO, }; const std::array Controller::PortDisplayOrder = {{0, 2, 3, 4, 1, 5, 6, 7}}; @@ -140,6 +142,9 @@ std::unique_ptr Controller::Create(ControllerType type, u32 index) case ControllerType::NeGconRumble: return NeGconRumble::Create(index); + case ControllerType::JogCon: + return JogCon::Create(index); + case ControllerType::None: default: return {}; diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 1de21f683..e3f9feaf0 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -51,6 +51,7 @@ + @@ -131,6 +132,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index e1dc75c5c..1068324cc 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -65,6 +65,7 @@ + @@ -139,6 +140,7 @@ + diff --git a/src/core/jogcon.cpp b/src/core/jogcon.cpp new file mode 100644 index 000000000..0f1e29719 --- /dev/null +++ b/src/core/jogcon.cpp @@ -0,0 +1,638 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "jogcon.h" +#include "host.h" +#include "system.h" + +#include "util/imgui_manager.h" +#include "util/input_manager.h" +#include "util/state_wrapper.h" + +#include "common/assert.h" +#include "common/bitutils.h" +#include "common/error.h" +#include "common/log.h" + +#include "IconsEmoji.h" +#include "IconsPromptFont.h" +#include "fmt/format.h" + +LOG_CHANNEL(AnalogController); + +JogCon::JogCon(u32 index) : Controller(index) +{ +} + +JogCon::~JogCon() = default; + +ControllerType JogCon::GetType() const +{ + return ControllerType::JogCon; +} + +void JogCon::Reset() +{ + // Reset starts in jogcon mode? + m_jogcon_mode = true; + ResetTransferState(); + ResetMotorConfig(); +} + +bool JogCon::DoState(StateWrapper& sw, bool apply_input_state) +{ + if (!Controller::DoState(sw, apply_input_state)) + return false; + + u16 button_state = m_button_state; + s8 steering_state = m_steering_state; + sw.Do(&button_state); + sw.Do(&steering_state); + if (apply_input_state) + { + m_button_state = button_state; + m_steering_state = steering_state; + } + + sw.Do(&m_command); + sw.Do(&m_command_step); + sw.Do(&m_status_byte); + sw.Do(&m_last_steering_state); + sw.Do(&m_last_motor_command); + sw.Do(&m_steering_hold_position); + sw.Do(&m_steering_hold_strength); + + sw.Do(&m_configuration_mode); + + bool jogcon_mode = m_jogcon_mode; + sw.Do(&jogcon_mode); + if (jogcon_mode != m_jogcon_mode) + SetJogConMode(jogcon_mode, true); + + sw.Do(&m_rx_buffer); + sw.Do(&m_tx_buffer); + sw.Do(&m_rumble_config); + + return true; +} + +float JogCon::GetBindState(u32 index) const +{ + if (index >= static_cast(Button::MaxCount)) + { + const u32 sub_index = index - static_cast(Button::MaxCount); + if (sub_index >= static_cast(m_half_axis_state.size())) + return 0.0f; + + return static_cast(m_half_axis_state[sub_index]) * (1.0f / 255.0f); + } + else if (index < static_cast(Button::Mode)) + { + return static_cast(((m_button_state >> index) & 1u) ^ 1u); + } + else + { + return 0.0f; + } +} + +void JogCon::SetBindState(u32 index, float value) +{ + if (index == static_cast(Button::Mode)) + { + // analog toggle + if (value >= m_button_deadzone) + { + if (m_command == Command::Idle) + SetJogConMode(!m_jogcon_mode, true); + else + m_mode_toggle_queued = true; + } + + return; + } + else if (index >= static_cast(Button::MaxCount)) + { + const u32 sub_index = index - static_cast(Button::MaxCount); + if (sub_index >= static_cast(m_half_axis_state.size())) + return; + + const u8 u8_value = static_cast( + std::clamp(((value < m_analog_deadzone) ? 0.0f : value) * m_analog_sensitivity * 255.0f, 0.0f, 255.0f)); + if (u8_value == m_half_axis_state[sub_index]) + return; + + m_half_axis_state[sub_index] = u8_value; + System::SetRunaheadReplayFlag(); + + m_steering_state = + (m_half_axis_state[static_cast(HalfAxis::SteeringRight)] != 0) ? + static_cast((m_half_axis_state[static_cast(HalfAxis::SteeringRight)] / 2)) : + -static_cast((static_cast(m_half_axis_state[static_cast(HalfAxis::SteeringLeft)]) + 1) / 2); + } + + const u16 bit = u16(1) << static_cast(index); + + if (value >= m_button_deadzone) + { + if (m_button_state & bit) + System::SetRunaheadReplayFlag(); + + m_button_state &= ~(bit); + } + else + { + if (!(m_button_state & bit)) + System::SetRunaheadReplayFlag(); + + m_button_state |= bit; + } +} + +u32 JogCon::GetButtonStateBits() const +{ + return m_button_state ^ 0xFFFF; +} + +void JogCon::ResetTransferState() +{ + if (m_mode_toggle_queued) + { + SetJogConMode(!m_jogcon_mode, true); + m_mode_toggle_queued = false; + } + + m_command = Command::Idle; + m_command_step = 0; +} + +u32 JogCon::GetInputOverlayIconColor() const +{ + return m_jogcon_mode ? 0xFF2534F0u : 0xFFCCCCCCu; +} + +void JogCon::SetJogConMode(bool enabled, bool show_message) +{ + if (m_jogcon_mode == enabled) + return; + + m_jogcon_mode = enabled; + m_configuration_mode = enabled && m_configuration_mode; + + INFO_LOG("Controller {} switched to {} mode.", m_index + 1u, m_jogcon_mode ? "JogCon" : "Digital"); + if (show_message) + { + Host::AddIconOSDMessage( + fmt::format("Controller{}JogConMode", m_index), ICON_PF_GAMEPAD_ALT, + m_jogcon_mode ? fmt::format(TRANSLATE_FS("Controller", "Controller {} switched to JogCon mode."), m_index + 1u) : + fmt::format(TRANSLATE_FS("Controller", "Controller {} switched to Digital mode."), m_index + 1u)); + } +} + +u8 JogCon::GetIDByte() const +{ + return Truncate8((GetModeID() << 4) | GetResponseNumHalfwords()); +} + +u8 JogCon::GetModeID() const +{ + if (m_configuration_mode) + return 0xF; + else if (m_jogcon_mode) + return 0xE; + else + return 0x4; +} + +u8 JogCon::GetResponseNumHalfwords() const +{ + return m_jogcon_mode ? 3 : 1; +} + +void JogCon::SetMotorState(u8 value) +{ + const u8 command = (value >> 4); + const u8 strength = (value & 0x0F); + + DEV_LOG("0x{:02X} command=0x{:X} force={}", value, command, strength); + + switch (command) + { + case MOTOR_COMMAND_STOP: + { + m_steering_hold_strength = 0; + SetMotorDirection(MOTOR_COMMAND_STOP, 0); + } + break; + + case MOTOR_COMMAND_RIGHT: + case MOTOR_COMMAND_LEFT: + { + m_steering_hold_strength = 0; + SetMotorDirection(command, strength); + } + break; + + case MOTOR_COMMAND_HOLD: + case MOTOR_COMMAND_DROP_REVOLUTIONS_AND_HOLD: + { + DEV_LOG("Hold wheel in position {} with {} strength.", m_steering_hold_position, strength); + m_steering_hold_strength = strength; + UpdateSteeringHold(); + + if (command == MOTOR_COMMAND_DROP_REVOLUTIONS_AND_HOLD) + ERROR_LOG("JogCon Drop revolutions and hold command is not handled."); + } + break; + + case MOTOR_COMMAND_DROP_REVOLUTIONS: + { + ERROR_LOG("JogCon drop revolutions command is not handled."); + } + break; + + case MOTOR_COMMAND_NEW_HOLD: + { + ERROR_LOG("JogCon new hold position {}", m_steering_state); + m_steering_hold_position = m_steering_state; + } + break; + + default: + { + ERROR_LOG("Unknown JogCon command 0x{:X}", command); + } + break; + } + + m_last_motor_command = command; +} + +void JogCon::SetMotorDirection(u8 direction_command, u8 strength) +{ + if (direction_command == MOTOR_COMMAND_STOP || strength == 0) + { + DEV_LOG("Stop motor"); + if (m_force_feedback_device) + m_force_feedback_device->DisableForce(ForceFeedbackDevice::Effect::Constant); + InputManager::SetPadVibrationIntensity(m_index, 0.0f, 0.0f); + return; + } + + DEV_LOG("Turn wheel {} with {} strength", (direction_command == MOTOR_COMMAND_LEFT) ? "LEFT" : "RIGHT", strength); + + const float f_strength = (static_cast(strength) / 15.0f); + if (m_force_feedback_device) + { + // 0->15 => -32768..32767, direction is flipped because it's indicating where the force is coming _from_. + const s32 ffb_value = + static_cast(f_strength * ((direction_command == MOTOR_COMMAND_LEFT) ? 32767.0f : -32768.0f)); + m_force_feedback_device->SetConstantForce(ffb_value); + } + + InputManager::SetPadVibrationIntensity(m_index, f_strength, 0.0f); +} + +void JogCon::UpdateSteeringHold() +{ + if (m_steering_hold_strength > 0) + { + const u8 direction_command = + (std::abs(static_cast(m_steering_state) - static_cast(m_steering_hold_position)) < + m_steering_hold_deadzone) ? + MOTOR_COMMAND_STOP : + ((m_steering_state < m_steering_hold_position) ? MOTOR_COMMAND_RIGHT : MOTOR_COMMAND_LEFT); + DEV_LOG("Hold strength {} pos {} hold {} dir {}", m_steering_hold_strength, m_steering_state, + m_steering_hold_position, direction_command); + SetMotorDirection(direction_command, m_steering_hold_strength); + } +} + +void JogCon::ResetMotorConfig() +{ + m_rumble_config.fill(0xFF); + SetMotorState(0); +} + +void JogCon::Poll() +{ + m_tx_buffer[2] = Truncate8(m_button_state); + m_tx_buffer[3] = Truncate8(m_button_state >> 8); + + m_tx_buffer[4] = Truncate8(m_steering_state); + m_tx_buffer[5] = Truncate8(m_steering_state >> 8); // 0xFF if negative, otherwise 0x00 + + u8 rotation_state = 0; + if (m_steering_state > m_last_steering_state) + rotation_state = 1; + else if (m_steering_state < m_last_steering_state) + rotation_state = 2; + + m_tx_buffer[6] = rotation_state | (m_last_motor_command << 4); + + m_last_steering_state = m_steering_state; + UpdateSteeringHold(); +} + +bool JogCon::Transfer(const u8 data_in, u8* data_out) +{ + bool ack; + m_rx_buffer[m_command_step] = data_in; + + switch (m_command) + { + case Command::Idle: + { + *data_out = 0xFF; + + if (data_in == 0x01) + { + DEBUG_LOG("ACK controller access"); + m_command = Command::Ready; + m_tx_buffer.fill(0); + m_rx_buffer.fill(0); + return true; + } + + return false; + } + break; + + case Command::Ready: + { + Assert(m_command_step == 0); + + if (data_in == 0x42) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::ReadPad; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + Poll(); + } + else if (m_jogcon_mode && data_in == 0x43) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::SetMode; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + Poll(); + } + else if (m_configuration_mode && data_in == 0x45) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::GetAnalogMode; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x01, 0x02, BoolToUInt8(m_jogcon_mode), 0x01, 0x01, 0x00}; + } + else if (m_configuration_mode && data_in == 0x46) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::Command46; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + else if (m_configuration_mode && data_in == 0x47) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::Command47; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00}; + } + else if (m_configuration_mode && data_in == 0x4C) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::Command4C; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + else if (m_configuration_mode && data_in == 0x4D) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::GetSetRumble; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + else + { + ERROR_LOG("Unimplemented command 0x{:02X}", data_in); + + *data_out = 0xFF; + return false; + } + } + break; + + case Command::ReadPad: + { + if (m_command_step >= 2 && m_command_step < 7 && m_rumble_config[m_command_step - 2] == 0x00) + SetMotorState(data_in); + } + break; + + case Command::GetAnalogMode: + { + // just send the byte, nothing special to do here + } + break; + + case Command::SetMode: + { + m_configuration_mode = (m_rx_buffer[2] == 1 && m_jogcon_mode); + + if (m_configuration_mode) + m_status_byte = 0x5A; + + DEV_LOG("0x{:02x}({}) config mode", m_rx_buffer[2], m_configuration_mode ? "enter" : "leave"); + } + break; + + case Command::GetSetRumble: + { + if (m_command_step >= 2 && m_command_step < 7) + { + const u8 index = m_command_step - 2; + if (index >= 0) + { + m_tx_buffer[m_command_step] = m_rumble_config[index]; + m_rumble_config[index] = data_in; + + if (data_in == 0x00) + WARNING_LOG("Motor mapped to byte index {}", index); + } + } + else + { + // reset motor value if we're no longer mapping it + if (std::find(m_rumble_config.begin(), m_rumble_config.end(), 0) == m_rumble_config.end()) + SetMotorState(0); + } + } + break; + + case Command::Command46: + { + if (m_command_step == 2) + { + if (data_in == 0x00) + { + m_tx_buffer[4] = 0x01; + m_tx_buffer[5] = 0x02; + m_tx_buffer[6] = 0x00; + m_tx_buffer[7] = 0x0A; + } + else if (data_in == 0x01) + { + m_tx_buffer[4] = 0x01; + m_tx_buffer[5] = 0x01; + m_tx_buffer[6] = 0x01; + m_tx_buffer[7] = 0x14; + } + } + } + break; + + case Command::Command47: + { + if (m_command_step == 2 && data_in != 0x00) + { + m_tx_buffer[4] = 0x00; + m_tx_buffer[5] = 0x00; + m_tx_buffer[6] = 0x00; + m_tx_buffer[7] = 0x00; + } + } + break; + + case Command::Command4C: + { + if (m_command_step == 2) + { + if (data_in == 0x00) + m_tx_buffer[5] = 0x04; + else if (data_in == 0x01) + m_tx_buffer[4] = 0x03; + } + } + break; + + DefaultCaseIsUnreachable(); + } + + *data_out = m_tx_buffer[m_command_step]; + + m_command_step = (m_command_step + 1) % m_response_length; + ack = (m_command_step != 0); + + if (m_command_step == 0) + { + m_command = Command::Idle; + + DEBUG_LOG("Rx: {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", m_rx_buffer[0], m_rx_buffer[1], + m_rx_buffer[2], m_rx_buffer[3], m_rx_buffer[4], m_rx_buffer[5], m_rx_buffer[6], m_rx_buffer[7]); + DEBUG_LOG("Tx: {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", m_tx_buffer[0], m_tx_buffer[1], + m_tx_buffer[2], m_tx_buffer[3], m_tx_buffer[4], m_tx_buffer[5], m_tx_buffer[6], m_tx_buffer[7]); + } + + return ack; +} + +void JogCon::LoadSettings(const SettingsInterface& si, const char* section, bool initial) +{ + Controller::LoadSettings(si, section, initial); + + m_analog_deadzone = std::clamp(si.GetFloatValue(section, "AnalogDeadzone", DEFAULT_STICK_DEADZONE), 0.0f, 1.0f); + m_analog_sensitivity = + std::clamp(si.GetFloatValue(section, "AnalogSensitivity", DEFAULT_STICK_SENSITIVITY), 0.01f, 3.0f); + m_button_deadzone = std::clamp(si.GetFloatValue(section, "ButtonDeadzone", DEFAULT_BUTTON_DEADZONE), 0.01f, 1.0f); + m_steering_hold_deadzone = static_cast(std::ceil( + std::clamp(si.GetFloatValue(section, "SteeringHoldDeadzone", DEFAULT_STEERING_HOLD_DEADZONE), 0.0f, 1.0f) * + 127.0f)); + + std::string force_feedback_device_name = si.GetStringValue(section, "ForceFeedbackDevice"); + if (m_force_feedback_device_name != force_feedback_device_name) + { + m_force_feedback_device_name = std::move(force_feedback_device_name); + m_force_feedback_device.reset(); + if (!m_force_feedback_device_name.empty()) + { + Error error; + m_force_feedback_device = InputManager::CreateForceFeedbackDevice(m_force_feedback_device_name, &error); + if (!m_force_feedback_device) + { + ERROR_LOG("Failed to create force feedback device: {}", error.GetDescription()); + if (initial) + { + Host::AddIconOSDWarning( + fmt::format("NoFFDevice{}", m_index), ICON_EMOJI_WARNING, + fmt::format(TRANSLATE_FS("JogCon", "Failed to create force feedback device for Port {}:\n{}"), + Controller::GetPortDisplayName(m_index), error.GetDescription()), + Host::OSD_WARNING_DURATION); + } + } + } + } +} + +std::unique_ptr JogCon::Create(u32 index) +{ + return std::make_unique(index); +} + +static const Controller::ControllerBindingInfo s_binding_info[] = { +#define BUTTON(name, display_name, icon_name, button, genb) \ + {name, display_name, icon_name, static_cast(button), InputBindingInfo::Type::Button, genb} +#define AXIS(name, display_name, icon_name, halfaxis, genb) \ + {name, \ + display_name, \ + icon_name, \ + static_cast(JogCon::Button::MaxCount) + static_cast(halfaxis), \ + InputBindingInfo::Type::HalfAxis, \ + genb} + + // clang-format off + BUTTON("Up", TRANSLATE_NOOP("JogCon", "D-Pad Up"), ICON_PF_DPAD_UP, JogCon::Button::Up, GenericInputBinding::DPadUp), + BUTTON("Right", TRANSLATE_NOOP("JogCon", "D-Pad Right"), ICON_PF_DPAD_RIGHT, JogCon::Button::Right, GenericInputBinding::DPadRight), + BUTTON("Down", TRANSLATE_NOOP("JogCon", "D-Pad Down"), ICON_PF_DPAD_DOWN, JogCon::Button::Down, GenericInputBinding::DPadDown), + BUTTON("Left", TRANSLATE_NOOP("JogCon", "D-Pad Left"), ICON_PF_DPAD_LEFT, JogCon::Button::Left, GenericInputBinding::DPadLeft), + BUTTON("Triangle", TRANSLATE_NOOP("JogCon", "Triangle"), ICON_PF_BUTTON_TRIANGLE, JogCon::Button::Triangle, GenericInputBinding::Triangle), + BUTTON("Circle", TRANSLATE_NOOP("JogCon", "Circle"), ICON_PF_BUTTON_CIRCLE, JogCon::Button::Circle, GenericInputBinding::Circle), + BUTTON("Cross", TRANSLATE_NOOP("JogCon", "Cross"), ICON_PF_BUTTON_CROSS, JogCon::Button::Cross, GenericInputBinding::Cross), + BUTTON("Square", TRANSLATE_NOOP("JogCon", "Square"), ICON_PF_BUTTON_SQUARE, JogCon::Button::Square, GenericInputBinding::Square), + BUTTON("Select", TRANSLATE_NOOP("JogCon", "Select"), ICON_PF_SELECT_SHARE, JogCon::Button::Select, GenericInputBinding::Select), + BUTTON("Start", TRANSLATE_NOOP("JogCon", "Start"), ICON_PF_START, JogCon::Button::Start, GenericInputBinding::Start), + BUTTON("L1", TRANSLATE_NOOP("JogCon", "L1"), ICON_PF_LEFT_SHOULDER_L1, JogCon::Button::L1, GenericInputBinding::L1), + BUTTON("R1", TRANSLATE_NOOP("JogCon", "R1"), ICON_PF_RIGHT_SHOULDER_R1, JogCon::Button::R1, GenericInputBinding::R1), + BUTTON("L2", TRANSLATE_NOOP("JogCon", "L2"), ICON_PF_LEFT_TRIGGER_L2, JogCon::Button::L2, GenericInputBinding::L2), + BUTTON("R2", TRANSLATE_NOOP("JogCon", "R2"), ICON_PF_RIGHT_TRIGGER_R2, JogCon::Button::R2, GenericInputBinding::R2), + BUTTON("Mode", TRANSLATE_NOOP("JogCon", "Mode"), ICON_PF_ANALOG_LEFT_RIGHT, JogCon::Button::Mode, GenericInputBinding::System), + + AXIS("SteeringLeft", TRANSLATE_NOOP("JogCon", "Steering Left"), ICON_PF_ANALOG_LEFT, JogCon::HalfAxis::SteeringLeft, GenericInputBinding::LeftStickLeft), + AXIS("SteeringRight", TRANSLATE_NOOP("JogCon", "Steering Right"), ICON_PF_ANALOG_RIGHT, JogCon::HalfAxis::SteeringRight, GenericInputBinding::LeftStickRight), + + // clang-format on + + {"ForceFeedbackDevice", TRANSLATE_NOOP("JogCon", "Force Feedback Device"), nullptr, + static_cast(JogCon::Button::MaxCount) + static_cast(JogCon::HalfAxis::MaxCount), + InputBindingInfo::Type::Device, GenericInputBinding::Unknown}, + +#undef BUTTON +#undef AXIS +}; + +static const SettingInfo s_settings[] = { + {SettingInfo::Type::Float, "AnalogDeadzone", TRANSLATE_NOOP("JogCon", "Analog Deadzone"), + TRANSLATE_NOOP("JogCon", + "Sets the analog stick deadzone, i.e. the fraction of the stick movement which will be ignored."), + "0.00f", "0.00f", "1.00f", "0.01f", "%.0f%%", nullptr, 100.0f}, + {SettingInfo::Type::Float, "AnalogSensitivity", TRANSLATE_NOOP("JogCon", "Analog Sensitivity"), + TRANSLATE_NOOP("JogCon", "Sets the analog stick axis scaling factor. A value between 130% and 140% is recommended " + "when using recent controllers, e.g. DualShock 4, Xbox One Controller."), + "1.33f", "0.01f", "2.00f", "0.01f", "%.0f%%", nullptr, 100.0f}, + {SettingInfo::Type::Float, "ButtonDeadzone", TRANSLATE_NOOP("JogCon", "Button/Trigger Deadzone"), + TRANSLATE_NOOP( + "JogCon", + "Sets the deadzone for activating buttons/triggers, i.e. the fraction of the trigger which will be ignored."), + "0.25", "0.01", "1.00", "0.01", "%.0f%%", nullptr, 100.0f}, + {SettingInfo::Type::Float, "SteeringHoldDeadzone", TRANSLATE_NOOP("JogCon", "Steering Hold Deadzone"), + TRANSLATE_NOOP( + "JogCon", "Sets the deadzone for holding the wheel at the set position, i.e. when it will not trigger an effect."), + "0.03", "0.01", "1.00", "0.01", "%.0f%%", nullptr, 100.0f}, +}; + +const Controller::ControllerInfo JogCon::INFO = { + ControllerType::JogCon, "JogCon", TRANSLATE_NOOP("ControllerType", "JogCon"), ICON_PF_STEERING_WHEEL, + s_binding_info, s_settings, Controller::VibrationCapabilities::SingleMotor}; diff --git a/src/core/jogcon.h b/src/core/jogcon.h new file mode 100644 index 000000000..ebbc1d43a --- /dev/null +++ b/src/core/jogcon.h @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "controller.h" + +#include + +class ForceFeedbackDevice; + +class JogCon final : public Controller +{ +public: + enum class Button : u8 + { + Select = 0, + L3 = 1, + R3 = 2, + Start = 3, + Up = 4, + Right = 5, + Down = 6, + Left = 7, + L2 = 8, + R2 = 9, + L1 = 10, + R1 = 11, + Triangle = 12, + Circle = 13, + Cross = 14, + Square = 15, + Mode = 16, + MaxCount + }; + + enum class HalfAxis : u8 + { + SteeringLeft, + SteeringRight, + MaxCount, + }; + + static const Controller::ControllerInfo INFO; + + JogCon(u32 index); + ~JogCon() override; + + static std::unique_ptr Create(u32 index); + + ControllerType GetType() const override; + + void Reset() override; + bool DoState(StateWrapper& sw, bool apply_input_state) override; + + float GetBindState(u32 index) const override; + void SetBindState(u32 index, float value) override; + u32 GetButtonStateBits() const override; + u32 GetInputOverlayIconColor() const override; + + void ResetTransferState() override; + bool Transfer(const u8 data_in, u8* data_out) override; + + void LoadSettings(const SettingsInterface& si, const char* section, bool initial) override; + +private: + enum class Command : u8 + { + Idle, + Ready, + ReadPad, + SetMode, + GetAnalogMode, + GetSetRumble, + Command46, + Command47, + Command4C, + }; + + enum : u8 + { + LargeMotor = 0, + SmallMotor = 1 + }; + + enum : u8 + { + MOTOR_COMMAND_STOP = 0x0, + MOTOR_COMMAND_RIGHT = 0x1, + MOTOR_COMMAND_LEFT = 0x2, + MOTOR_COMMAND_HOLD = 0x3, + MOTOR_COMMAND_DROP_REVOLUTIONS = 0x8, + MOTOR_COMMAND_DROP_REVOLUTIONS_AND_HOLD = 0xB, + MOTOR_COMMAND_NEW_HOLD = 0xC, + }; + + static constexpr float DEFAULT_STEERING_HOLD_DEADZONE = 0.03f; + + u8 GetIDByte() const; + u8 GetModeID() const; + + // Get number of response halfwords (excluding the initial controller info halfword) + u8 GetResponseNumHalfwords() const; + + void Poll(); + void UpdateSteeringHold(); + + void SetMotorState(u8 value); + void SetMotorDirection(u8 direction_command, u8 strength); + void ResetMotorConfig(); + + void SetJogConMode(bool enabled, bool show_message); + + // buttons are active low + u16 m_button_state = UINT16_C(0xFFFF); + s8 m_steering_state = 0; + + // both directions of axis state, merged to m_steering_state + std::array(HalfAxis::MaxCount)> m_half_axis_state{}; + + Command m_command = Command::Idle; + u8 m_command_step = 0; + u8 m_response_length = 0; + u8 m_status_byte = 0x5A; + + s8 m_last_steering_state = 0; + u8 m_last_motor_command = 0; + s8 m_steering_hold_position = 0; + u8 m_steering_hold_strength = 0; + + bool m_configuration_mode = false; + bool m_jogcon_mode = false; + bool m_mode_toggle_queued = false; + + std::array m_rumble_config{}; + + // Transmit and receive buffers, not including the first Hi-Z/ack response byte + static constexpr u32 MAX_RESPONSE_LENGTH = 8; + std::array m_rx_buffer; + std::array m_tx_buffer; + + s8 m_steering_hold_deadzone = 0; + + float m_analog_deadzone = 0.0f; + float m_analog_sensitivity = 1.33f; + float m_button_deadzone = 0.0f; + + std::string m_force_feedback_device_name; + std::unique_ptr m_force_feedback_device; +}; diff --git a/src/core/types.h b/src/core/types.h index 3c532b73b..90eb27b7c 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -224,6 +224,7 @@ enum class ControllerType : u8 Justifier, PopnController, DDGoController, + JogCon, Count }; diff --git a/src/util/imgui_glyph_ranges.inl b/src/util/imgui_glyph_ranges.inl index 95c81ba6a..28babefd5 100644 --- a/src/util/imgui_glyph_ranges.inl +++ b/src/util/imgui_glyph_ranges.inl @@ -3,6 +3,6 @@ static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe070,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf026,0xf028,0xf028,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf04a,0xf04c,0xf050,0xf050,0xf056,0xf056,0xf05e,0xf05e,0xf062,0xf063,0xf065,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf083,0xf085,0xf091,0xf091,0xf0ac,0xf0ae,0xf0b2,0xf0b2,0xf0c3,0xf0c3,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e0,0xf0e0,0xf0e2,0xf0e2,0xf0e7,0xf0e8,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf119,0xf119,0xf11b,0xf11c,0xf140,0xf140,0xf144,0xf144,0xf146,0xf146,0xf14a,0xf14a,0xf15b,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1c0,0xf1c0,0xf1c5,0xf1c5,0xf1de,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fb,0xf1fc,0xf201,0xf201,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2c1,0xf2c1,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f1,0xf2f2,0xf302,0xf302,0xf31e,0xf31e,0xf338,0xf338,0xf35d,0xf35d,0xf360,0xf360,0xf362,0xf362,0xf3fd,0xf3fd,0xf410,0xf410,0xf422,0xf422,0xf424,0xf424,0xf462,0xf462,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf51f,0xf51f,0xf538,0xf538,0xf53f,0xf53f,0xf545,0xf545,0xf547,0xf548,0xf54c,0xf54c,0xf55b,0xf55b,0xf55d,0xf55d,0xf565,0xf565,0xf56e,0xf570,0xf575,0xf575,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5c7,0xf5c7,0xf5cb,0xf5cb,0xf5e7,0xf5e7,0xf5ee,0xf5ee,0xf61f,0xf61f,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf70e,0xf70e,0xf78c,0xf78c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7a4,0xf7a5,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf87d,0xf87d,0xf8cc,0xf8cc,0x0,0x0 }; -static constexpr ImWchar PF_ICON_RANGE[] = { 0x2196,0x2199,0x219e,0x21a1,0x21b0,0x21b3,0x21ba,0x21c3,0x21c7,0x21ca,0x21d0,0x21d4,0x21dc,0x21dd,0x21e0,0x21e3,0x21ed,0x21ee,0x21f7,0x21f8,0x21fa,0x21fb,0x227a,0x227f,0x2284,0x2284,0x2349,0x2349,0x235e,0x235e,0x2360,0x2361,0x2364,0x2366,0x23b2,0x23b4,0x23ce,0x23ce,0x23f4,0x23f7,0x2427,0x243a,0x243c,0x243e,0x2460,0x246b,0x248f,0x248f,0x24f5,0x24fd,0x24ff,0x24ff,0x2717,0x2717,0x278a,0x278e,0x27fc,0x27fc,0xe001,0xe001,0xff21,0xff3a,0x1f52b,0x1f52b,0x0,0x0 }; +static constexpr ImWchar PF_ICON_RANGE[] = { 0x2196,0x2199,0x219e,0x21a1,0x21b0,0x21b3,0x21ba,0x21c3,0x21c7,0x21ca,0x21d0,0x21d4,0x21dc,0x21dd,0x21e0,0x21e3,0x21ed,0x21ee,0x21f7,0x21f8,0x21fa,0x21fb,0x227a,0x227f,0x2284,0x2284,0x2349,0x2349,0x235e,0x235e,0x2360,0x2361,0x2364,0x2366,0x23b2,0x23b4,0x23ce,0x23ce,0x23f4,0x23f7,0x2427,0x243a,0x243c,0x243e,0x2460,0x246b,0x248f,0x248f,0x24f5,0x24fd,0x24ff,0x24ff,0x2717,0x2717,0x278a,0x278e,0x27fc,0x27fc,0xe001,0xe001,0xff21,0xff3a,0x1f52b,0x1f52b,0x1f578,0x1f578,0x0,0x0 }; static constexpr ImWchar EMOJI_ICON_RANGE[] = { 0x2139,0x2139,0x23e9,0x23ea,0x23f8,0x23f8,0x26a0,0x26a0,0x1f4be,0x1f4be,0x1f4c2,0x1f4c2,0x1f4f7,0x1f4f8,0x1f504,0x1f504,0x1f507,0x1f507,0x1f509,0x1f50a,0x1f50d,0x1f50d,0x1f513,0x1f513,0x0,0x0 };