From 053139ff194100f30515ee1a9e94b57b7319a026 Mon Sep 17 00:00:00 2001 From: Matheus Fraguas Date: Sun, 2 Apr 2023 15:37:21 -0300 Subject: [PATCH] Initial JogCon support --- src/core/CMakeLists.txt | 2 + src/core/controller.cpp | 15 +- src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 2 + src/core/jogcon.cpp | 938 ++++++++++++++++++++++++++++++++++ src/core/jogcon.h | 192 +++++++ src/core/types.h | 1 + 7 files changed, 1150 insertions(+), 2 deletions(-) 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 4710da5af..6311c6f36 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -73,6 +73,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 5847cb4f5..7c02ad629 100644 --- a/src/core/controller.cpp +++ b/src/core/controller.cpp @@ -8,6 +8,7 @@ #include "fmt/format.h" #include "guncon.h" #include "host.h" +#include "jogcon.h" #include "justifier.h" #include "negcon.h" #include "negcon_rumble.h" @@ -23,9 +24,16 @@ static const Controller::ControllerInfo s_none_info = {ControllerType::None, Controller::VibrationCapabilities::NoVibration}; static const Controller::ControllerInfo* s_controller_info[] = { - &s_none_info, &DigitalController::INFO, &AnalogController::INFO, &AnalogJoystick::INFO, - &NeGcon::INFO, &NeGconRumble::INFO, &GunCon::INFO, &PlayStationMouse::INFO, + &s_none_info, + &DigitalController::INFO, + &AnalogController::INFO, + &AnalogJoystick::INFO, + &NeGcon::INFO, + &NeGconRumble::INFO, + &GunCon::INFO, + &PlayStationMouse::INFO, &Justifier::INFO, + &JogCon::INFO, }; const char* Controller::ControllerInfo::GetDisplayName() const @@ -114,6 +122,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 10f20d7e0..5061dc62f 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -60,6 +60,7 @@ + @@ -140,6 +141,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 5543de332..1e1d64bb2 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -67,6 +67,7 @@ + @@ -140,5 +141,6 @@ + \ No newline at end of file diff --git a/src/core/jogcon.cpp b/src/core/jogcon.cpp new file mode 100644 index 000000000..3556991d3 --- /dev/null +++ b/src/core/jogcon.cpp @@ -0,0 +1,938 @@ +// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin and contributors. +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "jogcon.h" +#include "host.h" +#include "settings.h" +#include "system.h" + +#include "util/imgui_manager.h" +#include "util/input_manager.h" +#include "util/state_wrapper.h" + +#include "common/bitutils.h" +#include "common/log.h" +#include "common/string_util.h" + +#include "IconsFontAwesome5.h" +#include "IconsPromptFont.h" + +#include + +Log_SetChannel(JogCon); + +JogCon::JogCon(u32 index) : Controller(index) +{ + m_status_byte = 0x5A; + m_axis_state.fill(0x80); + m_rumble_config.fill(0xFF); +} + +JogCon::~JogCon() = default; + +ControllerType JogCon::GetType() const +{ + return ControllerType::JogCon; +} + +bool JogCon::InAnalogMode() const +{ + return m_analog_mode; +} + +void JogCon::Reset() +{ + m_command = Command::Idle; + m_command_step = 0; + m_rx_buffer.fill(0x00); + m_tx_buffer.fill(0x00); + m_analog_mode = false; + m_configuration_mode = false; + + jog_position = 0x0; + jog_direction = JogconDirection::JOGCON_DIR_NONE; + jog_rotations = 0x0; + jog_last_command = JogconCommand::JOGCON_CMD_NONE; + + for (u32 i = 0; i < NUM_MOTORS; i++) + { + if (m_motor_state[i] != 0) + SetMotorState(i, 0); + } + + m_dualshock_enabled = false; + ResetRumbleConfig(); + + m_status_byte = 0x5A; + + if (m_force_analog_on_reset) + { + if (g_settings.controller_disable_analog_mode_forcing || System::IsRunningUnknownGame()) + { + Host::AddIconOSDMessage( + fmt::format("Controller{}AnalogMode", m_index), ICON_FA_GAMEPAD, + TRANSLATE_STR("OSDMessage", + "Analog mode forcing is disabled by game settings. Controller will start in digital mode."), + 10.0f); + } + else + { + SetAnalogMode(true, false); + } + } +} + +bool JogCon::DoState(StateWrapper& sw, bool apply_input_state) +{ + if (!Controller::DoState(sw, apply_input_state)) + return false; + + const bool old_analog_mode = m_analog_mode; + + sw.Do(&m_analog_mode); + sw.Do(&m_dualshock_enabled); + sw.DoEx(&m_legacy_rumble_unlocked, 44, false); + sw.Do(&m_configuration_mode); + sw.Do(&m_command_param); + sw.DoEx(&m_status_byte, 55, static_cast(0x5A)); + + u16 button_state = m_button_state; + sw.DoEx(&button_state, 44, static_cast(0xFFFF)); + if (apply_input_state) + m_button_state = button_state; + else + m_analog_mode = old_analog_mode; + + sw.Do(&m_command); + + sw.DoEx(&m_rumble_config, 45, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + sw.DoEx(&m_rumble_config_large_motor_index, 45, -1); + sw.DoEx(&m_rumble_config_small_motor_index, 45, -1); + sw.DoEx(&m_analog_toggle_queued, 45, false); + + MotorState motor_state = m_motor_state; + sw.Do(&motor_state); + + if (sw.IsReading()) + { + for (u8 i = 0; i < NUM_MOTORS; i++) + SetMotorState(i, motor_state[i]); + + if (old_analog_mode != m_analog_mode) + { + Host::AddIconOSDMessage(fmt::format("Controller{}AnalogMode", m_index), ICON_FA_GAMEPAD, + fmt::format(m_analog_mode ? + TRANSLATE_FS("JogCon", "Controller {} switched to analog mode.") : + TRANSLATE_FS("JogCon", "Controller {} switched to digital mode."), + m_index + 1u), + 5.0f); + } + } + return true; +} + +float JogCon::GetBindState(u32 index) const +{ + if (index >= static_cast(Button::Count)) + { + const u32 sub_index = index - static_cast(Button::Count); + 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::Analog)) + { + 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::Analog)) + { + // analog toggle + if (value >= m_button_deadzone) + { + if (m_command == Command::Idle) + ProcessAnalogModeToggle(); + else + m_analog_toggle_queued = true; + } + + return; + } + else if (index >= static_cast(Button::Count)) + { + const u32 sub_index = index - static_cast(Button::Count); + if (sub_index >= static_cast(m_half_axis_state.size())) + return; + + const u8 u8_value = static_cast(std::clamp(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(); + +#define MERGE(pos, neg) \ + ((m_half_axis_state[static_cast(pos)] != 0) ? (127u + ((m_half_axis_state[static_cast(pos)] + 1u) / 2u)) : \ + (127u - (m_half_axis_state[static_cast(neg)] / 2u))) + switch (static_cast(sub_index)) + { + case HalfAxis::LLeft: + case HalfAxis::LRight: + m_axis_state[static_cast(Axis::LeftX)] = ((m_invert_left_stick & 1u) != 0u) ? + MERGE(HalfAxis::LLeft, HalfAxis::LRight) : + MERGE(HalfAxis::LRight, HalfAxis::LLeft); + break; + + case HalfAxis::LDown: + case HalfAxis::LUp: + m_axis_state[static_cast(Axis::LeftY)] = ((m_invert_left_stick & 2u) != 0u) ? + MERGE(HalfAxis::LUp, HalfAxis::LDown) : + MERGE(HalfAxis::LDown, HalfAxis::LUp); + break; + + case HalfAxis::RLeft: + case HalfAxis::RRight: + m_axis_state[static_cast(Axis::RightX)] = ((m_invert_right_stick & 1u) != 0u) ? + MERGE(HalfAxis::RLeft, HalfAxis::RRight) : + MERGE(HalfAxis::RRight, HalfAxis::RLeft); + break; + + case HalfAxis::RDown: + case HalfAxis::RUp: + m_axis_state[static_cast(Axis::RightY)] = ((m_invert_right_stick & 2u) != 0u) ? + MERGE(HalfAxis::RUp, HalfAxis::RDown) : + MERGE(HalfAxis::RDown, HalfAxis::RUp); + break; + + default: + break; + } + + if (m_analog_deadzone > 0.0f) + { +#define MERGE_F(pos, neg) \ + ((m_half_axis_state[static_cast(pos)] != 0) ? \ + (static_cast(m_half_axis_state[static_cast(pos)]) / 255.0f) : \ + (static_cast(m_half_axis_state[static_cast(neg)]) / -255.0f)) + + float pos_x, pos_y; + if (static_cast(sub_index) < HalfAxis::RLeft) + { + pos_x = ((m_invert_left_stick & 1u) != 0u) ? MERGE_F(HalfAxis::LLeft, HalfAxis::LRight) : + MERGE_F(HalfAxis::LRight, HalfAxis::LLeft); + pos_y = ((m_invert_left_stick & 2u) != 0u) ? MERGE_F(HalfAxis::LUp, HalfAxis::LDown) : + MERGE_F(HalfAxis::LDown, HalfAxis::LUp); + } + else + { + pos_x = ((m_invert_right_stick & 1u) != 0u) ? MERGE_F(HalfAxis::RLeft, HalfAxis::RRight) : + MERGE_F(HalfAxis::RRight, HalfAxis::RLeft); + ; + pos_y = ((m_invert_right_stick & 2u) != 0u) ? MERGE_F(HalfAxis::RUp, HalfAxis::RDown) : + MERGE_F(HalfAxis::RDown, HalfAxis::RUp); + } + + if (InCircularDeadzone(m_analog_deadzone, pos_x, pos_y)) + { + // Set to 127 (center). + if (static_cast(sub_index) < HalfAxis::RLeft) + m_axis_state[static_cast(Axis::LeftX)] = m_axis_state[static_cast(Axis::LeftY)] = 127; + else + m_axis_state[static_cast(Axis::RightX)] = m_axis_state[static_cast(Axis::RightY)] = 127; + } +#undef MERGE_F + } + +#undef MERGE + + return; + } + + 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 +{ + // flip bits, native data is active low + return m_button_state ^ 0xFFFF; +} + +std::optional JogCon::GetAnalogInputBytes() const +{ + return m_axis_state[static_cast(Axis::LeftY)] << 24 | m_axis_state[static_cast(Axis::LeftX)] << 16 | + m_axis_state[static_cast(Axis::RightY)] << 8 | m_axis_state[static_cast(Axis::RightX)]; +} + +void JogCon::ResetTransferState() +{ + if (m_analog_toggle_queued) + { + ProcessAnalogModeToggle(); + m_analog_toggle_queued = false; + } + + m_command = Command::Idle; + m_command_step = 0; +} + +void JogCon::SetAnalogMode(bool enabled, bool show_message) +{ + if (m_analog_mode == enabled) + return; + + Log_InfoPrintf("Controller %u switched to %s mode.", m_index + 1u, enabled ? "analog" : "digital"); + if (show_message) + { + Host::AddIconOSDMessage(fmt::format("Controller{}AnalogMode", m_index), ICON_FA_GAMEPAD, + fmt::format(enabled ? + TRANSLATE_FS("JogCon", "Controller {} switched to analog mode.") : + TRANSLATE_FS("JogCon", "Controller {} switched to digital mode."), + m_index + 1u), + 5.0f); + } + m_analog_mode = enabled; +} + +void JogCon::ProcessAnalogModeToggle() +{ + if (m_analog_locked) + { + Host::AddIconOSDMessage( + fmt::format("Controller{}AnalogMode", m_index), ICON_FA_GAMEPAD, + fmt::format(m_analog_mode ? + TRANSLATE_FS("JogCon", "Controller {} is locked to analog mode by the game.") : + TRANSLATE_FS("JogCon", "Controller {} is locked to digital mode by the game."), + m_index + 1u), + 5.0f); + } + else + { + SetAnalogMode(!m_analog_mode, true); + ResetRumbleConfig(); + + if (m_dualshock_enabled) + m_status_byte = 0x00; + } +} + +void JogCon::SetMotorState(u32 motor, u8 value) +{ + DebugAssert(motor < NUM_MOTORS); + if (m_motor_state[motor] != value) + { + m_motor_state[motor] = value; + UpdateHostVibration(); + + JogconCommand command = static_cast((value & 0xC0) >> 4); + JogconDirection direction = static_cast((value & 0x30) >> 4); + u8 force = (value & 0x0F); + + jog_last_command = command; + + Log_InfoPrintf("Jogcon %u motor 0x%x: command %s, direction %s, force %x.", m_index + 1u, value, + command == JogconCommand::JOGCON_CMD_NONE ? "NONE" : (command == JogconCommand::JOGCON_CMD_DROP_REVOLUTIONS ? "DROP_REV" : (command == JogconCommand::JOGCON_CMD_NEW_START ? "NEW_START" : "OTHER")), + direction == JogconDirection::JOGCON_DIR_NONE ? "NONE" : + (direction == JogconDirection::JOGCON_DIR_CW ? "CW" : (direction == JogconDirection::JOGCON_DIR_CCW ? "CCW" : (direction == JogconDirection::JOGCON_DIR_START ? "START" : "OTHER"))), + force); + } +} + +void JogCon::UpdateHostVibration() +{ + std::array hvalues; + for (u32 motor = 0; motor < NUM_MOTORS; motor++) + { + const u8 state = m_motor_state[motor]; + const double x = static_cast(state); + const double strength = (state << 8) + state; + + hvalues[motor] = (state != 0) ? static_cast(strength / 65535.0) : 0.0f; + } + + InputManager::SetPadVibrationIntensity(m_index, hvalues[0], hvalues[1]); +} + +u8 JogCon::GetExtraButtonMaskLSB() const +{ + return 0xFF; + /*if (!m_analog_dpad_in_digital_mode || m_analog_mode || m_configuration_mode) + return 0xFF; + + static constexpr u8 NEG_THRESHOLD = static_cast(128.0f - (127.0 * 0.5f)); + static constexpr u8 POS_THRESHOLD = static_cast(128.0f + (127.0 * 0.5f)); + + const bool left = (m_axis_state[static_cast(Axis::LeftX)] <= NEG_THRESHOLD); + const bool right = (m_axis_state[static_cast(Axis::LeftX)] >= POS_THRESHOLD); + const bool up = (m_axis_state[static_cast(Axis::LeftY)] <= NEG_THRESHOLD); + const bool down = (m_axis_state[static_cast(Axis::LeftY)] >= POS_THRESHOLD); + + return ~((static_cast(left) << static_cast(Button::Left)) | + (static_cast(right) << static_cast(Button::Right)) | + (static_cast(up) << static_cast(Button::Up)) | + (static_cast(down) << static_cast(Button::Down)));*/ +} + +void JogCon::ResetRumbleConfig() +{ + m_rumble_config.fill(0xFF); + + m_rumble_config_large_motor_index = -1; + m_rumble_config_small_motor_index = -1; + + SetMotorState(LargeMotor, 0); + SetMotorState(SmallMotor, 0); +} + +void JogCon::SetMotorStateForConfigIndex(int index, u8 value) +{ + if (m_rumble_config_small_motor_index == index) + // SetMotorState(SmallMotor, ((value & 0x01) != 0) ? 255 : 0); + SetMotorState(SmallMotor, value); + else if (m_rumble_config_large_motor_index == index) + SetMotorState(LargeMotor, value); +} + +u8 JogCon::GetResponseNumHalfwords() const +{ + if (m_configuration_mode || m_analog_mode) + return 0x3; + + return (0x1 + m_digital_mode_extra_halfwords); +} + +u8 JogCon::GetModeID() const +{ + if (m_configuration_mode) + return 0xF; + + if (m_analog_mode) + return 0xE; + + return 0x4; +} + +u8 JogCon::GetIDByte() const +{ + return Truncate8((GetModeID() << 4) | GetResponseNumHalfwords()); +} + +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) + { + Log_DebugPrintf("ACK controller access"); + m_command = Command::Ready; + return true; + } + + Log_DevPrintf("Unknown data_in = 0x%02X", data_in); + return false; + } + break; + + case Command::Ready: + { + if (data_in == 0x42) + { + Assert(m_command_step == 0); + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::ReadPad; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + else if (data_in == 0x43) + { + Assert(m_command_step == 0); + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::ConfigModeSetMode; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + else if (m_configuration_mode && data_in == 0x44) + { + Assert(m_command_step == 0); + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::SetAnalogMode; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + ResetRumbleConfig(); + } + else if (m_configuration_mode && data_in == 0x45) + { + Assert(m_command_step == 0); + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::GetAnalogMode; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x01, 0x02, BoolToUInt8(m_analog_mode), 0x01, 0x01, 0x00}; + } + else if (m_configuration_mode && data_in == 0x46) + { + Assert(m_command_step == 0); + 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) + { + Assert(m_command_step == 0); + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::Command47; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}; + } + else if (m_configuration_mode && data_in == 0x4C) + { + Assert(m_command_step == 0); + 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) + { + Assert(m_command_step == 0); + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::GetSetRumble; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + m_rumble_config_large_motor_index = -1; + m_rumble_config_small_motor_index = -1; + } + else + { + if (m_configuration_mode) + Log_ErrorPrintf("Unimplemented config mode command 0x%02X", data_in); + + *data_out = 0xFF; + return false; + } + } + break; + + case Command::ReadPad: + { + const int rumble_index = m_command_step - 2; + + switch (m_command_step) + { + case 2: + { + m_tx_buffer[m_command_step] = Truncate8(m_button_state) & GetExtraButtonMaskLSB(); + + if (m_dualshock_enabled) + SetMotorStateForConfigIndex(rumble_index, data_in); + } + break; + + case 3: + { + m_tx_buffer[m_command_step] = Truncate8(m_button_state >> 8); + + if (m_dualshock_enabled) + { + SetMotorStateForConfigIndex(rumble_index, data_in); + } + else + { + bool legacy_rumble_on = (m_rx_buffer[2] & 0xC0) == 0x40 && (m_rx_buffer[3] & 0x01) != 0; + SetMotorState(SmallMotor, legacy_rumble_on ? 255 : 0); + } + } + break; + + case 4: // 5 position + { + if (m_axis_state[static_cast(Axis::LeftX)] > jog_position) + { + jog_direction = JogconDirection::JOGCON_DIR_CW; + } + else if (m_axis_state[static_cast(Axis::LeftX)] < jog_position) + { + jog_direction = JogconDirection::JOGCON_DIR_CCW; + } + else + { + jog_direction = JogconDirection::JOGCON_DIR_NONE; + } + + if (m_axis_state[static_cast(Axis::LeftX)] - 0x80 < 0x00) + jog_rotations = 0xFF; + + jog_position = m_axis_state[static_cast(Axis::LeftX)]; + + if (m_configuration_mode || m_analog_mode) + m_tx_buffer[m_command_step] = m_axis_state[static_cast(Axis::LeftX)] - 0x80; + + if (m_dualshock_enabled) + SetMotorStateForConfigIndex(rumble_index, data_in); + } + break; + + case 5: // 6 number of rotations + { + if (m_configuration_mode || m_analog_mode) + m_tx_buffer[m_command_step] = jog_rotations; // m_axis_state[static_cast(Axis::RightY)]; + + if (m_dualshock_enabled) + SetMotorStateForConfigIndex(rumble_index, data_in); + } + break; + + case 6: // 7 last command and rotation direction + { + if (m_configuration_mode || m_analog_mode) + m_tx_buffer[m_command_step] = (static_cast(jog_last_command) << 4) | static_cast(jog_direction); + + if (m_dualshock_enabled) + SetMotorStateForConfigIndex(rumble_index, data_in); + + // clear jogcon stuff + jog_rotations = 0x0; + jog_direction = JogconDirection::JOGCON_DIR_NONE; + jog_last_command = JogconCommand::JOGCON_CMD_NONE; + } + break; + + case 7: // 8 + { + if (m_configuration_mode || m_analog_mode) + m_tx_buffer[m_command_step] = 0x00; // m_axis_state[static_cast(Axis::LeftY)]; + + if (m_dualshock_enabled) + SetMotorStateForConfigIndex(rumble_index, data_in); + } + break; + + default: + { + } + break; + } + } + break; + + case Command::ConfigModeSetMode: + { + if (!m_configuration_mode) + { + switch (m_command_step) + { + case 2: + { + m_tx_buffer[m_command_step] = Truncate8(m_button_state) & GetExtraButtonMaskLSB(); + } + break; + + case 3: + { + m_tx_buffer[m_command_step] = Truncate8(m_button_state >> 8); + } + break; + + case 4: + { + if (m_configuration_mode || m_analog_mode) + m_tx_buffer[m_command_step] = m_axis_state[static_cast(Axis::LeftX)] - 0x80; + } + break; + + case 5: + { + if (m_configuration_mode || m_analog_mode) + m_tx_buffer[m_command_step] = 0x00; // m_axis_state[static_cast(Axis::RightY)]; + } + break; + + case 6: + { + if (m_configuration_mode || m_analog_mode) + m_tx_buffer[m_command_step] = static_cast(jog_direction); + } + break; + + case 7: + { + if (m_configuration_mode || m_analog_mode) + m_tx_buffer[m_command_step] = 0x00; // m_axis_state[static_cast(Axis::LeftY)]; + } + break; + + default: + { + } + break; + } + } + + if (m_command_step == (static_cast(m_response_length) - 1)) + { + m_configuration_mode = (m_rx_buffer[2] == 1); + + if (m_configuration_mode) + { + m_dualshock_enabled = true; + m_status_byte = 0x5A; + } + + Log_DevPrintf("0x%02x(%s) config mode", m_rx_buffer[2], m_configuration_mode ? "enter" : "leave"); + } + } + break; + + case Command::SetAnalogMode: + { + if (m_command_step == 2) + { + Log_DevPrintf("analog mode val 0x%02x", data_in); + + if (data_in == 0x00 || data_in == 0x01) + SetAnalogMode((data_in == 0x01), true); + } + else if (m_command_step == 3) + { + Log_DevPrintf("analog mode lock 0x%02x", data_in); + + if (data_in == 0x02 || data_in == 0x03) + m_analog_locked = (data_in == 0x03); + } + } + break; + + case Command::GetAnalogMode: + { + // Intentionally empty, analog mode byte is set in reply buffer when command is first received + } + break; + + case Command::Command46: + { + if (m_command_step == 2) + { + if (data_in == 0x00) + { + m_tx_buffer[4] = 0x04; + m_tx_buffer[5] = 0x03; + m_tx_buffer[6] = 0x01; + m_tx_buffer[7] = 0x1E; + } + else if (data_in == 0x01) + { + m_tx_buffer[4] = 0x00; + m_tx_buffer[5] = 0x00; + m_tx_buffer[6] = 0x00; + m_tx_buffer[7] = 0x00; + } + } + } + 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; + + case Command::GetSetRumble: + { + int rumble_index = m_command_step - 2; + if (rumble_index >= 0) + { + m_tx_buffer[m_command_step] = m_rumble_config[rumble_index]; + m_rumble_config[rumble_index] = data_in; + + if (data_in == 0x00) + m_rumble_config_small_motor_index = rumble_index; + else if (data_in == 0x01) + m_rumble_config_large_motor_index = rumble_index; + } + + if (m_command_step == 7) + { + if (m_rumble_config_large_motor_index == -1) + SetMotorState(LargeMotor, 0); + + if (m_rumble_config_small_motor_index == -1) + SetMotorState(SmallMotor, 0); + } + } + 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) ? false : true; + + if (m_command_step == 0) + { + m_command = Command::Idle; + + Log_DebugPrintf("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]); + Log_DebugPrintf("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]); + + m_rx_buffer.fill(0x00); + m_tx_buffer.fill(0x00); + } + + return ack; +} + +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::Count) + 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("Analog", TRANSLATE_NOOP("JogCon", "Analog Toggle"), ICON_PF_ANALOG_LEFT_RIGHT, JogCon::Button::Analog, GenericInputBinding::System), + 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("L3", TRANSLATE_NOOP("JogCon", "L3"), ICON_PF_LEFT_ANALOG_CLICK, JogCon::Button::L3, GenericInputBinding::L3), + BUTTON("R3", TRANSLATE_NOOP("JogCon", "R3"), ICON_PF_RIGHT_ANALOG_CLICK, JogCon::Button::R3, GenericInputBinding::R3), + + AXIS("LLeft", TRANSLATE_NOOP("JogCon", "Left Stick Left"), ICON_PF_LEFT_ANALOG_LEFT, JogCon::HalfAxis::LLeft, GenericInputBinding::LeftStickLeft), + AXIS("LRight", TRANSLATE_NOOP("JogCon", "Left Stick Right"), ICON_PF_LEFT_ANALOG_RIGHT, JogCon::HalfAxis::LRight, GenericInputBinding::LeftStickRight), + AXIS("LDown", TRANSLATE_NOOP("JogCon", "Left Stick Down"), ICON_PF_LEFT_ANALOG_DOWN, JogCon::HalfAxis::LDown, GenericInputBinding::LeftStickDown), + AXIS("LUp", TRANSLATE_NOOP("JogCon", "Left Stick Up"), ICON_PF_LEFT_ANALOG_UP, JogCon::HalfAxis::LUp, GenericInputBinding::LeftStickUp), + AXIS("RLeft", TRANSLATE_NOOP("JogCon", "Right Stick Left"), ICON_PF_RIGHT_ANALOG_LEFT, JogCon::HalfAxis::RLeft, GenericInputBinding::RightStickLeft), + AXIS("RRight", TRANSLATE_NOOP("JogCon", "Right Stick Right"), ICON_PF_RIGHT_ANALOG_RIGHT, JogCon::HalfAxis::RRight, GenericInputBinding::RightStickRight), + AXIS("RDown", TRANSLATE_NOOP("JogCon", "Right Stick Down"), ICON_PF_RIGHT_ANALOG_DOWN, JogCon::HalfAxis::RDown, GenericInputBinding::RightStickDown), + AXIS("RUp", TRANSLATE_NOOP("JogCon", "Right Stick Up"), ICON_PF_RIGHT_ANALOG_UP, JogCon::HalfAxis::RUp, GenericInputBinding::RightStickUp), +// clang-format on + +#undef AXIS +#undef BUTTON +}; + +static const char* s_invert_settings[] = {TRANSLATE_NOOP("JogCon", "Not Inverted"), + TRANSLATE_NOOP("JogCon", "Invert Left/Right"), + TRANSLATE_NOOP("JogCon", "Invert Up/Down"), + TRANSLATE_NOOP("JogCon", "Invert Left/Right + Up/Down"), nullptr}; + +static const SettingInfo s_settings[] = { + {SettingInfo::Type::Boolean, "ForceAnalogOnReset", TRANSLATE_NOOP("JogCon", "Force Analog Mode on Reset"), + TRANSLATE_NOOP("JogCon", "Forces the controller to analog mode when the console is reset/powered on."), + "true", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, + {SettingInfo::Type::Boolean, "AnalogDPadInDigitalMode", + TRANSLATE_NOOP("JogCon", "Use Analog Sticks for D-Pad in Digital Mode"), + TRANSLATE_NOOP("JogCon", + "Allows you to use the analog sticks to control the d-pad in digital mode, as well as the buttons."), + "true", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, + {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::Integer, "VibrationBias", TRANSLATE_NOOP("JogCon", "Vibration Bias"), + TRANSLATE_NOOP("JogCon", "Sets the rumble bias value. If rumble in some games is too weak or not " + "functioning, try increasing this value."), + "8", "0", "255", "1", "%d", nullptr, 1.0f}, + {SettingInfo::Type::IntegerList, "InvertLeftStick", TRANSLATE_NOOP("JogCon", "Invert Left Stick"), + TRANSLATE_NOOP("JogCon", "Inverts the direction of the left analog stick."), "0", "0", "3", nullptr, + nullptr, s_invert_settings, 0.0f}, + {SettingInfo::Type::IntegerList, "InvertRightStick", TRANSLATE_NOOP("JogCon", "Invert Right Stick"), + TRANSLATE_NOOP("JogCon", "Inverts the direction of the right analog stick."), "0", "0", "3", nullptr, + nullptr, s_invert_settings, 0.0f}, +}; + +const Controller::ControllerInfo JogCon::INFO = {ControllerType::JogCon, + "JogCon", + TRANSLATE_NOOP("ControllerType", "JogCon"), + ICON_PF_GAMEPAD, + s_binding_info, + s_settings, + Controller::VibrationCapabilities::LargeSmallMotors}; + +void JogCon::LoadSettings(SettingsInterface& si, const char* section) +{ + Controller::LoadSettings(si, section); + m_force_analog_on_reset = si.GetBoolValue(section, "ForceAnalogOnReset", true); + m_analog_dpad_in_digital_mode = si.GetBoolValue(section, "AnalogDPadInDigitalMode", true); + 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.0f, 1.0f); + m_rumble_bias = static_cast(std::min(si.GetIntValue(section, "VibrationBias", 8), 255)); + m_invert_left_stick = static_cast(si.GetIntValue(section, "InvertLeftStick", 0)); + m_invert_right_stick = static_cast(si.GetIntValue(section, "InvertRightStick", 0)); +} diff --git a/src/core/jogcon.h b/src/core/jogcon.h new file mode 100644 index 000000000..792be6e26 --- /dev/null +++ b/src/core/jogcon.h @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin and contributors. +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once +#include "controller.h" +#include +#include +#include +#include + +class JogCon final : public Controller +{ +public: + enum class Axis : u8 + { + LeftX, + LeftY, + RightX, + RightY, + Count + }; + + 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, + Analog = 16, + Count + }; + + enum class HalfAxis : u8 + { + LLeft, + LRight, + LDown, + LUp, + RLeft, + RRight, + RDown, + RUp, + Count + }; + + + enum class JogconDirection : u8 + { + JOGCON_DIR_NONE = 0x0, + JOGCON_DIR_CW = 0x1, + JOGCON_DIR_CCW = 0x2, + JOGCON_DIR_START = 0x3, + // Bellow values are only used as return on getJogconData() + JOGCON_DIR_MAX = 0x4, // Position and revolutions maxed out + JOGCON_DIR_OTHER = 0xF // Using 0x0F as generic unhandled code + }; + + enum class JogconCommand : u8 + { + JOGCON_CMD_NONE = 0x0, + JOGCON_CMD_DROP_REVOLUTIONS = 0x8, + JOGCON_CMD_NEW_START = 0xC, + // Bellow values are only used as return on getJogconData() + JOGCON_CMD_OTHER = 0xF // Using 0xF as generic unhandled code return + }; + + static constexpr u8 NUM_MOTORS = 2; + + static const Controller::ControllerInfo INFO; + + JogCon(u32 index); + ~JogCon() override; + + static std::unique_ptr Create(u32 index); + + ControllerType GetType() const override; + bool InAnalogMode() const override; + + void Reset() override; + bool DoState(StateWrapper& sw, bool ignore_input_state) override; + + float GetBindState(u32 index) const override; + void SetBindState(u32 index, float value) override; + u32 GetButtonStateBits() const override; + std::optional GetAnalogInputBytes() const override; + + void ResetTransferState() override; + bool Transfer(const u8 data_in, u8* data_out) override; + + void LoadSettings(SettingsInterface& si, const char* section) override; + +private: + using MotorState = std::array; + + enum class Command : u8 + { + Idle, + Ready, + ReadPad, // 0x42 + ConfigModeSetMode, // 0x43 + SetAnalogMode, // 0x44 + GetAnalogMode, // 0x45 + Command46, // 0x46 + Command47, // 0x47 + Command4C, // 0x4C + GetSetRumble // 0x4D + }; + + Command m_command = Command::Idle; + int m_command_step = 0; + + // 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; + u32 m_response_length = 0; + + // Get number of response halfwords (excluding the initial controller info halfword) + u8 GetResponseNumHalfwords() const; + + u8 GetModeID() const; + u8 GetIDByte() const; + + void SetAnalogMode(bool enabled, bool show_message); + void ProcessAnalogModeToggle(); + void SetMotorState(u32 motor, u8 value); + void UpdateHostVibration(); + u8 GetExtraButtonMaskLSB() const; + void ResetRumbleConfig(); + void SetMotorStateForConfigIndex(int index, u8 value); + + bool m_force_analog_on_reset = false; + bool m_analog_dpad_in_digital_mode = false; + float m_analog_deadzone = 0.0f; + float m_analog_sensitivity = 1.0f; + float m_button_deadzone = 0.0f; + u8 m_rumble_bias = 0; + u8 m_invert_left_stick = 0; + u8 m_invert_right_stick = 0; + + bool m_analog_mode = false; + bool m_analog_locked = false; + bool m_dualshock_enabled = false; + bool m_configuration_mode = false; + + std::array(Axis::Count)> m_axis_state{}; + + enum : u8 + { + LargeMotor = 0, + SmallMotor = 1 + }; + + std::array m_rumble_config{}; + int m_rumble_config_large_motor_index = -1; + int m_rumble_config_small_motor_index = -1; + + bool m_analog_toggle_queued = false; + u8 m_status_byte = 0; + + // TODO: Set this with command 0x4D and increase response length in digital mode accordingly + u8 m_digital_mode_extra_halfwords = 0; + + // buttons are active low + u16 m_button_state = UINT16_C(0xFFFF); + + MotorState m_motor_state{}; + + // both directions of axis state, merged to m_axis_state + std::array(HalfAxis::Count)> m_half_axis_state{}; + + // Member variables that are no longer used, but kept and serialized for compatibility with older save states + u8 m_command_param = 0; + bool m_legacy_rumble_unlocked = false; + + u8 jog_position = 0x0; + JogconDirection jog_direction = JogconDirection::JOGCON_DIR_NONE; + u8 jog_rotations = 0x0; + JogconCommand jog_last_command = JogconCommand::JOGCON_CMD_NONE; +}; diff --git a/src/core/types.h b/src/core/types.h index 1227afbd6..8491578a6 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -196,6 +196,7 @@ enum class ControllerType : u8 NeGcon, NeGconRumble, Justifier, + JogCon, Count };