diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp index 422fc3622..f8a07adf5 100644 --- a/src/core/analog_controller.cpp +++ b/src/core/analog_controller.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "analog_controller.h" @@ -464,7 +464,8 @@ void AnalogController::Poll() m_tx_buffer[0] = GetIDByte(); m_tx_buffer[1] = m_status_byte; - const u16 button_state = m_button_state & GetExtraButtonMask(); + const u16 button_mask = m_button_state & GetExtraButtonMask(); + const u16 button_state = m_disable_socd ? ControllerHelpers::RemoveOpposingDirections(button_mask) : button_mask; m_tx_buffer[2] = Truncate8(button_state); m_tx_buffer[3] = Truncate8(button_state >> 8); @@ -813,7 +814,7 @@ static constexpr const char* s_shoulder_settings[] = { TRANSLATE_NOOP("AnalogController", "Never"), TRANSLATE_NOOP("AnalogController", "Digital Mode Only"), TRANSLATE_NOOP("AnalogController", "Analog and Digital Modes"), nullptr}; -static const SettingInfo s_settings[] = { +static constexpr SettingInfo s_settings[] = { {SettingInfo::Type::Boolean, "ForceAnalogOnReset", TRANSLATE_NOOP("AnalogController", "Automatically Enable Analog Mode"), TRANSLATE_NOOP("AnalogController", "Forces the controller to analog mode when the game is started/restarted."), @@ -824,6 +825,11 @@ static const SettingInfo s_settings[] = { "AnalogController", "Allows you to use the left analog stick to control the d-pad in digital mode, as well as the buttons."), "true", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, + {SettingInfo::Type::Boolean, "DisableSOCD", + TRANSLATE_NOOP("AnalogController", "Disable Simultaneous Opposing Cardinal Directions"), + TRANSLATE_NOOP("AnalogController", + "Prevents concurrent left/right or up/down inputs from being presented to the game."), + "false", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, {SettingInfo::Type::IntegerList, "AnalogShoulderButtons", TRANSLATE_NOOP("AnalogController", "Use Right Analog for Shoulder Buttons"), TRANSLATE_NOOP( @@ -880,9 +886,9 @@ const Controller::ControllerInfo AnalogController::INFO = {ControllerType::Analo void AnalogController::LoadSettings(const SettingsInterface& si, const char* section, bool initial) { - Controller::LoadSettings(si, section, initial); m_force_analog_on_reset = si.GetBoolValue(section, "ForceAnalogOnReset", true); m_analog_dpad_in_digital_mode = si.GetBoolValue(section, "AnalogDPadInDigitalMode", true); + m_disable_socd = si.GetBoolValue(section, "DisableSOCD", false); m_analog_shoulder_buttons = static_cast(si.GetUIntValue(section, "AnalogShoulderButtons", 0u)); m_analog_trigger_buttons = static_cast(si.GetUIntValue(section, "AnalogTriggerButtons", 0u)); m_analog_deadzone = std::clamp(si.GetFloatValue(section, "AnalogDeadzone", DEFAULT_STICK_DEADZONE), 0.0f, 1.0f); diff --git a/src/core/analog_controller.h b/src/core/analog_controller.h index bca39a579..832539ab3 100644 --- a/src/core/analog_controller.h +++ b/src/core/analog_controller.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -136,8 +136,9 @@ private: u8 m_invert_left_stick = 0; u8 m_invert_right_stick = 0; - bool m_force_analog_on_reset = false; - bool m_analog_dpad_in_digital_mode = false; + bool m_force_analog_on_reset : 1 = false; + bool m_analog_dpad_in_digital_mode : 1 = false; + bool m_disable_socd : 1 = false; u8 m_analog_shoulder_buttons = 0; u8 m_analog_trigger_buttons = 0; diff --git a/src/core/analog_joystick.cpp b/src/core/analog_joystick.cpp index 64de0e1b8..34e017b09 100644 --- a/src/core/analog_joystick.cpp +++ b/src/core/analog_joystick.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "analog_joystick.h" @@ -273,7 +273,8 @@ bool AnalogJoystick::Transfer(const u8 data_in, u8* data_out) case TransferState::ButtonsLSB: { - *data_out = Truncate8(m_button_state); + *data_out = + Truncate8(m_disable_socd ? ControllerHelpers::RemoveOpposingDirections(m_button_state) : m_button_state); m_transfer_state = TransferState::ButtonsMSB; return true; } @@ -381,7 +382,12 @@ static constexpr const char* s_invert_settings[] = { TRANSLATE_NOOP("AnalogJoystick", "Invert Up/Down"), TRANSLATE_NOOP("AnalogJoystick", "Invert Left/Right + Up/Down"), nullptr}; -static const SettingInfo s_settings[] = { +static constexpr SettingInfo s_settings[] = { + {SettingInfo::Type::Boolean, "DisableSOCD", + TRANSLATE_NOOP("AnalogJoystick", "Disable Simultaneous Opposing Cardinal Directions"), + TRANSLATE_NOOP("AnalogJoystick", + "Prevents concurrent left/right or up/down inputs from being presented to the game."), + "false", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, {SettingInfo::Type::Float, "AnalogDeadzone", TRANSLATE_NOOP("AnalogJoystick", "Analog Deadzone"), TRANSLATE_NOOP("AnalogJoystick", "Sets the analog stick deadzone, i.e. the fraction of the stick movement which will be ignored."), @@ -416,4 +422,5 @@ void AnalogJoystick::LoadSettings(const SettingsInterface& si, const char* secti std::clamp(si.GetFloatValue(section, "AnalogSensitivity", DEFAULT_STICK_SENSITIVITY), 0.01f, 3.0f); m_invert_left_stick = static_cast(si.GetIntValue(section, "InvertLeftStick", 0)); m_invert_right_stick = static_cast(si.GetIntValue(section, "InvertRightStick", 0)); + m_disable_socd = si.GetBoolValue(section, "DisableSOCD", false); } diff --git a/src/core/analog_joystick.h b/src/core/analog_joystick.h index 7f21ec16e..dc653a71f 100644 --- a/src/core/analog_joystick.h +++ b/src/core/analog_joystick.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -104,6 +104,7 @@ private: float m_analog_sensitivity = 1.33f; u8 m_invert_left_stick = 0; u8 m_invert_right_stick = 0; + bool m_disable_socd = false; // On original hardware, the mode toggle is a switch rather than a button, so we'll enable Analog Mode by default bool m_analog_mode = true; diff --git a/src/core/controller_helpers.h b/src/core/controller_helpers.h index df08331b4..165106233 100644 --- a/src/core/controller_helpers.h +++ b/src/core/controller_helpers.h @@ -6,6 +6,7 @@ #include "common/types.h" #include +#include namespace ControllerHelpers { @@ -33,4 +34,21 @@ ALWAYS_INLINE float MergeHalfAxesToFloat(u8 neg_value, u8 pos_value, bool invert return (invert ? -result : result); } +/// Removes opposing directions (left/right and up/down), preventing these buttons from being pressed concurrently. +/// Note: Assumes up is bit 4, right is bit 5, down is bit 6, and left is bit 7, which is the case for all PSX pads. +template + requires(std::is_integral_v || std::is_enum_v) +ALWAYS_INLINE T RemoveOpposingDirections(T state) +{ + using BitsType = std::make_unsigned_t< + typename std::conditional_t, std::underlying_type, std::type_identity>::type>; + + const BitsType bits = static_cast(state); + const BitsType conflicts = static_cast(~(bits | (bits >> 2))) & static_cast(0x0030u); + + // Active-low, so setting both conflicting bits releases them. + // Prefer Up (bit 4) over Down (bit 6), and Right (bit 5) over Left (bit 7). + return static_cast(bits | (conflicts << 2)); +} + } // namespace ControllerHelpers \ No newline at end of file diff --git a/src/core/digital_controller.cpp b/src/core/digital_controller.cpp index 80a6a2075..21284aa59 100644 --- a/src/core/digital_controller.cpp +++ b/src/core/digital_controller.cpp @@ -1,7 +1,8 @@ -// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "digital_controller.h" +#include "controller_helpers.h" #include "system.h" #include "util/state_wrapper.h" @@ -11,6 +12,7 @@ #include "common/assert.h" #include "common/bitutils.h" +#include "common/settings_interface.h" DigitalController::DigitalController(u32 index, u16 button_mask) : Controller(index), m_button_mask(button_mask) { @@ -127,21 +129,32 @@ bool DigitalController::Transfer(const u8 data_in, u8* data_out) case TransferState::ButtonsLSB: { - *data_out = Truncate8(m_button_state & m_button_mask); + *data_out = + Truncate8((m_disable_socd ? ControllerHelpers::RemoveOpposingDirections(m_button_state) : m_button_state) & + m_button_mask); m_transfer_state = TransferState::ButtonsMSB; return true; } case TransferState::ButtonsMSB: - *data_out = Truncate8((m_button_state & m_button_mask) >> 8); + { + // Can skip opposing directions here since they're all in the LSB. + const u16 buttons_to_send = m_button_state & m_button_mask; + *data_out = Truncate8(buttons_to_send >> 8); m_transfer_state = TransferState::Idle; return false; + } default: UnreachableCode(); } } +void DigitalController::LoadSettings(const SettingsInterface& si, const char* section, bool initial) +{ + m_disable_socd = si.GetBoolValue(section, "DisableSOCD", false); +} + std::unique_ptr DigitalController::Create(u32 index, ControllerType type) { // popn controller - right/down/left grounded @@ -156,6 +169,14 @@ std::unique_ptr DigitalController::Create(u32 index, Controll return std::make_unique(index, mask); } +static constexpr SettingInfo s_settings[] = { + {SettingInfo::Type::Boolean, "DisableSOCD", + TRANSLATE_NOOP("DigitalController", "Disable Simultaneous Opposing Cardinal Directions"), + TRANSLATE_NOOP("DigitalController", + "Prevents concurrent left/right or up/down inputs from being presented to the game."), + "false", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, +}; + 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} @@ -180,13 +201,15 @@ static const Controller::ControllerBindingInfo s_binding_info[] = { #undef BUTTON }; -const Controller::ControllerInfo DigitalController::INFO = {ControllerType::DigitalController, - "DigitalController", - TRANSLATE_NOOP("ControllerType", "Digital Controller"), - ICON_PF_GAMEPAD_ALT, - "images/controllers/digital_controller.svg", - s_binding_info, - {}}; +const Controller::ControllerInfo DigitalController::INFO = { + ControllerType::DigitalController, + "DigitalController", + TRANSLATE_NOOP("ControllerType", "Digital Controller"), + ICON_PF_GAMEPAD_ALT, + "images/controllers/digital_controller.svg", + s_binding_info, + s_settings, +}; static const Controller::ControllerBindingInfo s_popn_binding_info[] = { #define BUTTON(name, display_name, icon_name, button, genb) \ @@ -209,10 +232,12 @@ static const Controller::ControllerBindingInfo s_popn_binding_info[] = { #undef BUTTON }; -const Controller::ControllerInfo DigitalController::INFO_POPN = {ControllerType::PopnController, - "PopnController", - TRANSLATE_NOOP("ControllerType", "Pop'n Controller"), - ICON_PF_POPN_CONTROLLER, - nullptr, - s_popn_binding_info, - {}}; +const Controller::ControllerInfo DigitalController::INFO_POPN = { + ControllerType::PopnController, + "PopnController", + TRANSLATE_NOOP("ControllerType", "Pop'n Controller"), + ICON_PF_POPN_CONTROLLER, + nullptr, + s_popn_binding_info, + {}, +}; diff --git a/src/core/digital_controller.h b/src/core/digital_controller.h index b1c173b9e..dcca39a77 100644 --- a/src/core/digital_controller.h +++ b/src/core/digital_controller.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -52,6 +52,8 @@ public: 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 TransferState : u8 { @@ -67,4 +69,5 @@ private: u16 m_button_mask = UINT16_C(0xFFFF); TransferState m_transfer_state = TransferState::Idle; + bool m_disable_socd = false; }; diff --git a/src/core/jogcon.cpp b/src/core/jogcon.cpp index 3105a0bfa..b466e4c81 100644 --- a/src/core/jogcon.cpp +++ b/src/core/jogcon.cpp @@ -564,8 +564,6 @@ bool JogCon::Transfer(const u8 data_in, u8* data_out) 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); diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp index 8f1957596..5ff865679 100644 --- a/src/core/negcon.cpp +++ b/src/core/negcon.cpp @@ -1,7 +1,8 @@ -// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "negcon.h" +#include "controller_helpers.h" #include "host.h" #include "system.h" @@ -208,7 +209,8 @@ bool NeGcon::Transfer(const u8 data_in, u8* data_out) case TransferState::ButtonsLSB: { - *data_out = Truncate8(m_button_state); + *data_out = + Truncate8(m_disable_socd ? ControllerHelpers::RemoveOpposingDirections(m_button_state) : m_button_state); m_transfer_state = TransferState::ButtonsMSB; return true; } @@ -292,7 +294,11 @@ static const Controller::ControllerBindingInfo s_binding_info[] = { #undef BUTTON }; -static const SettingInfo s_settings[] = { +static constexpr SettingInfo s_settings[] = { + {SettingInfo::Type::Boolean, "DisableSOCD", + TRANSLATE_NOOP("NeGcon", "Disable Simultaneous Opposing Cardinal Directions"), + TRANSLATE_NOOP("NeGcon", "Prevents concurrent left/right or up/down inputs from being presented to the game."), + "false", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, {SettingInfo::Type::Float, "SteeringDeadzone", TRANSLATE_NOOP("NeGcon", "Steering Axis Deadzone"), TRANSLATE_NOOP("NeGcon", "Sets deadzone for steering axis."), "0", "0", "0.99", "0.01", "%.0f%%", nullptr, 100.0f}, {SettingInfo::Type::Float, "SteeringSaturation", TRANSLATE_NOOP("NeGcon", "Steering Axis Saturation"), @@ -337,7 +343,7 @@ const Controller::ControllerInfo NeGcon::INFO = {ControllerType::NeGcon, void NeGcon::LoadSettings(const SettingsInterface& si, const char* section, bool initial) { - Controller::LoadSettings(si, section, initial); + m_disable_socd = si.GetBoolValue(section, "DisableSOCD", false); m_steering_modifier = { .deadzone = si.GetFloatValue(section, "SteeringDeadzone", DEFAULT_STEERING_MODIFIER.deadzone), .saturation = si.GetFloatValue(section, "SteeringSaturation", DEFAULT_STEERING_MODIFIER.saturation), diff --git a/src/core/negcon.h b/src/core/negcon.h index 286aa12c4..76a6dc403 100644 --- a/src/core/negcon.h +++ b/src/core/negcon.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -125,6 +125,7 @@ private: u16 m_button_state = UINT16_C(0xFFFF); TransferState m_transfer_state = TransferState::Idle; + bool m_disable_socd = false; AxisModifier m_steering_modifier = DEFAULT_STEERING_MODIFIER; std::array m_half_axis_modifiers = { diff --git a/src/core/negcon_rumble.cpp b/src/core/negcon_rumble.cpp index e6dee6517..4ddedfc54 100644 --- a/src/core/negcon_rumble.cpp +++ b/src/core/negcon_rumble.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "negcon_rumble.h" @@ -62,12 +62,9 @@ void NeGconRumble::Reset() m_status_byte = 0x5A; - if (m_force_analog_on_reset) - { - // NOTE: Should be NeGconRumble, but we don't want to break it for games that haven't opted in. - if (CanStartInAnalogMode(ControllerType::AnalogController)) - SetAnalogMode(true, false); - } + // NOTE: Should be NeGconRumble, but we don't want to break it for games that haven't opted in. + if (CanStartInAnalogMode(ControllerType::AnalogController)) + SetAnalogMode(true, false); } bool NeGconRumble::DoState(StateWrapper& sw, bool apply_input_state) @@ -293,11 +290,6 @@ float NeGconRumble::GetMotorStrength(u32 motor) const return (state != 0) ? static_cast(strength / 65535.0) : 0.0f; } -u8 NeGconRumble::GetExtraButtonMaskLSB() const -{ - return 0xFF; -} - void NeGconRumble::ResetRumbleConfig() { m_rumble_config.fill(0xFF); @@ -446,7 +438,8 @@ bool NeGconRumble::Transfer(const u8 data_in, u8* data_out) { case 2: { - m_tx_buffer[m_command_step] = Truncate8(m_button_state) & GetExtraButtonMaskLSB(); + m_tx_buffer[m_command_step] = + Truncate8(m_disable_socd ? ControllerHelpers::RemoveOpposingDirections(m_button_state) : m_button_state); if (m_dualshock_enabled) SetMotorStateForConfigIndex(rumble_index, data_in); @@ -525,7 +518,8 @@ bool NeGconRumble::Transfer(const u8 data_in, u8* data_out) { case 2: { - m_tx_buffer[m_command_step] = Truncate8(m_button_state) & GetExtraButtonMaskLSB(); + m_tx_buffer[m_command_step] = + Truncate8(m_disable_socd ? ControllerHelpers::RemoveOpposingDirections(m_button_state) : m_button_state); } break; @@ -752,7 +746,11 @@ constinit const Controller::ControllerBindingInfo NeGconRumble::s_binding_info[] #undef BUTTON }; -static const SettingInfo s_settings[] = { +static constexpr SettingInfo s_settings[] = { + {SettingInfo::Type::Boolean, "DisableSOCD", + TRANSLATE_NOOP("NeGconRumble", "Disable Simultaneous Opposing Cardinal Directions"), + TRANSLATE_NOOP("NeGconRumble", "Prevents concurrent left/right or up/down inputs from being presented to the game."), + "false", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, {SettingInfo::Type::Float, "SteeringDeadzone", TRANSLATE_NOOP("NeGconRumble", "Steering Axis Deadzone"), TRANSLATE_NOOP("NeGconRumble", "Sets deadzone size for steering axis."), "0", "0", "0.99", "0.01", "%.0f%%", nullptr, 100.0f}, @@ -781,9 +779,9 @@ const Controller::ControllerInfo NeGconRumble::INFO = {ControllerType::NeGconRum void NeGconRumble::LoadSettings(const SettingsInterface& si, const char* section, bool initial) { - Controller::LoadSettings(si, section, initial); m_steering_deadzone = si.GetFloatValue(section, "SteeringDeadzone", 0.10f); m_steering_sensitivity = si.GetFloatValue(section, "SteeringSensitivity", 1.00f); + m_disable_socd = si.GetBoolValue(section, "DisableSOCD", false); m_vibration_bias[0] = static_cast( std::clamp(si.GetIntValue(section, "LargeMotorVibrationBias", DEFAULT_LARGE_MOTOR_VIBRATION_BIAS), -255, 255)); m_vibration_bias[1] = static_cast( diff --git a/src/core/negcon_rumble.h b/src/core/negcon_rumble.h index 19e8f1343..b86902488 100644 --- a/src/core/negcon_rumble.h +++ b/src/core/negcon_rumble.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once @@ -97,7 +97,7 @@ private: static const Controller::ControllerBindingInfo s_binding_info[]; std::array m_vibration_bias{DEFAULT_LARGE_MOTOR_VIBRATION_BIAS, DEFAULT_SMALL_MOTOR_VIBRATION_BIAS}; - bool m_force_analog_on_reset = true; + bool m_disable_socd = false; bool m_analog_mode = false; bool m_analog_locked = false; @@ -146,7 +146,6 @@ private: void ProcessAnalogModeToggle(); void SetMotorState(u32 motor, u8 value); float GetMotorStrength(u32 motor) const; - u8 GetExtraButtonMaskLSB() const; void ResetRumbleConfig(); void SetMotorStateForConfigIndex(int index, u8 value);