diff --git a/src/core-tests/CMakeLists.txt b/src/core-tests/CMakeLists.txt
index 99f02f03c..1cd4406a6 100644
--- a/src/core-tests/CMakeLists.txt
+++ b/src/core-tests/CMakeLists.txt
@@ -5,6 +5,7 @@ add_executable(core-tests
cheats_tests.cpp
cpu_disasm_tests.cpp
lightgun_controller_tests.cpp
+ negcon_base_tests.cpp
spu_tests.cpp
stub_host.cpp
)
diff --git a/src/core-tests/core-tests.vcxproj b/src/core-tests/core-tests.vcxproj
index 3dce54202..0828eeccf 100644
--- a/src/core-tests/core-tests.vcxproj
+++ b/src/core-tests/core-tests.vcxproj
@@ -6,6 +6,7 @@
+
@@ -43,4 +44,4 @@
-
+
\ No newline at end of file
diff --git a/src/core-tests/core-tests.vcxproj.filters b/src/core-tests/core-tests.vcxproj.filters
index fb8c94920..f3c8cf4cb 100644
--- a/src/core-tests/core-tests.vcxproj.filters
+++ b/src/core-tests/core-tests.vcxproj.filters
@@ -6,6 +6,8 @@
+
+
diff --git a/src/core-tests/negcon_base_tests.cpp b/src/core-tests/negcon_base_tests.cpp
new file mode 100644
index 000000000..f42b43732
--- /dev/null
+++ b/src/core-tests/negcon_base_tests.cpp
@@ -0,0 +1,233 @@
+// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors.
+// SPDX-License-Identifier: CC-BY-NC-ND-4.0
+
+#include "core/negcon.h"
+#include "core/negcon_base.h"
+#include "core/negcon_rumble.h"
+#include "core/save_state_version.h"
+
+#include "util/ini_settings_interface.h"
+#include "util/state_wrapper.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+static_assert(std::is_base_of_v);
+static_assert(std::is_base_of_v);
+static_assert(static_cast(NeGcon::Button::Count) == 8);
+static_assert(static_cast(NeGconRumble::Button::Analog) == 8);
+static_assert(static_cast(NeGconRumble::Button::Count) == 9);
+
+static const Controller::ControllerBindingInfo* FindBinding(const Controller::ControllerInfo& info,
+ std::string_view name);
+static bool HasSetting(const Controller::ControllerInfo& info, std::string_view name);
+
+TEST(NegConBase, BindingIndices)
+{
+ const Controller::ControllerBindingInfo* negcon_steering_left = FindBinding(NeGcon::INFO, "SteeringLeft");
+ const Controller::ControllerBindingInfo* rumble_analog = FindBinding(NeGconRumble::INFO, "Analog");
+ const Controller::ControllerBindingInfo* rumble_steering_left = FindBinding(NeGconRumble::INFO, "SteeringLeft");
+ const Controller::ControllerBindingInfo* rumble_large_motor = FindBinding(NeGconRumble::INFO, "LargeMotor");
+ const Controller::ControllerBindingInfo* rumble_mode_led = FindBinding(NeGconRumble::INFO, "ModeLED");
+
+ ASSERT_NE(negcon_steering_left, nullptr);
+ ASSERT_NE(rumble_analog, nullptr);
+ ASSERT_NE(rumble_steering_left, nullptr);
+ ASSERT_NE(rumble_large_motor, nullptr);
+ ASSERT_NE(rumble_mode_led, nullptr);
+ EXPECT_EQ(negcon_steering_left->bind_index, 8u);
+ EXPECT_EQ(rumble_analog->bind_index, 8u);
+ EXPECT_EQ(rumble_steering_left->bind_index, 9u);
+ EXPECT_EQ(rumble_large_motor->bind_index, 14u);
+ EXPECT_EQ(rumble_mode_led->bind_index, 16u);
+}
+
+TEST(NegConBase, CommonButtonMappings)
+{
+ NeGcon negcon(0);
+ NeGconRumble rumble(0);
+
+ negcon.SetBindState(static_cast(NeGcon::Button::Up), 1.0f);
+ negcon.SetBindState(static_cast(NeGcon::Button::A), 1.0f);
+ rumble.SetBindState(static_cast(NeGconRumble::Button::Up), 1.0f);
+ rumble.SetBindState(static_cast(NeGconRumble::Button::A), 1.0f);
+
+ EXPECT_EQ(negcon.GetButtonStateBits(), UINT16_C(0x2010));
+ EXPECT_EQ(rumble.GetButtonStateBits(), UINT16_C(0x2010));
+ EXPECT_EQ(negcon.GetBindState(static_cast(NeGcon::Button::Up)), 1.0f);
+ EXPECT_EQ(rumble.GetBindState(static_cast(NeGconRumble::Button::Up)), 1.0f);
+ EXPECT_EQ(rumble.GetBindState(static_cast(NeGconRumble::Button::Analog)), 0.0f);
+}
+
+TEST(NegConBase, DefaultAxisModifiers)
+{
+ NeGcon negcon(0);
+ NeGconRumble rumble(0);
+ constexpr u32 negcon_axis_start = static_cast(NeGcon::Button::Count);
+ constexpr u32 rumble_axis_start = static_cast(NeGconRumble::Button::Count);
+
+ negcon.SetBindState(negcon_axis_start + static_cast(NeGcon::HalfAxis::SteeringRight), 1.0f);
+ rumble.SetBindState(rumble_axis_start + static_cast(NeGconRumble::HalfAxis::SteeringRight), 1.0f);
+ negcon.SetBindState(negcon_axis_start + static_cast(NeGcon::HalfAxis::I), 0.5f);
+ rumble.SetBindState(rumble_axis_start + static_cast(NeGconRumble::HalfAxis::I), 0.5f);
+
+ ASSERT_TRUE(negcon.GetAnalogInputBytes().has_value());
+ ASSERT_TRUE(rumble.GetAnalogInputBytes().has_value());
+ EXPECT_EQ(negcon.GetAnalogInputBytes(), rumble.GetAnalogInputBytes());
+ EXPECT_EQ(negcon.GetAnalogInputBytes().value(), UINT32_C(0x000080FF));
+ EXPECT_EQ(negcon.GetBindState(negcon_axis_start + static_cast(NeGcon::HalfAxis::SteeringRight)), 1.0f);
+ EXPECT_EQ(rumble.GetBindState(rumble_axis_start + static_cast(NeGconRumble::HalfAxis::SteeringRight)), 1.0f);
+}
+
+TEST(NegConBase, ConfiguredAxisModifiers)
+{
+ INISettingsInterface si;
+ si.SetFloatValue("Controller", "SteeringDeadzone", 0.2f);
+ si.SetFloatValue("Controller", "SteeringSaturation", 0.8f);
+ si.SetFloatValue("Controller", "SteeringScaling", 0.5f);
+ si.SetFloatValue("Controller", "IDeadzone", 0.1f);
+ si.SetFloatValue("Controller", "ISaturation", 0.9f);
+ si.SetFloatValue("Controller", "ILinearity", 0.69314718f);
+ si.SetFloatValue("Controller", "IScaling", 0.5f);
+ si.SetFloatValue("Controller", "IIDeadzone", 0.25f);
+ si.SetFloatValue("Controller", "IISaturation", 0.75f);
+ si.SetFloatValue("Controller", "LDeadzone", 0.0f);
+ si.SetFloatValue("Controller", "LSaturation", 0.5f);
+ si.SetFloatValue("Controller", "LScaling", 0.25f);
+
+ NeGcon negcon(0);
+ NeGconRumble rumble(0);
+ negcon.LoadSettings(si, "Controller", true);
+ rumble.LoadSettings(si, "Controller", true);
+
+ constexpr u32 negcon_axis_start = static_cast(NeGcon::Button::Count);
+ constexpr u32 rumble_axis_start = static_cast(NeGconRumble::Button::Count);
+ negcon.SetBindState(negcon_axis_start + static_cast(NeGcon::HalfAxis::SteeringRight), 0.5f);
+ rumble.SetBindState(rumble_axis_start + static_cast(NeGconRumble::HalfAxis::SteeringRight), 0.5f);
+ negcon.SetBindState(negcon_axis_start + static_cast(NeGcon::HalfAxis::I), 0.5f);
+ rumble.SetBindState(rumble_axis_start + static_cast(NeGconRumble::HalfAxis::I), 0.5f);
+ negcon.SetBindState(negcon_axis_start + static_cast(NeGcon::HalfAxis::II), 0.5f);
+ rumble.SetBindState(rumble_axis_start + static_cast(NeGconRumble::HalfAxis::II), 0.5f);
+ negcon.SetBindState(negcon_axis_start + static_cast(NeGcon::HalfAxis::L), 0.5f);
+ rumble.SetBindState(rumble_axis_start + static_cast(NeGconRumble::HalfAxis::L), 0.5f);
+
+ ASSERT_TRUE(negcon.GetAnalogInputBytes().has_value());
+ ASSERT_TRUE(rumble.GetAnalogInputBytes().has_value());
+ EXPECT_EQ(negcon.GetAnalogInputBytes(), rumble.GetAnalogInputBytes());
+ EXPECT_EQ(negcon.GetAnalogInputBytes().value(), UINT32_C(0x408020A0));
+}
+
+TEST(NegConBase, RumbleSettings)
+{
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "SteeringDeadzone"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "SteeringSaturation"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "SteeringLinearity"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "SteeringScaling"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "IDeadzone"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "ISaturation"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "ILinearity"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "IScaling"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "IIDeadzone"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "IISaturation"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "IILinearity"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "IIScaling"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "LDeadzone"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "LSaturation"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "LLinearity"));
+ EXPECT_TRUE(HasSetting(NeGconRumble::INFO, "LScaling"));
+ EXPECT_FALSE(HasSetting(NeGconRumble::INFO, "SteeringSensitivity"));
+}
+
+TEST(NegConBase, DeviceStateLayouts)
+{
+ std::array state_data = {};
+
+ NeGcon negcon(0);
+ negcon.SetBindState(static_cast(NeGcon::Button::A), 1.0f);
+ StateWrapper negcon_sw(std::span(state_data), StateWrapper::Mode::Write, SAVE_STATE_VERSION);
+ ASSERT_TRUE(negcon.DoState(negcon_sw, true));
+ ASSERT_FALSE(negcon_sw.HasError());
+ EXPECT_EQ(negcon_sw.GetPosition(), 3u);
+
+ u16 negcon_buttons;
+ std::memcpy(&negcon_buttons, state_data.data(), sizeof(negcon_buttons));
+ EXPECT_EQ(negcon_buttons, UINT16_C(0xDFFF));
+
+ state_data.fill(0);
+ NeGconRumble rumble(0);
+ rumble.SetBindState(static_cast(NeGconRumble::Button::A), 1.0f);
+ StateWrapper rumble_sw(std::span(state_data), StateWrapper::Mode::Write, SAVE_STATE_VERSION);
+ ASSERT_TRUE(rumble.DoState(rumble_sw, true));
+ ASSERT_FALSE(rumble_sw.HasError());
+ EXPECT_EQ(rumble_sw.GetPosition(), 24u);
+
+ u16 rumble_buttons;
+ std::memcpy(&rumble_buttons, &state_data[4], sizeof(rumble_buttons));
+ EXPECT_EQ(rumble_buttons, UINT16_C(0xDFFF));
+
+ std::array legacy_state_data = {};
+ NeGconRumble legacy_rumble(0);
+ legacy_rumble.SetBindState(static_cast(NeGconRumble::Button::A), 1.0f);
+ StateWrapper legacy_rumble_sw(std::span(legacy_state_data), StateWrapper::Mode::Read, 43);
+ ASSERT_TRUE(legacy_rumble.DoState(legacy_rumble_sw, true));
+ ASSERT_FALSE(legacy_rumble_sw.HasError());
+ EXPECT_EQ(legacy_rumble_sw.GetPosition(), legacy_state_data.size());
+ EXPECT_EQ(legacy_rumble.GetButtonStateBits(), 0u);
+}
+
+TEST(NegConBase, DeviceProtocols)
+{
+ NeGcon negcon(0);
+ negcon.SetBindState(static_cast(NeGcon::Button::A), 1.0f);
+
+ u8 data_out;
+ EXPECT_TRUE(negcon.Transfer(0x01, &data_out));
+ EXPECT_EQ(data_out, 0xFF);
+ EXPECT_TRUE(negcon.Transfer(0x42, &data_out));
+ EXPECT_EQ(data_out, 0x23);
+ EXPECT_TRUE(negcon.Transfer(0x00, &data_out));
+ EXPECT_EQ(data_out, 0x5A);
+ EXPECT_TRUE(negcon.Transfer(0x00, &data_out));
+ EXPECT_EQ(data_out, 0xFF);
+ EXPECT_TRUE(negcon.Transfer(0x00, &data_out));
+ EXPECT_EQ(data_out, 0xDF);
+
+ NeGconRumble rumble(0);
+ rumble.SetBindState(static_cast(NeGconRumble::Button::A), 1.0f);
+ EXPECT_TRUE(rumble.Transfer(0x01, &data_out));
+ EXPECT_EQ(data_out, 0xFF);
+ EXPECT_TRUE(rumble.Transfer(0x42, &data_out));
+ EXPECT_EQ(data_out, 0x41);
+ EXPECT_TRUE(rumble.Transfer(0x00, &data_out));
+ EXPECT_EQ(data_out, 0x5A);
+ EXPECT_TRUE(rumble.Transfer(0x00, &data_out));
+ EXPECT_EQ(data_out, 0xFF);
+ EXPECT_FALSE(rumble.Transfer(0x00, &data_out));
+ EXPECT_EQ(data_out, 0xDF);
+}
+
+const Controller::ControllerBindingInfo* FindBinding(const Controller::ControllerInfo& info, std::string_view name)
+{
+ for (const Controller::ControllerBindingInfo& binding : info.bindings)
+ {
+ if (name == binding.name)
+ return &binding;
+ }
+
+ return nullptr;
+}
+
+bool HasSetting(const Controller::ControllerInfo& info, std::string_view name)
+{
+ for (const SettingInfo& setting : info.settings)
+ {
+ if (name == setting.name)
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index b7574318f..063ea73dc 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -116,6 +116,8 @@ add_library(core
multitap.h
negcon.cpp
negcon.h
+ negcon_base.cpp
+ negcon_base.h
negcon_rumble.cpp
negcon_rumble.h
pad.cpp
diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj
index 763cc036f..6feb13f29 100644
--- a/src/core/core.vcxproj
+++ b/src/core/core.vcxproj
@@ -67,6 +67,7 @@
+
@@ -164,6 +165,7 @@
+
diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters
index 5c6fa0cba..e63425b9f 100644
--- a/src/core/core.vcxproj.filters
+++ b/src/core/core.vcxproj.filters
@@ -33,6 +33,7 @@
+
@@ -112,6 +113,7 @@
+
diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp
index d845fd3cf..60cc8e405 100644
--- a/src/core/negcon.cpp
+++ b/src/core/negcon.cpp
@@ -3,29 +3,17 @@
#include "negcon.h"
#include "controller_helpers.h"
-#include "host.h"
-#include "system.h"
#include "util/state_wrapper.h"
#include "util/translation.h"
#include "common/assert.h"
#include "common/bitutils.h"
-#include "common/log.h"
-#include "common/settings_interface.h"
#include "IconsPromptFont.h"
-#include
-#include
-
-// Mapping of Button to index of corresponding bit in m_button_state
-static constexpr std::array(NeGcon::Button::Count)> s_button_indices = {3, 4, 5, 6,
- 7, 11, 12, 13};
-NeGcon::NeGcon(u32 index) : Controller(index)
+NeGcon::NeGcon(u32 index) : NegConBase(index, static_cast(Button::Count))
{
- m_axis_state.fill(0x00);
- m_axis_state[static_cast(Axis::Steering)] = 0x80;
}
NeGcon::~NeGcon() = default;
@@ -51,116 +39,6 @@ bool NeGcon::DoState(StateWrapper& sw, bool apply_input_state)
return true;
}
-float NeGcon::GetBindState(u32 index) const
-{
- if (index == (static_cast(Button::Count) + static_cast(HalfAxis::SteeringLeft)) ||
- index == (static_cast(Button::Count) + static_cast(HalfAxis::SteeringRight)))
- {
- float value = m_axis_state[static_cast(Axis::Steering)];
- value = value - 128.0f;
- value /= value < 0.0f ? 128.0f : 127.0f;
- value = std::clamp(value, -1.0f, 1.0f);
- if (index == (static_cast(Button::Count) + static_cast(HalfAxis::SteeringLeft)))
- {
- value *= -1.0f;
- }
- return std::max(0.0f, value);
- }
- else if (index >= static_cast(Button::Count))
- {
- // less one because of the two steering axes
- const u32 sub_index = index - (static_cast(Button::Count) + 1);
- if (sub_index >= m_axis_state.size())
- return 0.0f;
-
- return static_cast(m_axis_state[sub_index]) * (1.0f / 255.0f);
- }
- else
- {
- const u32 bit = s_button_indices[index];
- return static_cast(((m_button_state >> bit) & 1u) ^ 1u);
- }
-}
-
-static float apply_axis_modifier(float value, const NeGcon::AxisModifier& axis_modifier)
-{
- value = (value - axis_modifier.deadzone) / (axis_modifier.saturation - axis_modifier.deadzone);
- value = std::clamp(value, 0.0f, 1.0f);
- value = std::pow(value, std::exp(axis_modifier.linearity));
- return value;
-}
-
-static u8 get_scaled_value(float value, const NeGcon::AxisModifier& axis_modifier)
-{
- value = axis_modifier.scaling * axis_modifier.unit * value + axis_modifier.zero;
- return static_cast(std::clamp(std::round(value), 0.0f, 255.0f));
-}
-
-void NeGcon::SetBindState(u32 index, float value)
-{
- // Steering Axis: -1..1 -> 0..255
- if (index == (static_cast(Button::Count) + static_cast(HalfAxis::SteeringLeft)) ||
- index == (static_cast(Button::Count) + static_cast(HalfAxis::SteeringRight)))
- {
- value = apply_axis_modifier(value, m_steering_modifier);
-
- m_half_axis_state[index - static_cast(Button::Count)] = std::clamp(value, 0.0f, 1.0f);
-
- float merged = m_half_axis_state[1] - m_half_axis_state[0];
- m_axis_state[static_cast(Axis::Steering)] = get_scaled_value(merged, m_steering_modifier);
- }
- else if (index >= static_cast(Button::Count))
- {
- // less one because of the two steering axes
- const u32 sub_index = index - (static_cast(Button::Count) + 1);
- if (sub_index >= m_axis_state.size())
- return;
-
- if (index >= (static_cast(Button::Count) + static_cast(HalfAxis::I)) &&
- index <= (static_cast(Button::Count) + static_cast(HalfAxis::L)))
- {
- const AxisModifier& axis_modifier =
- m_half_axis_modifiers[index - (static_cast(Button::Count) + static_cast(HalfAxis::I))];
- value = apply_axis_modifier(value, axis_modifier);
- m_axis_state[sub_index] = get_scaled_value(value, axis_modifier);
- }
- else
- {
- m_axis_state[sub_index] = static_cast(std::clamp(value * 255.0f, 0.0f, 255.0f));
- }
- }
- else if (index < static_cast(Button::Count))
- {
- const u16 bit = u16(1) << s_button_indices[static_cast(index)];
-
- if (value >= 0.5f)
- {
- if (m_button_state & bit)
- System::SetRunaheadReplayFlag(false);
-
- m_button_state &= ~bit;
- }
- else
- {
- if (!(m_button_state & bit))
- System::SetRunaheadReplayFlag(false);
-
- m_button_state |= bit;
- }
- }
-}
-
-u32 NeGcon::GetButtonStateBits() const
-{
- return m_button_state ^ 0xFFFF;
-}
-
-std::optional NeGcon::GetAnalogInputBytes() const
-{
- return m_axis_state[static_cast(Axis::L)] << 24 | m_axis_state[static_cast(Axis::II)] << 16 |
- m_axis_state[static_cast(Axis::I)] << 8 | m_axis_state[static_cast(Axis::Steering)];
-}
-
void NeGcon::ResetTransferState()
{
m_transfer_state = TransferState::Idle;
@@ -337,40 +215,3 @@ const Controller::ControllerInfo NeGcon::INFO = {ControllerType::NeGcon,
"images/controllers/negcon.svg",
s_binding_info,
s_settings};
-
-void NeGcon::LoadSettings(const SettingsInterface& si, const char* section, bool 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),
- .linearity = si.GetFloatValue(section, "SteeringLinearity", DEFAULT_STEERING_MODIFIER.linearity),
- .scaling = si.GetFloatValue(section, "SteeringScaling", DEFAULT_STEERING_MODIFIER.scaling),
- .zero = DEFAULT_STEERING_MODIFIER.zero,
- .unit = DEFAULT_STEERING_MODIFIER.unit,
- };
- m_half_axis_modifiers[0] = {
- .deadzone = si.GetFloatValue(section, "IDeadzone", DEFAULT_PEDAL_MODIFIER.deadzone),
- .saturation = si.GetFloatValue(section, "ISaturation", DEFAULT_PEDAL_MODIFIER.saturation),
- .linearity = si.GetFloatValue(section, "ILinearity", DEFAULT_PEDAL_MODIFIER.linearity),
- .scaling = si.GetFloatValue(section, "IScaling", DEFAULT_PEDAL_MODIFIER.scaling),
- .zero = DEFAULT_PEDAL_MODIFIER.zero,
- .unit = DEFAULT_PEDAL_MODIFIER.unit,
- };
- m_half_axis_modifiers[1] = {
- .deadzone = si.GetFloatValue(section, "IIDeadzone", DEFAULT_PEDAL_MODIFIER.deadzone),
- .saturation = si.GetFloatValue(section, "IISaturation", DEFAULT_PEDAL_MODIFIER.saturation),
- .linearity = si.GetFloatValue(section, "IILinearity", DEFAULT_PEDAL_MODIFIER.linearity),
- .scaling = si.GetFloatValue(section, "IIScaling", DEFAULT_PEDAL_MODIFIER.scaling),
- .zero = DEFAULT_PEDAL_MODIFIER.zero,
- .unit = DEFAULT_PEDAL_MODIFIER.unit,
- };
- m_half_axis_modifiers[2] = {
- .deadzone = si.GetFloatValue(section, "LDeadzone", DEFAULT_PEDAL_MODIFIER.deadzone),
- .saturation = si.GetFloatValue(section, "LSaturation", DEFAULT_PEDAL_MODIFIER.saturation),
- .linearity = si.GetFloatValue(section, "LLinearity", DEFAULT_PEDAL_MODIFIER.linearity),
- .scaling = si.GetFloatValue(section, "LScaling", DEFAULT_PEDAL_MODIFIER.scaling),
- .zero = DEFAULT_PEDAL_MODIFIER.zero,
- .unit = DEFAULT_PEDAL_MODIFIER.unit,
- };
-}
diff --git a/src/core/negcon.h b/src/core/negcon.h
index 76a6dc403..09478092f 100644
--- a/src/core/negcon.h
+++ b/src/core/negcon.h
@@ -3,82 +3,13 @@
#pragma once
-#include "controller.h"
+#include "negcon_base.h"
-#include
#include
-#include
-class NeGcon final : public Controller
+class NeGcon final : public NegConBase
{
public:
- enum class Axis : u8
- {
- Steering = 0,
- I = 1,
- II = 2,
- L = 3,
- Count
- };
-
- enum class Button : u8
- {
- Start = 0,
- Up = 1,
- Right = 2,
- Down = 3,
- Left = 4,
- R = 5,
- B = 6,
- A = 7,
- Count
- };
-
- enum class HalfAxis : u8
- {
- SteeringLeft,
- SteeringRight,
- I,
- II,
- L,
- Count
- };
-
- struct AxisModifier
- {
- float deadzone;
- float saturation;
- float linearity;
- float scaling;
- float zero;
- float unit;
- };
-
- static constexpr float DEFAULT_DEADZONE = 0.00f;
- static constexpr float DEFAULT_SATURATION = 1.00f;
- static constexpr float DEFAULT_LINEARITY = 0.00f;
- static constexpr float DEFAULT_SCALING = 1.00f;
- static constexpr float DEFAULT_STEERING_ZERO = 128.0f;
- static constexpr float DEFAULT_STEERING_UNIT = 128.0f;
- static constexpr float DEFAULT_PEDAL_ZERO = 0.0f;
- static constexpr float DEFAULT_PEDAL_UNIT = 255.0f;
- static constexpr AxisModifier DEFAULT_STEERING_MODIFIER = {
- .deadzone = DEFAULT_DEADZONE,
- .saturation = DEFAULT_SATURATION,
- .linearity = DEFAULT_LINEARITY,
- .scaling = DEFAULT_SCALING,
- .zero = DEFAULT_STEERING_ZERO,
- .unit = DEFAULT_STEERING_UNIT,
- };
- static constexpr AxisModifier DEFAULT_PEDAL_MODIFIER = {
- .deadzone = DEFAULT_DEADZONE,
- .saturation = DEFAULT_SATURATION,
- .linearity = DEFAULT_LINEARITY,
- .scaling = DEFAULT_SCALING,
- .zero = DEFAULT_PEDAL_ZERO,
- .unit = DEFAULT_PEDAL_UNIT,
- };
-
static const Controller::ControllerInfo INFO;
explicit NeGcon(u32 index);
@@ -91,17 +22,9 @@ public:
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;
-
void ResetTransferState() override;
bool Transfer(const u8 data_in, u8* data_out) override;
- u32 GetButtonStateBits() const override;
- std::optional GetAnalogInputBytes() const override;
-
- void LoadSettings(const SettingsInterface& si, const char* section, bool initial) override;
-
private:
enum class TransferState : u8
{
@@ -116,21 +39,5 @@ private:
AnalogL
};
- std::array(Axis::Count)> m_axis_state{};
-
- // steering, merged to m_axis_state
- std::array m_half_axis_state;
-
- // buttons are active low; bits 0-2, 8-10, 14-15 are not used and are always high
- 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 = {
- DEFAULT_PEDAL_MODIFIER,
- DEFAULT_PEDAL_MODIFIER,
- DEFAULT_PEDAL_MODIFIER,
- };
};
diff --git a/src/core/negcon_base.cpp b/src/core/negcon_base.cpp
new file mode 100644
index 000000000..49c7e90c0
--- /dev/null
+++ b/src/core/negcon_base.cpp
@@ -0,0 +1,177 @@
+// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors.
+// SPDX-License-Identifier: CC-BY-NC-ND-4.0
+
+#include "negcon_base.h"
+#include "system.h"
+
+#include "common/settings_interface.h"
+
+#include
+#include
+#include
+
+// Mapping of Button to index of corresponding bit in m_button_state
+static constexpr std::array(NegConBase::Button::Count)> s_button_indices = {3, 4, 5, 6,
+ 7, 11, 12, 13};
+
+static float ApplyAxisModifier(float value, const NegConBase::AxisModifier& axis_modifier);
+static u8 GetScaledValue(float value, const NegConBase::AxisModifier& axis_modifier);
+
+NegConBase::NegConBase(u32 index, u32 half_axis_bind_start)
+ : Controller(index), m_half_axis_bind_start(half_axis_bind_start)
+{
+ m_axis_state.fill(0x00);
+ m_axis_state[static_cast(Axis::Steering)] = 0x80;
+}
+
+NegConBase::~NegConBase() = default;
+
+float NegConBase::GetBindState(u32 index) const
+{
+ if (index == (m_half_axis_bind_start + static_cast(HalfAxis::SteeringLeft)) ||
+ index == (m_half_axis_bind_start + static_cast(HalfAxis::SteeringRight)))
+ {
+ float value = m_axis_state[static_cast(Axis::Steering)];
+ value = value - 128.0f;
+ value /= value < 0.0f ? 128.0f : 127.0f;
+ value = std::clamp(value, -1.0f, 1.0f);
+ if (index == (m_half_axis_bind_start + static_cast(HalfAxis::SteeringLeft)))
+ value *= -1.0f;
+
+ return std::max(0.0f, value);
+ }
+ else if (index >= m_half_axis_bind_start)
+ {
+ // less one because of the two steering axes
+ const u32 sub_index = index - (m_half_axis_bind_start + 1);
+ if (sub_index >= m_axis_state.size())
+ return 0.0f;
+
+ return static_cast(m_axis_state[sub_index]) * (1.0f / 255.0f);
+ }
+ else if (index < static_cast(Button::Count))
+ {
+ const u32 bit = s_button_indices[index];
+ return static_cast(((m_button_state >> bit) & 1u) ^ 1u);
+ }
+ else
+ {
+ return 0.0f;
+ }
+}
+
+void NegConBase::SetBindState(u32 index, float value)
+{
+ // Steering Axis: -1..1 -> 0..255
+ if (index == (m_half_axis_bind_start + static_cast(HalfAxis::SteeringLeft)) ||
+ index == (m_half_axis_bind_start + static_cast(HalfAxis::SteeringRight)))
+ {
+ value = ApplyAxisModifier(value, m_steering_modifier);
+
+ m_half_axis_state[index - m_half_axis_bind_start] = std::clamp(value, 0.0f, 1.0f);
+
+ const float merged = m_half_axis_state[1] - m_half_axis_state[0];
+ m_axis_state[static_cast(Axis::Steering)] = GetScaledValue(merged, m_steering_modifier);
+ }
+ else if (index >= m_half_axis_bind_start)
+ {
+ // less one because of the two steering axes
+ const u32 sub_index = index - (m_half_axis_bind_start + 1);
+ if (sub_index >= m_axis_state.size())
+ return;
+
+ if (index >= (m_half_axis_bind_start + static_cast(HalfAxis::I)) &&
+ index <= (m_half_axis_bind_start + static_cast(HalfAxis::L)))
+ {
+ const AxisModifier& axis_modifier =
+ m_half_axis_modifiers[index - (m_half_axis_bind_start + static_cast(HalfAxis::I))];
+ value = ApplyAxisModifier(value, axis_modifier);
+ m_axis_state[sub_index] = GetScaledValue(value, axis_modifier);
+ }
+ else
+ {
+ m_axis_state[sub_index] = static_cast(std::clamp(value * 255.0f, 0.0f, 255.0f));
+ }
+ }
+ else if (index < static_cast(Button::Count))
+ {
+ const u16 bit = u16(1) << s_button_indices[static_cast(index)];
+
+ if (value >= 0.5f)
+ {
+ if (m_button_state & bit)
+ System::SetRunaheadReplayFlag(false);
+
+ m_button_state &= ~bit;
+ }
+ else
+ {
+ if (!(m_button_state & bit))
+ System::SetRunaheadReplayFlag(false);
+
+ m_button_state |= bit;
+ }
+ }
+}
+
+u32 NegConBase::GetButtonStateBits() const
+{
+ return m_button_state ^ 0xFFFF;
+}
+
+std::optional NegConBase::GetAnalogInputBytes() const
+{
+ return m_axis_state[static_cast(Axis::L)] << 24 | m_axis_state[static_cast(Axis::II)] << 16 |
+ m_axis_state[static_cast(Axis::I)] << 8 | m_axis_state[static_cast(Axis::Steering)];
+}
+
+void NegConBase::LoadSettings(const SettingsInterface& si, const char* section, bool 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),
+ .linearity = si.GetFloatValue(section, "SteeringLinearity", DEFAULT_STEERING_MODIFIER.linearity),
+ .scaling = si.GetFloatValue(section, "SteeringScaling", DEFAULT_STEERING_MODIFIER.scaling),
+ .zero = DEFAULT_STEERING_MODIFIER.zero,
+ .unit = DEFAULT_STEERING_MODIFIER.unit,
+ };
+ m_half_axis_modifiers[0] = {
+ .deadzone = si.GetFloatValue(section, "IDeadzone", DEFAULT_PEDAL_MODIFIER.deadzone),
+ .saturation = si.GetFloatValue(section, "ISaturation", DEFAULT_PEDAL_MODIFIER.saturation),
+ .linearity = si.GetFloatValue(section, "ILinearity", DEFAULT_PEDAL_MODIFIER.linearity),
+ .scaling = si.GetFloatValue(section, "IScaling", DEFAULT_PEDAL_MODIFIER.scaling),
+ .zero = DEFAULT_PEDAL_MODIFIER.zero,
+ .unit = DEFAULT_PEDAL_MODIFIER.unit,
+ };
+ m_half_axis_modifiers[1] = {
+ .deadzone = si.GetFloatValue(section, "IIDeadzone", DEFAULT_PEDAL_MODIFIER.deadzone),
+ .saturation = si.GetFloatValue(section, "IISaturation", DEFAULT_PEDAL_MODIFIER.saturation),
+ .linearity = si.GetFloatValue(section, "IILinearity", DEFAULT_PEDAL_MODIFIER.linearity),
+ .scaling = si.GetFloatValue(section, "IIScaling", DEFAULT_PEDAL_MODIFIER.scaling),
+ .zero = DEFAULT_PEDAL_MODIFIER.zero,
+ .unit = DEFAULT_PEDAL_MODIFIER.unit,
+ };
+ m_half_axis_modifiers[2] = {
+ .deadzone = si.GetFloatValue(section, "LDeadzone", DEFAULT_PEDAL_MODIFIER.deadzone),
+ .saturation = si.GetFloatValue(section, "LSaturation", DEFAULT_PEDAL_MODIFIER.saturation),
+ .linearity = si.GetFloatValue(section, "LLinearity", DEFAULT_PEDAL_MODIFIER.linearity),
+ .scaling = si.GetFloatValue(section, "LScaling", DEFAULT_PEDAL_MODIFIER.scaling),
+ .zero = DEFAULT_PEDAL_MODIFIER.zero,
+ .unit = DEFAULT_PEDAL_MODIFIER.unit,
+ };
+}
+
+float ApplyAxisModifier(float value, const NegConBase::AxisModifier& axis_modifier)
+{
+ value = (value - axis_modifier.deadzone) / (axis_modifier.saturation - axis_modifier.deadzone);
+ value = std::clamp(value, 0.0f, 1.0f);
+ value = std::pow(value, std::exp(axis_modifier.linearity));
+ return value;
+}
+
+u8 GetScaledValue(float value, const NegConBase::AxisModifier& axis_modifier)
+{
+ value = axis_modifier.scaling * axis_modifier.unit * value + axis_modifier.zero;
+ return static_cast(std::clamp(std::round(value), 0.0f, 255.0f));
+}
diff --git a/src/core/negcon_base.h b/src/core/negcon_base.h
new file mode 100644
index 000000000..66989375b
--- /dev/null
+++ b/src/core/negcon_base.h
@@ -0,0 +1,112 @@
+// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors.
+// SPDX-License-Identifier: CC-BY-NC-ND-4.0
+
+#pragma once
+
+#include "controller.h"
+
+#include
+#include
+
+class NegConBase : public Controller
+{
+public:
+ enum class Axis : u8
+ {
+ Steering = 0,
+ I = 1,
+ II = 2,
+ L = 3,
+ Count
+ };
+
+ enum class Button : u8
+ {
+ Start = 0,
+ Up = 1,
+ Right = 2,
+ Down = 3,
+ Left = 4,
+ R = 5,
+ B = 6,
+ A = 7,
+ Count
+ };
+
+ enum class HalfAxis : u8
+ {
+ SteeringLeft,
+ SteeringRight,
+ I,
+ II,
+ L,
+ Count
+ };
+
+ struct AxisModifier
+ {
+ float deadzone;
+ float saturation;
+ float linearity;
+ float scaling;
+ float zero;
+ float unit;
+ };
+
+ static constexpr float DEFAULT_DEADZONE = 0.00f;
+ static constexpr float DEFAULT_SATURATION = 1.00f;
+ static constexpr float DEFAULT_LINEARITY = 0.00f;
+ static constexpr float DEFAULT_SCALING = 1.00f;
+ static constexpr float DEFAULT_STEERING_ZERO = 128.0f;
+ static constexpr float DEFAULT_STEERING_UNIT = 128.0f;
+ static constexpr float DEFAULT_PEDAL_ZERO = 0.0f;
+ static constexpr float DEFAULT_PEDAL_UNIT = 255.0f;
+ static constexpr AxisModifier DEFAULT_STEERING_MODIFIER = {
+ .deadzone = DEFAULT_DEADZONE,
+ .saturation = DEFAULT_SATURATION,
+ .linearity = DEFAULT_LINEARITY,
+ .scaling = DEFAULT_SCALING,
+ .zero = DEFAULT_STEERING_ZERO,
+ .unit = DEFAULT_STEERING_UNIT,
+ };
+ static constexpr AxisModifier DEFAULT_PEDAL_MODIFIER = {
+ .deadzone = DEFAULT_DEADZONE,
+ .saturation = DEFAULT_SATURATION,
+ .linearity = DEFAULT_LINEARITY,
+ .scaling = DEFAULT_SCALING,
+ .zero = DEFAULT_PEDAL_ZERO,
+ .unit = DEFAULT_PEDAL_UNIT,
+ };
+
+ ~NegConBase() override;
+
+ float GetBindState(u32 index) const override;
+ void SetBindState(u32 index, float value) override;
+
+ u32 GetButtonStateBits() const override;
+ std::optional GetAnalogInputBytes() const override;
+
+ void LoadSettings(const SettingsInterface& si, const char* section, bool initial) override;
+
+protected:
+ NegConBase(u32 index, u32 half_axis_bind_start);
+
+ std::array(Axis::Count)> m_axis_state{};
+
+ // steering, merged to m_axis_state
+ std::array m_half_axis_state{};
+
+ // buttons are active low; bits 0-2, 8-10, 14-15 are not used and are always high
+ u16 m_button_state = UINT16_C(0xFFFF);
+
+ bool m_disable_socd = false;
+
+private:
+ u32 m_half_axis_bind_start;
+ AxisModifier m_steering_modifier = DEFAULT_STEERING_MODIFIER;
+ std::array m_half_axis_modifiers = {
+ DEFAULT_PEDAL_MODIFIER,
+ DEFAULT_PEDAL_MODIFIER,
+ DEFAULT_PEDAL_MODIFIER,
+ };
+};
diff --git a/src/core/negcon_rumble.cpp b/src/core/negcon_rumble.cpp
index 336700cac..8b16eef1a 100644
--- a/src/core/negcon_rumble.cpp
+++ b/src/core/negcon_rumble.cpp
@@ -3,8 +3,7 @@
#include "negcon_rumble.h"
#include "controller_helpers.h"
-#include "settings.h"
-#include "system.h"
+#include "host.h"
#include "util/imgui_manager.h"
#include "util/input_manager.h"
@@ -14,24 +13,19 @@
#include "common/assert.h"
#include "common/bitutils.h"
#include "common/log.h"
-#include "common/string_util.h"
#include "IconsFontAwesome.h"
#include "IconsPromptFont.h"
#include "fmt/format.h"
+#include
#include
LOG_CHANNEL(Controller);
-// Mapping of Button to index of corresponding bit in m_button_state
-static constexpr std::array(NeGconRumble::Button::Count)> s_button_indices = {3, 4, 5, 6,
- 7, 11, 12, 13};
-NeGconRumble::NeGconRumble(u32 index) : Controller(index)
+NeGconRumble::NeGconRumble(u32 index) : NegConBase(index, HALFAXIS_BIND_START_INDEX)
{
m_status_byte = 0x5A;
- m_axis_state.fill(0x00);
- m_axis_state[static_cast(Axis::Steering)] = 0x80;
m_rumble_config.fill(0xFF);
}
@@ -120,23 +114,9 @@ float NeGconRumble::GetBindState(u32 index) const
{
return GetMotorStrength(index - MOTOR_BIND_START_INDEX);
}
- else if (index >= (HALFAXIS_BIND_START_INDEX + static_cast(HalfAxis::I)))
- {
- return static_cast(m_axis_state[index - (HALFAXIS_BIND_START_INDEX + static_cast(HalfAxis::I)) + 1]) *
- (1.0f / 255.0f);
- }
- else if (index >= HALFAXIS_BIND_START_INDEX)
- {
- return static_cast(m_half_axis_state[index - HALFAXIS_BIND_START_INDEX]) * (1.0f / 255.0f);
- }
- else if (index < static_cast(Button::Analog))
- {
- const u16 bit = u16(1) << s_button_indices[static_cast(index)];
- return BoolToFloat((m_button_state & bit) == 0);
- }
else
{
- return 0.0f;
+ return NegConBase::GetBindState(index);
}
}
@@ -155,60 +135,7 @@ void NeGconRumble::SetBindState(u32 index, float value)
return;
}
- // Steering Axis: -1..1 -> 0..255
- else if (index == (static_cast(Button::Count) + static_cast(HalfAxis::SteeringLeft)) ||
- index == (static_cast(Button::Count) + static_cast(HalfAxis::SteeringRight)))
- {
- value *= m_steering_sensitivity;
- if (value < m_steering_deadzone)
- value = 0.0f;
-
- m_half_axis_state[index - static_cast(Button::Count)] =
- static_cast(std::clamp(value * 255.0f, 0.0f, 255.0f));
-
- m_axis_state[static_cast(Axis::Steering)] =
- ControllerHelpers::MergeHalfAxes(m_half_axis_state[static_cast(HalfAxis::SteeringLeft)],
- m_half_axis_state[static_cast(HalfAxis::SteeringRight)], false);
- }
- else if (index >= static_cast(Button::Count))
- {
- // less one because of the two steering axes
- const u32 sub_index = index - (static_cast(Button::Count) + 1);
- if (sub_index >= m_axis_state.size())
- return;
-
- m_axis_state[sub_index] = static_cast(std::clamp(value * 255.0f, 0.0f, 255.0f));
- }
- else if (index < static_cast(Button::Count))
- {
- const u16 bit = u16(1) << s_button_indices[static_cast(index)];
-
- if (value >= 0.5f)
- {
- if (m_button_state & bit)
- System::SetRunaheadReplayFlag(false);
-
- m_button_state &= ~bit;
- }
- else
- {
- if (!(m_button_state & bit))
- System::SetRunaheadReplayFlag(false);
-
- m_button_state |= bit;
- }
- }
-}
-
-u32 NeGconRumble::GetButtonStateBits() const
-{
- return m_button_state ^ 0xFFFF;
-}
-
-std::optional NeGconRumble::GetAnalogInputBytes() const
-{
- return m_axis_state[static_cast(Axis::L)] << 24 | m_axis_state[static_cast(Axis::II)] << 16 |
- m_axis_state[static_cast(Axis::I)] << 8 | m_axis_state[static_cast(Axis::Steering)];
+ NegConBase::SetBindState(index, value);
}
void NeGconRumble::ResetTransferState()
@@ -717,25 +644,25 @@ constinit const Controller::ControllerBindingInfo NeGconRumble::s_binding_info[]
{name, display_name, icon_name, LED_BIND_START_INDEX + index, InputBindingInfo::Type::LED, genb}
// clang-format off
- BUTTON("Up", TRANSLATE_NOOP("NeGconRumble", "D-Pad Up"), ICON_PF_DPAD_UP, NeGconRumble::Button::Up, GenericInputBinding::DPadUp),
- BUTTON("Right", TRANSLATE_NOOP("NeGconRumble", "D-Pad Right"), ICON_PF_DPAD_RIGHT, NeGconRumble::Button::Right, GenericInputBinding::DPadRight),
- BUTTON("Down", TRANSLATE_NOOP("NeGconRumble", "D-Pad Down"), ICON_PF_DPAD_DOWN, NeGconRumble::Button::Down, GenericInputBinding::DPadDown),
- BUTTON("Left", TRANSLATE_NOOP("NeGconRumble", "D-Pad Left"), ICON_PF_DPAD_LEFT, NeGconRumble::Button::Left, GenericInputBinding::DPadLeft),
- BUTTON("Start", TRANSLATE_NOOP("NeGconRumble", "Start"),ICON_PF_START, NeGconRumble::Button::Start, GenericInputBinding::Start),
- BUTTON("A", TRANSLATE_NOOP("NeGconRumble", "A Button"), ICON_PF_BUTTON_A, NeGconRumble::Button::A, GenericInputBinding::Circle),
- BUTTON("B", TRANSLATE_NOOP("NeGconRumble", "B Button"), ICON_PF_BUTTON_B, NeGconRumble::Button::B, GenericInputBinding::Triangle),
- AXIS("I", TRANSLATE_NOOP("NeGconRumble", "I Button"), ICON_PF_RIGHT_TRIGGER_R2, NeGconRumble::HalfAxis::I, GenericInputBinding::R2),
- AXIS("II", TRANSLATE_NOOP("NeGconRumble", "II Button"), ICON_PF_LEFT_TRIGGER_L2, NeGconRumble::HalfAxis::II, GenericInputBinding::L2),
- AXIS("L", TRANSLATE_NOOP("NeGconRumble", "Left Trigger"), ICON_PF_LEFT_ANALOG_LEFT, NeGconRumble::HalfAxis::L, GenericInputBinding::L1),
- BUTTON("R", TRANSLATE_NOOP("NeGconRumble", "Right Trigger"), ICON_PF_RIGHT_SHOULDER_R1, NeGconRumble::Button::R, GenericInputBinding::R1),
- AXIS("SteeringLeft", TRANSLATE_NOOP("NeGconRumble", "Steering (Twist) Left"), ICON_PF_LEFT_ANALOG_LEFT, NeGconRumble::HalfAxis::SteeringLeft, GenericInputBinding::LeftStickLeft),
- AXIS("SteeringRight", TRANSLATE_NOOP("NeGconRumble", "Steering (Twist) Right"), ICON_PF_LEFT_ANALOG_RIGHT, NeGconRumble::HalfAxis::SteeringRight, GenericInputBinding::LeftStickRight),
- BUTTON("Analog", TRANSLATE_NOOP("NeGconRumble", "Analog Toggle"), ICON_PF_ANALOG_LEFT_RIGHT, NeGconRumble::Button::Analog, GenericInputBinding::System),
+ BUTTON("Up", TRANSLATE_NOOP("NeGcon", "D-Pad Up"), ICON_PF_DPAD_UP, NeGconRumble::Button::Up, GenericInputBinding::DPadUp),
+ BUTTON("Right", TRANSLATE_NOOP("NeGcon", "D-Pad Right"), ICON_PF_DPAD_RIGHT, NeGconRumble::Button::Right, GenericInputBinding::DPadRight),
+ BUTTON("Down", TRANSLATE_NOOP("NeGcon", "D-Pad Down"), ICON_PF_DPAD_DOWN, NeGconRumble::Button::Down, GenericInputBinding::DPadDown),
+ BUTTON("Left", TRANSLATE_NOOP("NeGcon", "D-Pad Left"), ICON_PF_DPAD_LEFT, NeGconRumble::Button::Left, GenericInputBinding::DPadLeft),
+ BUTTON("Start", TRANSLATE_NOOP("NeGcon", "Start"),ICON_PF_START, NeGconRumble::Button::Start, GenericInputBinding::Start),
+ BUTTON("A", TRANSLATE_NOOP("NeGcon", "A Button"), ICON_PF_BUTTON_A, NeGconRumble::Button::A, GenericInputBinding::Circle),
+ BUTTON("B", TRANSLATE_NOOP("NeGcon", "B Button"), ICON_PF_BUTTON_B, NeGconRumble::Button::B, GenericInputBinding::Triangle),
+ AXIS("I", TRANSLATE_NOOP("NeGcon", "I Button"), ICON_PF_BUTTON_ALT_1, NeGconRumble::HalfAxis::I, GenericInputBinding::R2),
+ AXIS("II", TRANSLATE_NOOP("NeGcon", "II Button"), ICON_PF_BUTTON_ALT_2, NeGconRumble::HalfAxis::II, GenericInputBinding::L2),
+ AXIS("L", TRANSLATE_NOOP("NeGcon", "Left Trigger"), ICON_PF_LEFT_TRIGGER_LT, NeGconRumble::HalfAxis::L, GenericInputBinding::L1),
+ BUTTON("R", TRANSLATE_NOOP("NeGcon", "Right Trigger"), ICON_PF_RIGHT_TRIGGER_RT, NeGconRumble::Button::R, GenericInputBinding::R1),
+ AXIS("SteeringLeft", TRANSLATE_NOOP("NeGcon", "Steering (Twist) Left"), ICON_PF_ANALOG_LEFT, NeGconRumble::HalfAxis::SteeringLeft, GenericInputBinding::LeftStickLeft),
+ AXIS("SteeringRight", TRANSLATE_NOOP("NeGcon", "Steering (Twist) Right"), ICON_PF_ANALOG_RIGHT, NeGconRumble::HalfAxis::SteeringRight, GenericInputBinding::LeftStickRight),
+ BUTTON("Analog", TRANSLATE_NOOP("NeGcon", "Analog Toggle"), ICON_PF_ANALOG_LEFT_RIGHT, NeGconRumble::Button::Analog, GenericInputBinding::System),
- MOTOR("LargeMotor", TRANSLATE_NOOP("AnalogController", "Large Motor"), ICON_PF_VIBRATION_L, LargeMotor, GenericInputBinding::LargeMotor),
- MOTOR("SmallMotor", TRANSLATE_NOOP("AnalogController", "Small Motor"), ICON_PF_VIBRATION, SmallMotor, GenericInputBinding::SmallMotor),
+ MOTOR("LargeMotor", TRANSLATE_NOOP("NeGcon", "Large Motor"), ICON_PF_VIBRATION_L, LargeMotor, GenericInputBinding::LargeMotor),
+ MOTOR("SmallMotor", TRANSLATE_NOOP("NeGcon", "Small Motor"), ICON_PF_VIBRATION, SmallMotor, GenericInputBinding::SmallMotor),
- MODE_LED("ModeLED", TRANSLATE_NOOP("AnalogController", "Mode LED"), ICON_PF_LED, 0, GenericInputBinding::ModeLED),
+ MODE_LED("ModeLED", TRANSLATE_NOOP("NeGcon", "Mode LED"), ICON_PF_LED, 0, GenericInputBinding::ModeLED),
// clang-format on
#undef MOTOR
@@ -745,22 +672,48 @@ constinit const Controller::ControllerBindingInfo NeGconRumble::s_binding_info[]
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."),
+ 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("NeGconRumble", "Steering Axis Deadzone"),
- TRANSLATE_NOOP("NeGconRumble", "Sets deadzone size for steering axis."), "0", "0", "0.99", "0.01", "%.0f%%", nullptr,
- 100.0f},
- {SettingInfo::Type::Float, "SteeringSensitivity", TRANSLATE_NOOP("NeGconRumble", "Steering Axis Sensitivity"),
- TRANSLATE_NOOP("NeGconRumble", "Sets the steering axis scaling factor."), "1", "0.01", "2", "0.01", "%.0f%%",
- nullptr, 100.0f},
- {SettingInfo::Type::Integer, "LargeMotorVibrationBias", TRANSLATE_NOOP("NeGconRumble", "Large Motor Vibration Bias"),
- TRANSLATE_NOOP("NeGconRumble",
+ {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"),
+ TRANSLATE_NOOP("NeGcon", "Sets saturation for steering axis."), "1", "0.01", "1", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "SteeringLinearity", TRANSLATE_NOOP("NeGcon", "Steering Axis Linearity"),
+ TRANSLATE_NOOP("NeGcon", "Sets linearity for steering axis."), "0", "-2", "2", "0.05", "%.2f", nullptr, 1.0f},
+ {SettingInfo::Type::Float, "SteeringScaling", TRANSLATE_NOOP("NeGcon", "Steering Scaling"),
+ TRANSLATE_NOOP("NeGcon", "Sets scaling for steering axis."), "1", "0.01", "10", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "IDeadzone", TRANSLATE_NOOP("NeGcon", "I Button Deadzone"),
+ TRANSLATE_NOOP("NeGcon", "Sets deadzone for button I."), "0", "0", "0.99", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "ISaturation", TRANSLATE_NOOP("NeGcon", "I Button Saturation"),
+ TRANSLATE_NOOP("NeGcon", "Sets saturation for button I."), "1", "0.01", "1", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "ILinearity", TRANSLATE_NOOP("NeGcon", "I Button Linearity"),
+ TRANSLATE_NOOP("NeGcon", "Sets linearity for button I."), "0", "-2", "2", "0.01", "%.2f", nullptr, 1.0f},
+ {SettingInfo::Type::Float, "IScaling", TRANSLATE_NOOP("NeGcon", "I Scaling"),
+ TRANSLATE_NOOP("NeGcon", "Sets scaling for button I."), "1", "0.01", "10", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "IIDeadzone", TRANSLATE_NOOP("NeGcon", "II Button Deadzone"),
+ TRANSLATE_NOOP("NeGcon", "Sets deadzone for button II."), "0", "0", "0.99", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "IISaturation", TRANSLATE_NOOP("NeGcon", "II Button Saturation"),
+ TRANSLATE_NOOP("NeGcon", "Sets saturation for button II."), "1", "0.01", "1", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "IILinearity", TRANSLATE_NOOP("NeGcon", "II Button Linearity"),
+ TRANSLATE_NOOP("NeGcon", "Sets linearity for button II."), "0", "-2", "2", "0.01", "%.2f", nullptr, 1.0f},
+ {SettingInfo::Type::Float, "IIScaling", TRANSLATE_NOOP("NeGcon", "II Scaling"),
+ TRANSLATE_NOOP("NeGcon", "Sets scaling for button II."), "1", "0.01", "10", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "LDeadzone", TRANSLATE_NOOP("NeGcon", "Left Trigger Deadzone"),
+ TRANSLATE_NOOP("NeGcon", "Sets deadzone for left trigger."), "0", "0", "0.99", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "LSaturation", TRANSLATE_NOOP("NeGcon", "Left Trigger Saturation"),
+ TRANSLATE_NOOP("NeGcon", "Sets saturation for left trigger."), "1", "0.01", "1", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Float, "LLinearity", TRANSLATE_NOOP("NeGcon", "Left Trigger Linearity"),
+ TRANSLATE_NOOP("NeGcon", "Sets linearity for left trigger."), "0", "-2", "2", "0.01", "%.2f", nullptr, 1.0f},
+ {SettingInfo::Type::Float, "LScaling", TRANSLATE_NOOP("NeGcon", "Left Trigger Scaling"),
+ TRANSLATE_NOOP("NeGcon", "Sets scaling for left trigger."), "1", "0.01", "10", "0.01", "%.0f%%", nullptr, 100.0f},
+ {SettingInfo::Type::Integer, "LargeMotorVibrationBias", TRANSLATE_NOOP("NeGcon", "Large Motor Vibration Bias"),
+ TRANSLATE_NOOP("NeGcon",
"Sets the bias value for the large vibration motor. If vibration in some games is too weak or not "
"functioning, try increasing this value. Negative values will decrease the intensity of vibration."),
"8", "-255", "255", "1", "%d", nullptr, 1.0f},
- {SettingInfo::Type::Integer, "SmallMotorVibrationBias", TRANSLATE_NOOP("NeGconRumble", "Small Motor Vibration Bias"),
- TRANSLATE_NOOP("NeGconRumble",
+ {SettingInfo::Type::Integer, "SmallMotorVibrationBias", TRANSLATE_NOOP("NeGcon", "Small Motor Vibration Bias"),
+ TRANSLATE_NOOP("NeGcon",
"Sets the bias value for the small vibration motor. If vibration in some games is too weak or not "
"functioning, try increasing this value. Negative values will decrease the intensity of vibration."),
"8", "-255", "255", "1", "%d", nullptr, 1.0f},
@@ -776,11 +729,9 @@ const Controller::ControllerInfo NeGconRumble::INFO = {ControllerType::NeGconRum
void NeGconRumble::LoadSettings(const SettingsInterface& si, const char* section, bool 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);
+ NegConBase::LoadSettings(si, section, initial);
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(
std::clamp(si.GetIntValue(section, "SmallMotorVibrationBias", DEFAULT_SMALL_MOTOR_VIBRATION_BIAS), -255, 255));
-}
\ No newline at end of file
+}
diff --git a/src/core/negcon_rumble.h b/src/core/negcon_rumble.h
index b86902488..db72b8a5f 100644
--- a/src/core/negcon_rumble.h
+++ b/src/core/negcon_rumble.h
@@ -3,24 +3,14 @@
#pragma once
-#include "controller.h"
+#include "negcon_base.h"
#include
#include
-#include
-class NeGconRumble final : public Controller
+class NeGconRumble final : public NegConBase
{
public:
- enum class Axis : u8
- {
- Steering = 0,
- I = 1,
- II = 2,
- L = 3,
- Count
- };
-
enum class Button : u8
{
Start = 0,
@@ -35,16 +25,6 @@ public:
Count
};
- enum class HalfAxis : u8
- {
- SteeringLeft,
- SteeringRight,
- I,
- II,
- L,
- Count
- };
-
static constexpr u8 NUM_MOTORS = 2;
static const Controller::ControllerInfo INFO;
@@ -65,9 +45,6 @@ public:
void ResetTransferState() override;
bool Transfer(const u8 data_in, u8* data_out) override;
- u32 GetButtonStateBits() const override;
- std::optional GetAnalogInputBytes() const override;
-
void LoadSettings(const SettingsInterface& si, const char* section, bool initial) override;
private:
@@ -97,27 +74,17 @@ 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_disable_socd = false;
-
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
};
- // steering, merged to m_axis_state
- std::array m_half_axis_state{};
-
- // buttons are active low; bits 0-2, 8-10, 14-15 are not used and are always high
- u16 m_button_state = UINT16_C(0xFFFF);
-
MotorState m_motor_state{};
Command m_command = Command::Idle;
@@ -148,7 +115,4 @@ private:
float GetMotorStrength(u32 motor) const;
void ResetRumbleConfig();
void SetMotorStateForConfigIndex(int index, u8 value);
-
- float m_steering_deadzone = 0.00f;
- float m_steering_sensitivity = 1.00f;
};