diff --git a/pcsx2-qt/Settings/ControllerBindingWidget.cpp b/pcsx2-qt/Settings/ControllerBindingWidget.cpp
index e2fc7d2f66..0f4cb17024 100644
--- a/pcsx2-qt/Settings/ControllerBindingWidget.cpp
+++ b/pcsx2-qt/Settings/ControllerBindingWidget.cpp
@@ -112,6 +112,14 @@ void ControllerBindingWidget::onTypeChanged()
{
m_bindings_widget = ControllerBindingWidget_Guitar::createInstance(this);
}
+ else if (cinfo->type == Pad::ControllerType::Jogcon)
+ {
+ m_bindings_widget = ControllerBindingWidget_Jogcon::createInstance(this);
+ }
+ else if (cinfo->type == Pad::ControllerType::Negcon)
+ {
+ m_bindings_widget = ControllerBindingWidget_Negcon::createInstance(this);
+ }
else if (cinfo->type == Pad::ControllerType::Popn)
{
m_bindings_widget = ControllerBindingWidget_Popn::createInstance(this);
@@ -907,6 +915,48 @@ ControllerBindingWidget_Base* ControllerBindingWidget_Guitar::createInstance(Con
return new ControllerBindingWidget_Guitar(parent);
}
+ControllerBindingWidget_Jogcon::ControllerBindingWidget_Jogcon(ControllerBindingWidget* parent)
+ : ControllerBindingWidget_Base(parent)
+{
+ m_ui.setupUi(this);
+ initBindingWidgets();
+}
+
+ControllerBindingWidget_Jogcon::~ControllerBindingWidget_Jogcon()
+{
+}
+
+QIcon ControllerBindingWidget_Jogcon::getIcon() const
+{
+ return QIcon::fromTheme("controller-line");
+}
+
+ControllerBindingWidget_Base* ControllerBindingWidget_Jogcon::createInstance(ControllerBindingWidget* parent)
+{
+ return new ControllerBindingWidget_Jogcon(parent);
+}
+
+ControllerBindingWidget_Negcon::ControllerBindingWidget_Negcon(ControllerBindingWidget* parent)
+ : ControllerBindingWidget_Base(parent)
+{
+ m_ui.setupUi(this);
+ initBindingWidgets();
+}
+
+ControllerBindingWidget_Negcon::~ControllerBindingWidget_Negcon()
+{
+}
+
+QIcon ControllerBindingWidget_Negcon::getIcon() const
+{
+ return QIcon::fromTheme("controller-line");
+}
+
+ControllerBindingWidget_Base* ControllerBindingWidget_Negcon::createInstance(ControllerBindingWidget* parent)
+{
+ return new ControllerBindingWidget_Negcon(parent);
+}
+
ControllerBindingWidget_Popn::ControllerBindingWidget_Popn(ControllerBindingWidget* parent)
: ControllerBindingWidget_Base(parent)
{
diff --git a/pcsx2-qt/Settings/ControllerBindingWidget.h b/pcsx2-qt/Settings/ControllerBindingWidget.h
index 17d62eb6db..747791639a 100644
--- a/pcsx2-qt/Settings/ControllerBindingWidget.h
+++ b/pcsx2-qt/Settings/ControllerBindingWidget.h
@@ -12,6 +12,8 @@
#include "ui_ControllerBindingWidget.h"
#include "ui_ControllerBindingWidget_DualShock2.h"
#include "ui_ControllerBindingWidget_Guitar.h"
+#include "ui_ControllerBindingWidget_Jogcon.h"
+#include "ui_ControllerBindingWidget_Negcon.h"
#include "ui_ControllerBindingWidget_Popn.h"
#include "ui_ControllerMacroWidget.h"
#include "ui_ControllerMacroEditWidget.h"
@@ -200,6 +202,38 @@ private:
Ui::ControllerBindingWidget_Guitar m_ui;
};
+class ControllerBindingWidget_Jogcon final : public ControllerBindingWidget_Base
+{
+ Q_OBJECT
+
+public:
+ ControllerBindingWidget_Jogcon(ControllerBindingWidget* parent);
+ ~ControllerBindingWidget_Jogcon();
+
+ QIcon getIcon() const override;
+
+ static ControllerBindingWidget_Base* createInstance(ControllerBindingWidget* parent);
+
+private:
+ Ui::ControllerBindingWidget_Jogcon m_ui;
+};
+
+class ControllerBindingWidget_Negcon final : public ControllerBindingWidget_Base
+{
+ Q_OBJECT
+
+public:
+ ControllerBindingWidget_Negcon(ControllerBindingWidget* parent);
+ ~ControllerBindingWidget_Negcon();
+
+ QIcon getIcon() const override;
+
+ static ControllerBindingWidget_Base* createInstance(ControllerBindingWidget* parent);
+
+private:
+ Ui::ControllerBindingWidget_Negcon m_ui;
+};
+
class ControllerBindingWidget_Popn final : public ControllerBindingWidget_Base
{
Q_OBJECT
diff --git a/pcsx2-qt/Settings/ControllerBindingWidget_Jogcon.ui b/pcsx2-qt/Settings/ControllerBindingWidget_Jogcon.ui
new file mode 100644
index 0000000000..f00ad80360
--- /dev/null
+++ b/pcsx2-qt/Settings/ControllerBindingWidget_Jogcon.ui
@@ -0,0 +1,832 @@
+
+
+ ControllerBindingWidget_Jogcon
+
+
+
+ 0
+ 0
+ 1232
+ 644
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 1100
+ 500
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
-
+
+
+ D-Pad
+
+
+
-
+
+
+ Down
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Left
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Up
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Right
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Large Motor
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+ -
+
+
-
+
+
+ L2
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ R2
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ L1
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ R1
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Start
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Select
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Face Buttons
+
+
+
-
+
+
+ Cross
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Square
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Triangle
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Circle
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Small Motor
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 400
+ 266
+
+
+
+
+
+
+ :/images/Jogcon.svg
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Dial Left
+
+
+
-
+
+
+
+ 150
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Dial Right
+
+
+
-
+
+
+
+ 150
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+ InputBindingWidget
+ QPushButton
+ Settings/InputBindingWidget.h
+
+
+ InputVibrationBindingWidget
+ QPushButton
+ Settings/InputBindingWidget.h
+
+
+
+
+
+
+
diff --git a/pcsx2-qt/Settings/ControllerBindingWidget_Negcon.ui b/pcsx2-qt/Settings/ControllerBindingWidget_Negcon.ui
new file mode 100644
index 0000000000..35e6bf526f
--- /dev/null
+++ b/pcsx2-qt/Settings/ControllerBindingWidget_Negcon.ui
@@ -0,0 +1,730 @@
+
+
+ ControllerBindingWidget_Negcon
+
+
+
+ 0
+ 0
+ 1232
+ 644
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 1100
+ 500
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
-
+
+
+ D-Pad
+
+
+
-
+
+
+ Down
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Left
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Up
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Right
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Large Motor
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+ -
+
+
-
+
+
+ L
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Start
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ R
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Face Buttons
+
+
+
-
+
+
+ I
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ II
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ B
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ A
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Small Motor
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 400
+ 266
+
+
+
+
+
+
+ :/images/Negcon.svg
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Twist Left
+
+
+
-
+
+
+
+ 150
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Twist Right
+
+
+
-
+
+
+
+ 150
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+ InputBindingWidget
+ QPushButton
+ Settings/InputBindingWidget.h
+
+
+ InputVibrationBindingWidget
+ QPushButton
+ Settings/InputBindingWidget.h
+
+
+
+
+
+
+
diff --git a/pcsx2-qt/resources/resources.qrc b/pcsx2-qt/resources/resources.qrc
index 36557a577f..5bf25c58c6 100644
--- a/pcsx2-qt/resources/resources.qrc
+++ b/pcsx2-qt/resources/resources.qrc
@@ -200,6 +200,8 @@
images/DualShock_2.svg
images/GT_Force.svg
images/Guitar.svg
+ images/Jogcon.svg
+ images/Negcon.svg
images/Popn.svg
diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt
index 6177f31ed9..03d0197a29 100644
--- a/pcsx2/CMakeLists.txt
+++ b/pcsx2/CMakeLists.txt
@@ -436,6 +436,8 @@ set(pcsx2PADSources
SIO/Pad/PadBase.cpp
SIO/Pad/PadDualshock2.cpp
SIO/Pad/PadGuitar.cpp
+ SIO/Pad/PadJogcon.cpp
+ SIO/Pad/PadNegcon.cpp
SIO/Pad/PadPopn.cpp
SIO/Pad/PadNotConnected.cpp
)
@@ -444,6 +446,8 @@ set(pcsx2PADHeaders
SIO/Pad/PadBase.h
SIO/Pad/PadDualshock2.h
SIO/Pad/PadGuitar.h
+ SIO/Pad/PadJogcon.h
+ SIO/Pad/PadNegcon.h
SIO/Pad/PadPopn.h
SIO/Pad/PadNotConnected.h
SIO/Pad/PadTypes.h
diff --git a/pcsx2/SIO/Pad/Pad.cpp b/pcsx2/SIO/Pad/Pad.cpp
index bdac510cf3..5c1e64029b 100644
--- a/pcsx2/SIO/Pad/Pad.cpp
+++ b/pcsx2/SIO/Pad/Pad.cpp
@@ -6,6 +6,8 @@
#include "SIO/Pad/Pad.h"
#include "SIO/Pad/PadDualshock2.h"
#include "SIO/Pad/PadGuitar.h"
+#include "SIO/Pad/PadJogcon.h"
+#include "SIO/Pad/PadNegcon.h"
#include "SIO/Pad/PadPopn.h"
#include "SIO/Pad/PadNotConnected.h"
#include "SIO/Sio.h"
@@ -255,6 +257,8 @@ static const Pad::ControllerInfo* s_controller_info[] = {
&PadNotConnected::ControllerInfo,
&PadDualshock2::ControllerInfo,
&PadGuitar::ControllerInfo,
+ &PadJogcon::ControllerInfo,
+ &PadNegcon::ControllerInfo,
&PadPopn::ControllerInfo,
};
@@ -495,6 +499,12 @@ PadBase* Pad::CreatePad(u8 unifiedSlot, ControllerType controllerType, size_t ej
case ControllerType::Guitar:
s_controllers[unifiedSlot] = std::make_unique(unifiedSlot, ejectTicks);
break;
+ case ControllerType::Jogcon:
+ s_controllers[unifiedSlot] = std::make_unique(unifiedSlot, ejectTicks);
+ break;
+ case ControllerType::Negcon:
+ s_controllers[unifiedSlot] = std::make_unique(unifiedSlot, ejectTicks);
+ break;
case ControllerType::Popn:
s_controllers[unifiedSlot] = std::make_unique(unifiedSlot, ejectTicks);
break;
diff --git a/pcsx2/SIO/Pad/PadJogcon.cpp b/pcsx2/SIO/Pad/PadJogcon.cpp
new file mode 100644
index 0000000000..320a2c51d7
--- /dev/null
+++ b/pcsx2/SIO/Pad/PadJogcon.cpp
@@ -0,0 +1,531 @@
+// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#include "SIO/Pad/PadJogcon.h"
+#include "SIO/Pad/Pad.h"
+#include "SIO/Sio.h"
+
+#include "Common.h"
+#include "Input/InputManager.h"
+#include "Host.h"
+
+#include "IconsPromptFont.h"
+
+static const InputBindingInfo s_bindings[] = {
+ // clang-format off
+ {"Up", TRANSLATE_NOOP("Pad", "D-Pad Up"), ICON_PF_DPAD_UP, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_UP, GenericInputBinding::DPadUp},
+ {"Right", TRANSLATE_NOOP("Pad", "D-Pad Right"), ICON_PF_DPAD_RIGHT, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_RIGHT, GenericInputBinding::DPadRight},
+ {"Down", TRANSLATE_NOOP("Pad", "D-Pad Down"), ICON_PF_DPAD_DOWN, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_DOWN, GenericInputBinding::DPadDown},
+ {"Left", TRANSLATE_NOOP("Pad", "D-Pad Left"), ICON_PF_DPAD_LEFT, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_LEFT, GenericInputBinding::DPadLeft},
+ {"Triangle", TRANSLATE_NOOP("Pad", "Triangle"), ICON_PF_BUTTON_TRIANGLE, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_TRIANGLE, GenericInputBinding::Triangle},
+ {"Circle", TRANSLATE_NOOP("Pad", "Circle"), ICON_PF_BUTTON_CIRCLE, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_CIRCLE, GenericInputBinding::Circle},
+ {"Cross", TRANSLATE_NOOP("Pad", "Cross"), ICON_PF_BUTTON_CROSS, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_CROSS, GenericInputBinding::Cross},
+ {"Square", TRANSLATE_NOOP("Pad", "Square"), ICON_PF_BUTTON_SQUARE, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_SQUARE, GenericInputBinding::Square},
+ {"Select", TRANSLATE_NOOP("Pad", "Select"), ICON_PF_SELECT_SHARE, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_SELECT, GenericInputBinding::Select},
+ {"Start", TRANSLATE_NOOP("Pad", "Start"), ICON_PF_START, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_START, GenericInputBinding::Start},
+ {"L1", TRANSLATE_NOOP("Pad", "L1 (Left Bumper)"), ICON_PF_LEFT_SHOULDER_L1, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_L1, GenericInputBinding::L1},
+ {"L2", TRANSLATE_NOOP("Pad", "L2 (Left Trigger)"), ICON_PF_LEFT_TRIGGER_L2, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_L2, GenericInputBinding::L2},
+ {"R1", TRANSLATE_NOOP("Pad", "R1 (Right Bumper)"), ICON_PF_RIGHT_SHOULDER_R1, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_R1, GenericInputBinding::R1},
+ {"R2", TRANSLATE_NOOP("Pad", "R2 (Right Trigger)"), ICON_PF_RIGHT_TRIGGER_R2, InputBindingInfo::Type::Button, PadJogcon::Inputs::PAD_R2, GenericInputBinding::R2},
+ {}, {}, {}, {},
+ {"DialLeft", TRANSLATE_NOOP("Pad", "Dial (Left)"), nullptr, InputBindingInfo::Type::HalfAxis, PadJogcon::Inputs::PAD_DIAL_LEFT, GenericInputBinding::LeftStickLeft},
+ {"DialRight", TRANSLATE_NOOP("Pad", "Dial (Right)"), nullptr, InputBindingInfo::Type::HalfAxis, PadJogcon::Inputs::PAD_DIAL_RIGHT, GenericInputBinding::LeftStickRight},
+ {"LargeMotor", TRANSLATE_NOOP("Pad", "Large (Low Frequency) Motor"), nullptr, InputBindingInfo::Type::Motor, 0, GenericInputBinding::LargeMotor},
+ {"SmallMotor", TRANSLATE_NOOP("Pad", "Small (High Frequency) Motor"), nullptr, InputBindingInfo::Type::Motor, 0, GenericInputBinding::SmallMotor},
+ // clang-format on
+};
+
+static const SettingInfo s_settings[] = {
+ {SettingInfo::Type::Float, "Deadzone", TRANSLATE_NOOP("Pad", "Dial Deadzone"),
+ TRANSLATE_NOOP("Pad", "Sets the dial deadzone. Inputs below this value will not be sent to the PS2."),
+ "0.00", "0.00", "1.00", "0.01", TRANSLATE_NOOP("Pad", "%.0f%%"), nullptr, nullptr, 100.0f},
+ {SettingInfo::Type::Float, "AxisScale", TRANSLATE_NOOP("Pad", "Dial Sensitivity"),
+ TRANSLATE_NOOP("Pad", "Sets the dial scaling factor."),
+ "1.0", "0.01", "2.00", "0.01", TRANSLATE_NOOP("Pad", "%.0f%%"), nullptr, nullptr, 100.0f},
+};
+
+const Pad::ControllerInfo PadJogcon::ControllerInfo = {Pad::ControllerType::Jogcon, "Jogcon",
+ TRANSLATE_NOOP("Pad", "Jogcon"), ICON_PF_GAMEPAD_ALT, s_bindings, s_settings, Pad::VibrationCapabilities::LargeSmallMotors};
+
+void PadJogcon::ConfigLog()
+{
+ const auto [port, slot] = sioConvertPadToPortAndSlot(unifiedSlot);
+
+ // AL: Analog Light (is it turned on right now)
+ // AB: Analog Button (is it useable or is it locked in its current state)
+ Console.WriteLn(fmt::format("[Pad] Jogcon Config Finished - P{0}/S{1} - AL: {2} - AB: {3}",
+ port + 1,
+ slot + 1,
+ (this->analogLight ? "On" : "Off"),
+ (this->analogLocked ? "Locked" : "Usable")));
+}
+
+u8 PadJogcon::Mystery(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 5:
+ return 0x02;
+ case 8:
+ return 0x5a;
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadJogcon::ButtonQuery(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ case 4:
+ return 0xff;
+ case 5:
+ return 0x03;
+ case 8:
+ return 0x5a;
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadJogcon::Poll(u8 commandByte)
+{
+ const u32 buttons = GetButtons();
+ u8 largeMotor = 0x00;
+ u8 smallMotor = 0x00;
+
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ this->vibrationMotors[0] = commandByte;
+ return (buttons >> 8) & 0xff;
+ case 4:
+ this->vibrationMotors[1] = commandByte;
+
+ // Apply the vibration mapping to the motors
+ switch (this->largeMotorLastConfig)
+ {
+ case 0x00:
+ largeMotor = this->vibrationMotors[0];
+ break;
+ case 0x01:
+ largeMotor = this->vibrationMotors[1];
+ break;
+ default:
+ break;
+ }
+
+ // Small motor on the controller is only controlled by the LSB.
+ // Any value can be sent by the software, but only odd numbers
+ // (LSB set) will turn on the motor.
+ switch (this->smallMotorLastConfig)
+ {
+ case 0x00:
+ smallMotor = this->vibrationMotors[0] & 0x01;
+ break;
+ case 0x01:
+ smallMotor = this->vibrationMotors[1] & 0x01;
+ break;
+ default:
+ break;
+ }
+
+ // Order is reversed here - SetPadVibrationIntensity takes large motor first, then small. PS2 orders small motor first, large motor second.
+ InputManager::SetPadVibrationIntensity(this->unifiedSlot,
+ std::min(static_cast(largeMotor) * GetVibrationScale(1) * (1.0f / 255.0f), 1.0f),
+ // Small motor on the PS2 is either on full power or zero power, it has no variable speed. If the game supplies any value here at all,
+ // the pad in turn supplies full power to the motor, or no power at all if zero.
+ std::min(static_cast((smallMotor ? 0xff : 0)) * GetVibrationScale(0) * (1.0f / 255.0f), 1.0f));
+
+ return buttons & 0xff;
+ case 5:
+ return dial & 0xff;
+ case 6:
+ return (dial >> 8) & 0xff;
+ case 7:
+ {
+ const u8 b7 = this->dial > this->lastdial ? 1 : this->dial < this->lastdial ? 2 : 0;
+ this->lastdial = this->dial;
+ return b7;
+ }
+ case 8:
+ return 0x00;
+ }
+
+ Console.Warning("%s(%02X) Did not reach a valid return path! Returning zero as a failsafe!", __FUNCTION__, commandByte);
+ return 0x00;
+}
+
+u8 PadJogcon::Config(u8 commandByte)
+{
+ if (this->commandBytesReceived == 3)
+ {
+ if (commandByte)
+ {
+ if (!this->isInConfig)
+ {
+ this->isInConfig = true;
+ }
+ else
+ {
+ Console.Warning("%s(%02X) Unexpected enter while already in config mode", __FUNCTION__, commandByte);
+ }
+ }
+ else
+ {
+ if (this->isInConfig)
+ {
+ this->isInConfig = false;
+ this->ConfigLog();
+ }
+ else
+ {
+ Console.Warning("%s(%02X) Unexpected exit while not in config mode", __FUNCTION__, commandByte);
+ }
+ }
+ }
+
+ return 0x00;
+}
+
+// Changes the mode of the controller between digital and analog, and adjusts the analog LED accordingly.
+u8 PadJogcon::ModeSwitch(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ this->analogLight = commandByte;
+
+ if (this->analogLight)
+ {
+ this->currentMode = Pad::Mode::ANALOG;
+ }
+ else
+ {
+ this->currentMode = Pad::Mode::DIGITAL;
+ }
+
+ break;
+ case 4:
+ this->analogLocked = (commandByte == 0x03);
+ break;
+ default:
+ break;
+ }
+
+ return 0x00;
+}
+
+u8 PadJogcon::StatusInfo(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ return static_cast(Pad::PhysicalType::STANDARD);
+ case 4:
+ return 0x02;
+ case 5:
+ return this->analogLight;
+ case 6:
+ return 0x02;
+ case 7:
+ return 0x01;
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadJogcon::Constant1(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ commandStage = (commandByte != 0);
+ return 0x00;
+ case 5:
+ return 0x01;
+ case 6:
+ return (!commandStage ? 0x02 : 0x01);
+ case 7:
+ return (!commandStage ? 0x00 : 0x01);
+ case 8:
+ return (commandStage ? 0x0a : 0x14);
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadJogcon::Constant2(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 5:
+ return 0x02;
+ case 7:
+ return 0x01;
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadJogcon::Constant3(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ commandStage = (commandByte != 0);
+ return 0x00;
+ case 6:
+ return (!commandStage ? 0x04 : 0x07);
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadJogcon::VibrationMap(u8 commandByte)
+{
+ u8 ret = 0xff;
+
+ switch (commandBytesReceived)
+ {
+ case 3:
+ ret = this->smallMotorLastConfig;
+ this->smallMotorLastConfig = commandByte;
+ return ret;
+ case 4:
+ ret = this->largeMotorLastConfig;
+ this->largeMotorLastConfig = commandByte;
+ return ret;
+ case 8:
+ return 0xff;
+ default:
+ return 0xff;
+ }
+}
+
+PadJogcon::PadJogcon(u8 unifiedSlot, size_t ejectTicks)
+ : PadBase(unifiedSlot, ejectTicks)
+{
+ currentMode = Pad::Mode::PS1_JOGCON;
+}
+
+PadJogcon::~PadJogcon() = default;
+
+Pad::ControllerType PadJogcon::GetType() const
+{
+ return Pad::ControllerType::Jogcon;
+}
+
+const Pad::ControllerInfo& PadJogcon::GetInfo() const
+{
+ return ControllerInfo;
+}
+
+void PadJogcon::Set(u32 index, float value)
+{
+ if (index > Inputs::LENGTH)
+ {
+ return;
+ }
+
+ if (IsAnalogKey(index))
+ {
+ const float dz_value = (this->dialDeadzone > 0.0f && value < this->dialDeadzone) ? 0.0f : value;
+ this->rawInputs[index] = static_cast(std::clamp(dz_value * this->dialScale * 255.0f, 0.0f, 255.0f));
+
+ this->dial = this->rawInputs[Inputs::PAD_DIAL_RIGHT] != 0
+ ? this->rawInputs[Inputs::PAD_DIAL_RIGHT]
+ : -this->rawInputs[Inputs::PAD_DIAL_LEFT];
+ return;
+ }
+
+ this->rawInputs[index] = static_cast(std::clamp(value * 255.0f, 0.0f, 255.0f));
+ if (this->rawInputs[index] > 0.0f)
+ {
+ this->buttons &= ~(1u << bitmaskMapping[index]);
+ }
+ else
+ {
+ this->buttons |= (1u << bitmaskMapping[index]);
+ }
+}
+
+void PadJogcon::SetRawAnalogs(const std::tuple left, const std::tuple right)
+{
+ this->dial = (std::get<0>(left) << 8) | std::get<1>(left);
+}
+
+void PadJogcon::SetRawPressureButton(u32 index, const std::tuple value)
+{
+ this->rawInputs[index] = std::get<1>(value);
+
+ if (std::get<0>(value))
+ {
+ this->buttons &= ~(1u << bitmaskMapping[index]);
+ }
+ else
+ {
+ this->buttons |= (1u << bitmaskMapping[index]);
+ }
+}
+
+void PadJogcon::SetAxisScale(float deadzone, float scale)
+{
+ this->dialDeadzone = deadzone;
+ this->dialScale = scale;
+}
+
+float PadJogcon::GetVibrationScale(u32 motor) const
+{
+ return this->vibrationScale[motor];
+}
+
+void PadJogcon::SetVibrationScale(u32 motor, float scale)
+{
+ this->vibrationScale[motor] = scale;
+}
+
+float PadJogcon::GetPressureModifier() const
+{
+ return 0;
+}
+
+void PadJogcon::SetPressureModifier(float mod)
+{
+}
+
+void PadJogcon::SetButtonDeadzone(float deadzone)
+{
+}
+
+void PadJogcon::SetAnalogInvertL(bool x, bool y)
+{
+}
+
+void PadJogcon::SetAnalogInvertR(bool x, bool y)
+{
+}
+
+float PadJogcon::GetEffectiveInput(u32 index) const
+{
+ if (!IsAnalogKey(index))
+ return GetRawInput(index);
+
+ switch (index)
+ {
+ case Inputs::PAD_DIAL_LEFT:
+ return (dial < 0) ? -(dial / 255.0f) : 0;
+
+ case Inputs::PAD_DIAL_RIGHT:
+ return (dial > 0) ? (dial / 255.0f) : 0;
+
+ default:
+ return 0;
+ }
+}
+
+u8 PadJogcon::GetRawInput(u32 index) const
+{
+ return rawInputs[index];
+}
+
+std::tuple PadJogcon::GetRawLeftAnalog() const
+{
+ return std::tuple{(dial >> 8) & 0xff, dial & 0xff};
+}
+
+std::tuple PadJogcon::GetRawRightAnalog() const
+{
+ return std::tuple{0x7f, 0x7f};
+}
+
+u32 PadJogcon::GetButtons() const
+{
+ return buttons;
+}
+
+u8 PadJogcon::GetPressure(u32 index) const
+{
+ return 0;
+}
+
+bool PadJogcon::Freeze(StateWrapper& sw)
+{
+ if (!PadBase::Freeze(sw) || !sw.DoMarker("PadJogcon"))
+ return false;
+
+ // Private PadJogcon members
+ sw.Do(&analogLight);
+ sw.Do(&analogLocked);
+ sw.Do(&commandStage);
+ sw.Do(&vibrationMotors);
+ sw.Do(&smallMotorLastConfig);
+ sw.Do(&largeMotorLastConfig);
+ return !sw.HasError();
+}
+
+u8 PadJogcon::SendCommandByte(u8 commandByte)
+{
+ u8 ret = 0;
+
+ switch (this->commandBytesReceived)
+ {
+ case 0:
+ ret = 0x00;
+ break;
+ case 1:
+ this->currentCommand = static_cast(commandByte);
+
+ if (this->currentCommand != Pad::Command::POLL && this->currentCommand != Pad::Command::CONFIG && !this->isInConfig)
+ {
+ Console.Warning("%s(%02X) Config-only command was sent to a pad outside of config mode!", __FUNCTION__, commandByte);
+ }
+
+ ret = this->isInConfig ? static_cast(Pad::Mode::CONFIG) : static_cast(this->currentMode);
+ break;
+ case 2:
+ ret = 0x5a;
+ break;
+ default:
+ switch (this->currentCommand)
+ {
+ case Pad::Command::MYSTERY:
+ ret = Mystery(commandByte);
+ break;
+ case Pad::Command::BUTTON_QUERY:
+ ret = ButtonQuery(commandByte);
+ break;
+ case Pad::Command::POLL:
+ ret = Poll(commandByte);
+ break;
+ case Pad::Command::CONFIG:
+ ret = Config(commandByte);
+ break;
+ case Pad::Command::MODE_SWITCH:
+ ret = ModeSwitch(commandByte);
+ break;
+ case Pad::Command::STATUS_INFO:
+ ret = StatusInfo(commandByte);
+ break;
+ case Pad::Command::CONST_1:
+ ret = Constant1(commandByte);
+ break;
+ case Pad::Command::CONST_2:
+ ret = Constant2(commandByte);
+ break;
+ case Pad::Command::CONST_3:
+ ret = Constant3(commandByte);
+ break;
+ case Pad::Command::VIBRATION_MAP:
+ ret = VibrationMap(commandByte);
+ break;
+ default:
+ ret = 0x00;
+ break;
+ }
+ }
+
+ this->commandBytesReceived++;
+ return ret;
+}
diff --git a/pcsx2/SIO/Pad/PadJogcon.h b/pcsx2/SIO/Pad/PadJogcon.h
new file mode 100644
index 0000000000..a0367cb8e7
--- /dev/null
+++ b/pcsx2/SIO/Pad/PadJogcon.h
@@ -0,0 +1,123 @@
+// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#pragma once
+
+#include "SIO/Pad/PadBase.h"
+
+class PadJogcon final : public PadBase
+{
+public:
+ enum Inputs
+ {
+ PAD_UP, // Directional pad up
+ PAD_RIGHT, // Directional pad right
+ PAD_DOWN, // Directional pad down
+ PAD_LEFT, // Directional pad left
+ PAD_TRIANGLE, // Triangle button
+ PAD_CIRCLE, // Circle button
+ PAD_CROSS, // Cross button
+ PAD_SQUARE, // Square button
+ PAD_SELECT, // Select button
+ PAD_START, // Start button
+ PAD_L1, // L1 button
+ PAD_L2, // L2 button
+ PAD_R1, // R1 button
+ PAD_R2, // R2 button
+
+ // This workaround is necessary because InputRecorder doesn't support custom Pads beside DS2:
+ // https://github.com/PCSX2/pcsx2/blob/ded55635c105c547756f5369b998297f602ada2f/pcsx2/Recording/PadData.cpp#L42-L61
+ // We need to consider and avoid the DS2's indexes that aren't saved in InputRecorder
+ // and we also have to use DS2's analog indexes for our analog axes.
+ PADDING1, PADDING2, PADDING3, PADDING4,
+
+ PAD_DIAL_LEFT, // Dial (Left)
+ PAD_DIAL_RIGHT, // Dial (Right)
+ LENGTH,
+ };
+
+ static constexpr u8 VIBRATION_MOTORS = 2;
+
+private:
+ u32 buttons = 0xffffffffu;
+ s16 dial = 0x0000;
+ s16 lastdial = 0x0000;
+
+ bool analogLight = false;
+ bool analogLocked = false;
+ bool commandStage = false;
+ std::array vibrationMotors = {};
+ std::array vibrationScale = {1.0f, 1.0f};
+ float dialDeadzone = 0.0f;
+ float dialScale = 1.0f;
+ // Used to store the last vibration mapping request the PS2 made for the small motor.
+ u8 smallMotorLastConfig = 0xff;
+ // Used to store the last vibration mapping request the PS2 made for the large motor.
+ u8 largeMotorLastConfig = 0xff;
+
+ // Since we reordered the buttons for better UI, we need to remap them here.
+ static constexpr std::array bitmaskMapping = {{
+ 12, // PAD_UP
+ 13, // PAD_RIGHT
+ 14, // PAD_DOWN
+ 15, // PAD_LEFT
+ 4, // PAD_TRIANGLE
+ 5, // PAD_CIRCLE
+ 6, // PAD_CROSS
+ 7, // PAD_SQUARE
+ 8, // PAD_SELECT
+ 11, // PAD_START
+ 2, // PAD_L1
+ 0, // PAD_L2
+ 3, // PAD_R1
+ 1, // PAD_R2
+ }};
+
+ void ConfigLog();
+
+ u8 Mystery(u8 commandByte);
+ u8 ButtonQuery(u8 commandByte);
+ u8 Poll(u8 commandByte);
+ u8 Config(u8 commandByte);
+ u8 ModeSwitch(u8 commandByte);
+ u8 StatusInfo(u8 commandByte);
+ u8 Constant1(u8 commandByte);
+ u8 Constant2(u8 commandByte);
+ u8 Constant3(u8 commandByte);
+ u8 VibrationMap(u8 commandByte);
+
+public:
+ PadJogcon(u8 unifiedSlot, size_t ejectTicks);
+ ~PadJogcon() override;
+
+ static inline bool IsAnalogKey(int index)
+ {
+ return index == Inputs::PAD_DIAL_LEFT || index == Inputs::PAD_DIAL_RIGHT;
+ }
+
+ Pad::ControllerType GetType() const override;
+ const Pad::ControllerInfo& GetInfo() const override;
+ void Set(u32 index, float value) override;
+ void SetRawAnalogs(const std::tuple left, const std::tuple right) override;
+ void SetRawPressureButton(u32 index, const std::tuple value) override;
+ void SetAxisScale(float deadzone, float scale) override;
+ float GetVibrationScale(u32 motor) const override;
+ void SetVibrationScale(u32 motor, float scale) override;
+ float GetPressureModifier() const override;
+ void SetPressureModifier(float mod) override;
+ void SetButtonDeadzone(float deadzone) override;
+ void SetAnalogInvertL(bool x, bool y) override;
+ void SetAnalogInvertR(bool x, bool y) override;
+ float GetEffectiveInput(u32 index) const override;
+ u8 GetRawInput(u32 index) const override;
+ std::tuple GetRawLeftAnalog() const override;
+ std::tuple GetRawRightAnalog() const override;
+ u32 GetButtons() const override;
+ u8 GetPressure(u32 index) const override;
+
+ bool Freeze(StateWrapper& sw) override;
+
+ u8 SendCommandByte(u8 commandByte) override;
+
+ static const Pad::ControllerInfo ControllerInfo;
+};
diff --git a/pcsx2/SIO/Pad/PadNegcon.cpp b/pcsx2/SIO/Pad/PadNegcon.cpp
new file mode 100644
index 0000000000..a62fd56c20
--- /dev/null
+++ b/pcsx2/SIO/Pad/PadNegcon.cpp
@@ -0,0 +1,549 @@
+// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#include "SIO/Pad/PadNegcon.h"
+#include "SIO/Pad/Pad.h"
+#include "SIO/Sio.h"
+
+#include "Common.h"
+#include "Input/InputManager.h"
+#include "Host.h"
+
+#include "IconsPromptFont.h"
+
+static const InputBindingInfo s_bindings[] = {
+ // clang-format off
+ {"Up", TRANSLATE_NOOP("Pad", "D-Pad Up"), ICON_PF_DPAD_UP, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_UP, GenericInputBinding::DPadUp},
+ {"Right", TRANSLATE_NOOP("Pad", "D-Pad Right"), ICON_PF_DPAD_RIGHT, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_RIGHT, GenericInputBinding::DPadRight},
+ {"Down", TRANSLATE_NOOP("Pad", "D-Pad Down"), ICON_PF_DPAD_DOWN, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_DOWN, GenericInputBinding::DPadDown},
+ {"Left", TRANSLATE_NOOP("Pad", "D-Pad Left"), ICON_PF_DPAD_LEFT, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_LEFT, GenericInputBinding::DPadLeft},
+ {"B", TRANSLATE_NOOP("Pad", "B Button"), ICON_PF_BUTTON_B, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_B, GenericInputBinding::Triangle},
+ {"A", TRANSLATE_NOOP("Pad", "A Button"), ICON_PF_BUTTON_A, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_A, GenericInputBinding::Circle},
+ {"I", TRANSLATE_NOOP("Pad", "I Button"), nullptr, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_I, GenericInputBinding::Cross},
+ {"II", TRANSLATE_NOOP("Pad", "II Button"), nullptr, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_II, GenericInputBinding::Square},
+ {},
+ {"Start", TRANSLATE_NOOP("Pad", "Start"), ICON_PF_START, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_START, GenericInputBinding::Start},
+ {"L", TRANSLATE_NOOP("Pad", "L (Left Bumper)"), ICON_PF_LEFT_SHOULDER_L1, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_L, GenericInputBinding::L2},
+ {"R", TRANSLATE_NOOP("Pad", "R (Right Bumper)"), ICON_PF_RIGHT_SHOULDER_R1, InputBindingInfo::Type::Button, PadNegcon::Inputs::PAD_R, GenericInputBinding::R2},
+ {"TwistLeft", TRANSLATE_NOOP("Pad", "Twist (Left)"), nullptr, InputBindingInfo::Type::HalfAxis, PadNegcon::Inputs::PAD_TWIST_LEFT, GenericInputBinding::LeftStickLeft},
+ {"TwistRight", TRANSLATE_NOOP("Pad", "Twist (Right)"), nullptr, InputBindingInfo::Type::HalfAxis, PadNegcon::Inputs::PAD_TWIST_RIGHT, GenericInputBinding::LeftStickRight},
+ {"LargeMotor", TRANSLATE_NOOP("Pad", "Large (Low Frequency) Motor"), nullptr, InputBindingInfo::Type::Motor, 0, GenericInputBinding::LargeMotor},
+ {"SmallMotor", TRANSLATE_NOOP("Pad", "Small (High Frequency) Motor"), nullptr, InputBindingInfo::Type::Motor, 0, GenericInputBinding::SmallMotor},
+ // clang-format on
+};
+
+static const SettingInfo s_settings[] = {
+ {SettingInfo::Type::Float, "Deadzone", TRANSLATE_NOOP("Pad", "Twist Deadzone"),
+ TRANSLATE_NOOP("Pad", "Sets the twist deadzone. Inputs below this value will not be sent to the PS2."),
+ "0.00", "0.00", "1.00", "0.01", "%.0f%%", nullptr, nullptr, 100.0f},
+ {SettingInfo::Type::Float, "AxisScale", TRANSLATE_NOOP("Pad", "Twist Sensitivity"),
+ TRANSLATE_NOOP("Pad", "Sets the twist scaling factor."),
+ "1.0", "0.01", "2.00", "0.01", "%.0f%%", nullptr, nullptr, 100.0f},
+};
+
+const Pad::ControllerInfo PadNegcon::ControllerInfo = {Pad::ControllerType::Negcon, "Negcon",
+ TRANSLATE_NOOP("Pad", "Negcon"), ICON_PF_GAMEPAD_ALT, s_bindings, s_settings, Pad::VibrationCapabilities::LargeSmallMotors};
+
+void PadNegcon::ConfigLog()
+{
+ const auto [port, slot] = sioConvertPadToPortAndSlot(unifiedSlot);
+
+ // AL: Analog Light (is it turned on right now)
+ // AB: Analog Button (is it useable or is it locked in its current state)
+ Console.WriteLn(fmt::format("[Pad] Negcon Config Finished - P{0}/S{1} - AL: {2} - AB: {3}",
+ port + 1,
+ slot + 1,
+ (this->analogLight ? "On" : "Off"),
+ (this->analogLocked ? "Locked" : "Usable")));
+}
+
+u8 PadNegcon::Mystery(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 5:
+ return 0x02;
+ case 8:
+ return 0x5a;
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadNegcon::ButtonQuery(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ case 4:
+ return 0xff;
+ case 5:
+ return 0x03;
+ case 8:
+ return 0x5a;
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadNegcon::Poll(u8 commandByte)
+{
+ const u32 buttons = GetButtons();
+ u8 largeMotor = 0x00;
+ u8 smallMotor = 0x00;
+
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ this->vibrationMotors[0] = commandByte;
+ return (buttons >> 8) & 0xff;
+ case 4:
+ this->vibrationMotors[1] = commandByte;
+
+ // Apply the vibration mapping to the motors
+ switch (this->largeMotorLastConfig)
+ {
+ case 0x00:
+ largeMotor = this->vibrationMotors[0];
+ break;
+ case 0x01:
+ largeMotor = this->vibrationMotors[1];
+ break;
+ default:
+ break;
+ }
+
+ // Small motor on the controller is only controlled by the LSB.
+ // Any value can be sent by the software, but only odd numbers
+ // (LSB set) will turn on the motor.
+ switch (this->smallMotorLastConfig)
+ {
+ case 0x00:
+ smallMotor = this->vibrationMotors[0] & 0x01;
+ break;
+ case 0x01:
+ smallMotor = this->vibrationMotors[1] & 0x01;
+ break;
+ default:
+ break;
+ }
+
+ // Order is reversed here - SetPadVibrationIntensity takes large motor first, then small. PS2 orders small motor first, large motor second.
+ InputManager::SetPadVibrationIntensity(this->unifiedSlot,
+ std::min(static_cast(largeMotor) * GetVibrationScale(1) * (1.0f / 255.0f), 1.0f),
+ // Small motor on the PS2 is either on full power or zero power, it has no variable speed. If the game supplies any value here at all,
+ // the pad in turn supplies full power to the motor, or no power at all if zero.
+ std::min(static_cast((smallMotor ? 0xff : 0)) * GetVibrationScale(0) * (1.0f / 255.0f), 1.0f));
+
+ return buttons & 0xff;
+ case 5:
+ return this->analogs.twist;
+ case 6:
+ return this->analogs.i;
+ case 7:
+ return this->analogs.ii;
+ case 8:
+ return this->analogs.l;
+ }
+
+ Console.Warning("%s(%02X) Did not reach a valid return path! Returning zero as a failsafe!", __FUNCTION__, commandByte);
+ return 0x00;
+}
+
+u8 PadNegcon::Config(u8 commandByte)
+{
+ if (this->commandBytesReceived == 3)
+ {
+ if (commandByte)
+ {
+ if (!this->isInConfig)
+ {
+ this->isInConfig = true;
+ }
+ else
+ {
+ Console.Warning("%s(%02X) Unexpected enter while already in config mode", __FUNCTION__, commandByte);
+ }
+ }
+ else
+ {
+ if (this->isInConfig)
+ {
+ this->isInConfig = false;
+ this->ConfigLog();
+ }
+ else
+ {
+ Console.Warning("%s(%02X) Unexpected exit while not in config mode", __FUNCTION__, commandByte);
+ }
+ }
+ }
+
+ return 0x00;
+}
+
+// Changes the mode of the controller between digital and analog, and adjusts the analog LED accordingly.
+u8 PadNegcon::ModeSwitch(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ this->analogLight = commandByte;
+
+ if (this->analogLight)
+ {
+ this->currentMode = Pad::Mode::ANALOG;
+ }
+ else
+ {
+ this->currentMode = Pad::Mode::DIGITAL;
+ }
+
+ break;
+ case 4:
+ this->analogLocked = (commandByte == 0x03);
+ break;
+ default:
+ break;
+ }
+
+ return 0x00;
+}
+
+u8 PadNegcon::StatusInfo(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ return static_cast(Pad::PhysicalType::STANDARD);
+ case 4:
+ return 0x02;
+ case 5:
+ return this->analogLight;
+ case 6:
+ return 0x02;
+ case 7:
+ return 0x01;
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadNegcon::Constant1(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ commandStage = (commandByte != 0);
+ return 0x00;
+ case 5:
+ return 0x01;
+ case 6:
+ return (!commandStage ? 0x02 : 0x01);
+ case 7:
+ return (!commandStage ? 0x00 : 0x01);
+ case 8:
+ return (commandStage ? 0x0a : 0x14);
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadNegcon::Constant2(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 5:
+ return 0x02;
+ case 7:
+ return 0x01;
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadNegcon::Constant3(u8 commandByte)
+{
+ switch (this->commandBytesReceived)
+ {
+ case 3:
+ commandStage = (commandByte != 0);
+ return 0x00;
+ case 6:
+ return (!commandStage ? 0x04 : 0x07);
+ default:
+ return 0x00;
+ }
+}
+
+u8 PadNegcon::VibrationMap(u8 commandByte)
+{
+ u8 ret = 0xff;
+
+ switch (commandBytesReceived)
+ {
+ case 3:
+ ret = this->smallMotorLastConfig;
+ this->smallMotorLastConfig = commandByte;
+ return ret;
+ case 4:
+ ret = this->largeMotorLastConfig;
+ this->largeMotorLastConfig = commandByte;
+ return ret;
+ case 8:
+ return 0xff;
+ default:
+ return 0xff;
+ }
+}
+
+PadNegcon::PadNegcon(u8 unifiedSlot, size_t ejectTicks)
+ : PadBase(unifiedSlot, ejectTicks)
+{
+ currentMode = Pad::Mode::NEGCON;
+}
+
+PadNegcon::~PadNegcon() = default;
+
+Pad::ControllerType PadNegcon::GetType() const
+{
+ return Pad::ControllerType::Negcon;
+}
+
+const Pad::ControllerInfo& PadNegcon::GetInfo() const
+{
+ return ControllerInfo;
+}
+
+void PadNegcon::Set(u32 index, float value)
+{
+ if (index > Inputs::LENGTH)
+ {
+ return;
+ }
+
+ if (IsTwistKey(index))
+ {
+ const float dz_value = (this->twistDeadzone > 0.0f && value < this->twistDeadzone) ? 0.0f : value;
+ this->rawInputs[index] = static_cast(std::clamp(dz_value * this->twistScale * 255.0f, 0.0f, 255.0f));
+
+ this->analogs.twist = this->rawInputs[Inputs::PAD_TWIST_RIGHT] != 0 ? 127u + (this->rawInputs[Inputs::PAD_TWIST_RIGHT] + 1u) / 2u : 127u - (this->rawInputs[Inputs::PAD_TWIST_LEFT] - 1u) / 2;
+ return;
+ }
+
+ this->rawInputs[index] = static_cast(std::clamp(value * 255.0f, 0.0f, 255.0f));
+ if (IsAnalogKey(index))
+ {
+ switch (index)
+ {
+ case PAD_I:
+ this->analogs.i = this->rawInputs[index];
+ break;
+ case PAD_II:
+ this->analogs.ii = this->rawInputs[index];
+ break;
+ case PAD_L:
+ this->analogs.l = this->rawInputs[index];
+ break;
+ }
+ }
+
+ if (this->rawInputs[index] > 0.0f)
+ {
+ this->buttons &= ~(1u << bitmaskMapping[index]);
+ }
+ else
+ {
+ this->buttons |= (1u << bitmaskMapping[index]);
+ }
+}
+
+void PadNegcon::SetRawAnalogs(const std::tuple left, const std::tuple right)
+{
+ this->analogs.i = std::get<0>(left);
+ this->analogs.ii = std::get<1>(left);
+ this->analogs.l = std::get<0>(right);
+ this->analogs.twist = std::get<1>(right);
+}
+
+void PadNegcon::SetRawPressureButton(u32 index, const std::tuple value)
+{
+ this->rawInputs[index] = std::get<1>(value);
+ if (std::get<0>(value))
+ {
+ this->buttons &= ~(1u << bitmaskMapping[index]);
+ }
+ else
+ {
+ this->buttons |= (1u << bitmaskMapping[index]);
+ }
+}
+
+void PadNegcon::SetAxisScale(float deadzone, float scale)
+{
+ this->twistDeadzone = deadzone;
+ this->twistScale = scale;
+}
+
+float PadNegcon::GetVibrationScale(u32 motor) const
+{
+ return this->vibrationScale[motor];
+}
+
+void PadNegcon::SetVibrationScale(u32 motor, float scale)
+{
+ this->vibrationScale[motor] = scale;
+}
+
+float PadNegcon::GetPressureModifier() const
+{
+ return 0;
+}
+
+void PadNegcon::SetPressureModifier(float mod)
+{
+}
+
+void PadNegcon::SetButtonDeadzone(float deadzone)
+{
+}
+
+void PadNegcon::SetAnalogInvertL(bool x, bool y)
+{
+}
+
+void PadNegcon::SetAnalogInvertR(bool x, bool y)
+{
+}
+
+float PadNegcon::GetEffectiveInput(u32 index) const
+{
+ if (!IsAnalogKey(index))
+ return GetRawInput(index);
+
+ switch (index)
+ {
+ case Inputs::PAD_I:
+ return analogs.i;
+
+ case Inputs::PAD_II:
+ return analogs.ii;
+
+ case Inputs::PAD_L:
+ return analogs.l;
+
+ case Inputs::PAD_TWIST_LEFT:
+ return (analogs.twist < 127) ? ((127 - analogs.twist) / 127.0f) : 0;
+
+ case Inputs::PAD_TWIST_RIGHT:
+ return (analogs.twist > 128) ? ((analogs.twist - 128) / 127.0f) : 0;
+
+ default:
+ return 0;
+ }
+}
+
+u8 PadNegcon::GetRawInput(u32 index) const
+{
+ return rawInputs[index];
+}
+
+std::tuple PadNegcon::GetRawLeftAnalog() const
+{
+ return std::tuple{analogs.i, analogs.ii};
+}
+
+std::tuple PadNegcon::GetRawRightAnalog() const
+{
+ return std::tuple{analogs.l, analogs.twist};
+}
+
+u32 PadNegcon::GetButtons() const
+{
+ return buttons;
+}
+
+u8 PadNegcon::GetPressure(u32 index) const
+{
+ return 0;
+}
+
+bool PadNegcon::Freeze(StateWrapper& sw)
+{
+ if (!PadBase::Freeze(sw) || !sw.DoMarker("PadNegcon"))
+ return false;
+
+ // Private PadNegcon members
+ sw.Do(&analogLight);
+ sw.Do(&analogLocked);
+ sw.Do(&commandStage);
+ sw.Do(&vibrationMotors);
+ sw.Do(&smallMotorLastConfig);
+ sw.Do(&largeMotorLastConfig);
+ return !sw.HasError();
+}
+
+u8 PadNegcon::SendCommandByte(u8 commandByte)
+{
+ u8 ret = 0;
+
+ switch (this->commandBytesReceived)
+ {
+ case 0:
+ ret = 0x00;
+ break;
+ case 1:
+ this->currentCommand = static_cast(commandByte);
+
+ if (this->currentCommand != Pad::Command::POLL && this->currentCommand != Pad::Command::CONFIG && !this->isInConfig)
+ {
+ Console.Warning("%s(%02X) Config-only command was sent to a pad outside of config mode!", __FUNCTION__, commandByte);
+ }
+
+ ret = this->isInConfig ? static_cast(Pad::Mode::CONFIG) : static_cast(this->currentMode);
+ break;
+ case 2:
+ ret = 0x5a;
+ break;
+ default:
+ switch (this->currentCommand)
+ {
+ case Pad::Command::MYSTERY:
+ ret = Mystery(commandByte);
+ break;
+ case Pad::Command::BUTTON_QUERY:
+ ret = ButtonQuery(commandByte);
+ break;
+ case Pad::Command::POLL:
+ ret = Poll(commandByte);
+ break;
+ case Pad::Command::CONFIG:
+ ret = Config(commandByte);
+ break;
+ case Pad::Command::MODE_SWITCH:
+ ret = ModeSwitch(commandByte);
+ break;
+ case Pad::Command::STATUS_INFO:
+ ret = StatusInfo(commandByte);
+ break;
+ case Pad::Command::CONST_1:
+ ret = Constant1(commandByte);
+ break;
+ case Pad::Command::CONST_2:
+ ret = Constant2(commandByte);
+ break;
+ case Pad::Command::CONST_3:
+ ret = Constant3(commandByte);
+ break;
+ case Pad::Command::VIBRATION_MAP:
+ ret = VibrationMap(commandByte);
+ break;
+ default:
+ ret = 0x00;
+ break;
+ }
+ }
+
+ this->commandBytesReceived++;
+ return ret;
+}
diff --git a/pcsx2/SIO/Pad/PadNegcon.h b/pcsx2/SIO/Pad/PadNegcon.h
new file mode 100644
index 0000000000..d91f86f2d4
--- /dev/null
+++ b/pcsx2/SIO/Pad/PadNegcon.h
@@ -0,0 +1,135 @@
+// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
+// SPDX-License-Identifier: GPL-3.0+
+
+#pragma once
+
+#include "SIO/Pad/PadBase.h"
+
+class PadNegcon final : public PadBase
+{
+public:
+ enum Inputs
+ {
+ PAD_UP, // Directional pad up
+ PAD_RIGHT, // Directional pad right
+ PAD_DOWN, // Directional pad down
+ PAD_LEFT, // Directional pad left
+ PAD_B, // B button
+ PAD_A, // A button
+ PAD_I, // I button
+ PAD_II, // II button
+
+ // This workaround is necessary because InputRecorder doesn't support custom Pads beside DS2:
+ // https://github.com/PCSX2/pcsx2/blob/ded55635c105c547756f5369b998297f602ada2f/pcsx2/Recording/PadData.cpp#L42-L61
+ // We need to consider and avoid the DS2's indexes that aren't saved in InputRecorder
+ // and we also have to use DS2's analog indexes for our analog axes.
+ PAD_PADDING1,
+
+ PAD_START, // Start button
+ PAD_L, // L button
+ PAD_R, // R button
+ PAD_TWIST_LEFT, // Twist (Left)
+ PAD_TWIST_RIGHT, // Twist (Right)
+ LENGTH,
+ };
+
+ static constexpr u8 VIBRATION_MOTORS = 2;
+
+private:
+ struct Analogs
+ {
+ u8 i = 0x00;
+ u8 ii = 0x00;
+ u8 l = 0x00;
+ u8 twist = 0x80;
+ };
+
+ u32 buttons = 0xffffffffu;
+ Analogs analogs;
+
+ bool analogLight = false;
+ bool analogLocked = false;
+ bool commandStage = false;
+ std::array vibrationMotors = {};
+ std::array vibrationScale = {1.0f, 1.0f};
+ float twistDeadzone = 0.0f;
+ float twistScale = 1.0f;
+ // Used to store the last vibration mapping request the PS2 made for the small motor.
+ u8 smallMotorLastConfig = 0xff;
+ // Used to store the last vibration mapping request the PS2 made for the large motor.
+ u8 largeMotorLastConfig = 0xff;
+
+ // Since we reordered the buttons for better UI, we need to remap them here.
+ static constexpr std::array bitmaskMapping = {{
+ 12, // PAD_UP
+ 13, // PAD_RIGHT
+ 14, // PAD_DOWN
+ 15, // PAD_LEFT
+ 4, // PAD_B
+ 5, // PAD_A
+ 6, // PAD_I
+ 7, // PAD_II
+ 0,
+ 11, // PAD_START
+ 2, // PAD_L
+ 3, // PAD_R
+ }};
+
+ void ConfigLog();
+
+ u8 Mystery(u8 commandByte);
+ u8 ButtonQuery(u8 commandByte);
+ u8 Poll(u8 commandByte);
+ u8 Config(u8 commandByte);
+ u8 ModeSwitch(u8 commandByte);
+ u8 StatusInfo(u8 commandByte);
+ u8 Constant1(u8 commandByte);
+ u8 Constant2(u8 commandByte);
+ u8 Constant3(u8 commandByte);
+ u8 VibrationMap(u8 commandByte);
+
+public:
+ PadNegcon(u8 unifiedSlot, size_t ejectTicks);
+ ~PadNegcon() override;
+
+ static inline bool IsAnalogKey(int index)
+ {
+ return index == Inputs::PAD_I
+ || index == Inputs::PAD_II
+ || index == Inputs::PAD_L
+ || index == Inputs::PAD_TWIST_LEFT
+ || index == Inputs::PAD_TWIST_RIGHT;
+ }
+
+ static inline bool IsTwistKey(int index)
+ {
+ return index == Inputs::PAD_TWIST_LEFT
+ || index == Inputs::PAD_TWIST_RIGHT;
+ }
+
+ Pad::ControllerType GetType() const override;
+ const Pad::ControllerInfo& GetInfo() const override;
+ void Set(u32 index, float value) override;
+ void SetRawAnalogs(const std::tuple left, const std::tuple right) override;
+ void SetRawPressureButton(u32 index, const std::tuple value) override;
+ void SetAxisScale(float deadzone, float scale) override;
+ float GetVibrationScale(u32 motor) const override;
+ void SetVibrationScale(u32 motor, float scale) override;
+ float GetPressureModifier() const override;
+ void SetPressureModifier(float mod) override;
+ void SetButtonDeadzone(float deadzone) override;
+ void SetAnalogInvertL(bool x, bool y) override;
+ void SetAnalogInvertR(bool x, bool y) override;
+ float GetEffectiveInput(u32 index) const override;
+ u8 GetRawInput(u32 index) const override;
+ std::tuple GetRawLeftAnalog() const override;
+ std::tuple GetRawRightAnalog() const override;
+ u32 GetButtons() const override;
+ u8 GetPressure(u32 index) const override;
+
+ bool Freeze(StateWrapper& sw) override;
+
+ u8 SendCommandByte(u8 commandByte) override;
+
+ static const Pad::ControllerInfo ControllerInfo;
+};
diff --git a/pcsx2/SIO/Pad/PadTypes.h b/pcsx2/SIO/Pad/PadTypes.h
index e9a9baaff5..cf02d6236c 100644
--- a/pcsx2/SIO/Pad/PadTypes.h
+++ b/pcsx2/SIO/Pad/PadTypes.h
@@ -64,6 +64,8 @@ namespace Pad
NotConnected,
DualShock2,
Guitar,
+ Jogcon,
+ Negcon,
Popn,
Count
};
diff --git a/pcsx2/pcsx2.vcxproj b/pcsx2/pcsx2.vcxproj
index a0c70b5e14..cbe7dac10c 100644
--- a/pcsx2/pcsx2.vcxproj
+++ b/pcsx2/pcsx2.vcxproj
@@ -291,6 +291,8 @@
+
+
@@ -733,6 +735,8 @@
+
+
diff --git a/pcsx2/pcsx2.vcxproj.filters b/pcsx2/pcsx2.vcxproj.filters
index bc919af5c5..4964728abf 100644
--- a/pcsx2/pcsx2.vcxproj.filters
+++ b/pcsx2/pcsx2.vcxproj.filters
@@ -1431,6 +1431,12 @@
System\Ps2\EmotionEngine\EE\Dynarec\arm64
+
+ System\Ps2\Iop\SIO\PAD
+
+
+ System\Ps2\Iop\SIO\PAD
+
@@ -2372,6 +2378,12 @@
Tools\arm64
+
+ System\Ps2\Iop\SIO\PAD
+
+
+ System\Ps2\Iop\SIO\PAD
+