diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp index 07632e8dc5..7e7d2b9f42 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp @@ -3,6 +3,7 @@ #include "DolphinQt/Config/Mapping/MappingCommon.h" +#include #include #include diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp index 802290b12c..8e4964e190 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp @@ -5,14 +5,15 @@ #include #include +#include #include #include #include +#include #include #include -#include #include "Common/MathUtil.h" @@ -23,9 +24,10 @@ #include "InputCommon/ControllerEmu/ControlGroup/Force.h" #include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h" #include "InputCommon/ControllerInterface/CoreDevice.h" +#include "InputCommon/ControllerInterface/MappingCommon.h" #include "DolphinQt/Config/Mapping/MappingWidget.h" -#include "DolphinQt/QtUtils/ModalMessageBox.h" +#include "DolphinQt/Config/Mapping/MappingWindow.h" #include "DolphinQt/Settings.h" namespace @@ -238,34 +240,6 @@ QPolygonF GetPolygonSegmentFromRadiusGetter(F&& radius_getter, double direction, return shape; } -// Used to check if the user seems to have attempted proper calibration. -bool IsCalibrationDataSensible(const ControllerEmu::ReshapableInput::CalibrationData& data) -{ - // Test that the average input radius is not below a threshold. - // This will make sure the user has actually moved their stick from neutral. - - // Even the GC controller's small range would pass this test. - constexpr double REASONABLE_AVERAGE_RADIUS = 0.6; - - MathUtil::RunningVariance stats; - - for (auto& x : data) - stats.Push(x); - - if (stats.Mean() < REASONABLE_AVERAGE_RADIUS) - { - return false; - } - - // Test that the standard deviation is below a threshold. - // This will make sure the user has not just filled in one side of their input. - - // Approx. deviation of a square input gate, anything much more than that would be unusual. - constexpr double REASONABLE_DEVIATION = 0.14; - - return stats.StandardDeviation() < REASONABLE_DEVIATION; -} - // Used to test for a miscalibrated stick so the user can be informed. bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInput& input) { @@ -313,6 +287,23 @@ void GenerateFibonacciSphere(int point_count, F&& callback) } } +// Draws an analog stick pushed to the right by the provided amount. +void DrawPushedStick(QPainter& p, ReshapableInputIndicator& indicator, double value) +{ + auto stick_color = indicator.GetGateBrushColor(); + indicator.AdjustGateColor(&stick_color); + const auto stick_pen_color = stick_color.darker(125); + p.setPen(QPen{stick_pen_color, 0}); + p.setBrush(stick_color); + constexpr float circle_radius = 0.65f; + p.drawEllipse(QPointF{value * 0.35f, 0.f}, circle_radius, circle_radius); + + p.setPen(QPen{indicator.GetRawInputColor(), 0}); + p.setBrush(Qt::NoBrush); + constexpr float alt_circle_radius = 0.45f; + p.drawEllipse(QPointF{value * 0.45f, 0.f}, alt_circle_radius, alt_circle_radius); +} + } // namespace void MappingIndicator::paintEvent(QPaintEvent*) @@ -328,11 +319,16 @@ void MappingIndicator::paintEvent(QPaintEvent*) Draw(); } +QColor CursorIndicator::GetGateBrushColor() const +{ + return CURSOR_TV_COLOR; +} + void CursorIndicator::Draw() { const auto adj_coord = m_cursor_group.GetState(true); - DrawReshapableInput(m_cursor_group, CURSOR_TV_COLOR, + DrawReshapableInput(m_cursor_group, adj_coord.IsVisible() ? std::make_optional(Common::DVec2(adj_coord.x, adj_coord.y)) : std::nullopt); @@ -362,7 +358,7 @@ void SquareIndicator::TransformPainter(QPainter& p) } void ReshapableInputIndicator::DrawReshapableInput( - ControllerEmu::ReshapableInput& stick, QColor gate_brush_color, + ControllerEmu::ReshapableInput& stick, std::optional adj_coord) { QPainter p(this); @@ -378,13 +374,14 @@ void ReshapableInputIndicator::DrawReshapableInput( if (IsCalibrating()) { - DrawCalibration(p, raw_coord); + m_calibration_widget->Draw(p, raw_coord); return; } DrawUnderGate(p); - QColor gate_pen_color = gate_brush_color.darker(125); + auto gate_brush_color = GetGateBrushColor(); + auto gate_pen_color = gate_brush_color.darker(125); AdjustGateColor(&gate_brush_color); AdjustGateColor(&gate_pen_color); @@ -435,24 +432,33 @@ void ReshapableInputIndicator::DrawReshapableInput( } } -void AnalogStickIndicator::Draw() +QColor AnalogStickIndicator::GetGateBrushColor() const { // Some hacks for pretty colors: const bool is_c_stick = m_group.name == "C-Stick"; - const auto gate_brush_color = is_c_stick ? C_STICK_GATE_COLOR : STICK_GATE_COLOR; + return is_c_stick ? C_STICK_GATE_COLOR : STICK_GATE_COLOR; +} +void AnalogStickIndicator::Draw() +{ const auto adj_coord = m_group.GetReshapableState(true); - DrawReshapableInput(m_group, gate_brush_color, + DrawReshapableInput(m_group, (adj_coord.x || adj_coord.y) ? std::make_optional(adj_coord) : std::nullopt); } void TiltIndicator::Update(float elapsed_seconds) { + ReshapableInputIndicator::Update(elapsed_seconds); WiimoteEmu::EmulateTilt(&m_motion_state, &m_group, elapsed_seconds); } +QColor TiltIndicator::GetGateBrushColor() const +{ + return TILT_GATE_COLOR; +} + void TiltIndicator::Draw() { auto adj_coord = Common::DVec2{-m_motion_state.angle.y, m_motion_state.angle.x} / MathUtil::PI; @@ -468,7 +474,7 @@ void TiltIndicator::Draw() adj_coord.x = std::fmod(adj_coord.x + norm_360_deg + norm_180_deg, norm_360_deg) - norm_180_deg; adj_coord.y = std::fmod(adj_coord.y + norm_360_deg + norm_180_deg, norm_360_deg) - norm_180_deg; - DrawReshapableInput(m_group, TILT_GATE_COLOR, + DrawReshapableInput(m_group, (adj_coord.x || adj_coord.y) ? std::make_optional(adj_coord) : std::nullopt); } @@ -604,12 +610,18 @@ void SwingIndicator::DrawUnderGate(QPainter& p) void SwingIndicator::Update(float elapsed_seconds) { + ReshapableInputIndicator::Update(elapsed_seconds); WiimoteEmu::EmulateSwing(&m_motion_state, &m_swing_group, elapsed_seconds); } +QColor SwingIndicator::GetGateBrushColor() const +{ + return SWING_GATE_COLOR; +} + void SwingIndicator::Draw() { - DrawReshapableInput(m_swing_group, SWING_GATE_COLOR, + DrawReshapableInput(m_swing_group, Common::DVec2{-m_motion_state.position.x, m_motion_state.position.z}); } @@ -915,42 +927,103 @@ void IRPassthroughMappingIndicator::Draw() } } -void ReshapableInputIndicator::DrawCalibration(QPainter& p, Common::DVec2 point) +void CalibrationWidget::Draw(QPainter& p, Common::DVec2 point) { - const auto center = m_calibration_widget->GetCenter(); + DrawInProgressMapping(p); + DrawInProgressCalibration(p, point); +} +double CalibrationWidget::GetAnimationElapsedSeconds() const +{ + return DT_s{Clock::now() - m_animation_start_time}.count(); +} + +void CalibrationWidget::RestartAnimation() +{ + m_animation_start_time = Clock::now(); +} + +void CalibrationWidget::DrawInProgressMapping(QPainter& p) +{ + if (!IsMapping()) + return; + + p.rotate(qRadiansToDegrees(m_mapper->GetCurrentAngle())); + + const auto ping_pong = 1 - std::abs(1 - (2 * std::fmod(GetAnimationElapsedSeconds(), 1))); + + // Stick. + DrawPushedStick(p, m_indicator, + QEasingCurve(QEasingCurve::OutBounce).valueForProgress(ping_pong)); + + // Arrow. + p.save(); + const auto triangle_x = + (QEasingCurve(QEasingCurve::InOutQuart).valueForProgress(ping_pong) * 0.3) + 0.1; + p.translate(triangle_x, 0.0); + + // An equilateral triangle. + constexpr auto triangle_h = 0.2f; + constexpr auto triangle_w_2 = triangle_h / std::numbers::sqrt3_v; + + p.setPen(Qt::NoPen); + p.setBrush(m_indicator.GetRawInputColor()); + p.drawPolygon(QPolygonF{{triangle_h, 0.f}, {0.f, -triangle_w_2}, {0.f, +triangle_w_2}}); + + p.restore(); +} + +void CalibrationWidget::DrawInProgressCalibration(QPainter& p, Common::DVec2 point) +{ + if (!IsCalibrating()) + return; + + const auto elapsed_seconds = GetAnimationElapsedSeconds(); + + // Clockwise spinning stick starting from center. + p.save(); + p.rotate(elapsed_seconds * -360.0); + DrawPushedStick( + p, m_indicator, + -QEasingCurve(QEasingCurve::OutCirc).valueForProgress(std::min(elapsed_seconds * 2, 1.0))); + p.restore(); + + const auto center = m_calibrator->GetCenter(); p.save(); p.translate(center.x, center.y); // Input shape. - p.setPen(GetInputShapePen()); + p.setPen(m_indicator.GetInputShapePen()); p.setBrush(Qt::NoBrush); p.drawPolygon(GetPolygonFromRadiusGetter( - [this](double angle) { return m_calibration_widget->GetCalibrationRadiusAtAngle(angle); })); + [this](double angle) { return m_calibrator->GetCalibrationRadiusAtAngle(angle); })); - // Center. + // Calibrated center. if (center.x || center.y) { - p.setPen(GetInputDotPen(GetCenterColor())); + p.setPen(GetInputDotPen(m_indicator.GetCenterColor())); p.drawPoint(QPointF{}); } - p.restore(); - // Stick position. - p.setPen(GetInputDotPen(GetAdjustedInputColor())); - p.drawPoint(QPointF{point.x, point.y}); + // Show the red dot only if the input is at least halfway pressed. + // The cool spinning stick is otherwise uglified by the red dot always being shown. + if (Common::DVec2{point.x, point.y}.LengthSquared() > (0.5 * 0.5)) + { + p.setPen(GetInputDotPen(m_indicator.GetAdjustedInputColor())); + p.drawPoint(QPointF{point.x, point.y}); + } } void ReshapableInputIndicator::UpdateCalibrationWidget(Common::DVec2 point) { - if (m_calibration_widget) + if (m_calibration_widget != nullptr) m_calibration_widget->Update(point); } bool ReshapableInputIndicator::IsCalibrating() const { - return m_calibration_widget && m_calibration_widget->IsCalibrating(); + return m_calibration_widget != nullptr && m_calibration_widget->IsActive(); } void ReshapableInputIndicator::SetCalibrationWidget(CalibrationWidget* widget) @@ -958,110 +1031,172 @@ void ReshapableInputIndicator::SetCalibrationWidget(CalibrationWidget* widget) m_calibration_widget = widget; } -CalibrationWidget::CalibrationWidget(ControllerEmu::ReshapableInput& input, +CalibrationWidget::~CalibrationWidget() = default; + +CalibrationWidget::CalibrationWidget(MappingWidget& mapping_widget, + ControllerEmu::ReshapableInput& input, ReshapableInputIndicator& indicator) - : m_input(input), m_indicator(indicator), m_completion_action{} + : m_mapping_widget(mapping_widget), m_input(input), m_indicator(indicator) { + connect(mapping_widget.GetParent(), &MappingWindow::CancelMapping, this, + &CalibrationWidget::ResetActions); + connect(mapping_widget.GetParent(), &MappingWindow::ConfigChanged, this, + &CalibrationWidget::ResetActions); + m_indicator.SetCalibrationWidget(this); // Make it more apparent that this is a menu with more options. setPopupMode(ToolButtonPopupMode::MenuButtonPopup); - SetupActions(); - setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - m_informative_timer = new QTimer(this); - connect(m_informative_timer, &QTimer::timeout, this, [this] { - // If the user has started moving we'll assume they know what they are doing. - if (*std::ranges::max_element(m_calibration_data) > 0.5) - return; - - ModalMessageBox::information( - this, tr("Calibration"), - tr("For best results please slowly move your input to all possible regions.")); - }); - m_informative_timer->setSingleShot(true); + ResetActions(); } -void CalibrationWidget::SetupActions() +void CalibrationWidget::DeleteAllActions() { - const auto calibrate_action = new QAction(tr("Calibrate"), this); - const auto center_action = new QAction(tr("Center and Calibrate"), this); - const auto reset_action = new QAction(tr("Reset"), this); - - connect(calibrate_action, &QAction::triggered, [this] { - StartCalibration(); - m_new_center = Common::DVec2{}; - }); - connect(center_action, &QAction::triggered, [this] { - StartCalibration(); - m_new_center = std::nullopt; - }); - connect(reset_action, &QAction::triggered, [this] { - m_input.SetCalibrationToDefault(); - m_input.SetCenter({0, 0}); - }); - for (auto* action : actions()) - removeAction(action); + delete action; +} +void CalibrationWidget::ResetActions() +{ + m_calibrator.reset(); + m_mapper.reset(); + + // i18n: A button to start the process of game controller analog stick mapping and calibration. + auto* const map_and_calibrate_action = new QAction(tr("Map and Calibrate"), this); + + // i18n: A button to start the process of game controller analog stick calibration. + auto* const calibrate_action = new QAction(tr("Calibrate"), this); + + // i18n: A button to calibrate the center and extremities of a game controller analog stick. + auto* const center_action = new QAction(tr("Center and Calibrate"), this); + + // i18n: A button to reset game controller analog stick calibration. + auto* const reset_action = new QAction(tr("Reset Calibration"), this); + + connect(map_and_calibrate_action, &QAction::triggered, this, + &CalibrationWidget::StartMappingAndCalibration); + connect(calibrate_action, &QAction::triggered, this, [this]() { StartCalibration(); }); + connect(center_action, &QAction::triggered, this, [this]() { StartCalibration(std::nullopt); }); + connect(reset_action, &QAction::triggered, this, [this]() { + const auto lock = m_mapping_widget.GetController()->GetStateLock(); + m_input.SetCalibrationToDefault(); + m_input.SetCenter({}); + }); + + DeleteAllActions(); + + addAction(map_and_calibrate_action); addAction(calibrate_action); addAction(center_action); addAction(reset_action); - setDefaultAction(calibrate_action); - m_completion_action = new QAction(tr("Finish Calibration"), this); - connect(m_completion_action, &QAction::triggered, [this] { - m_input.SetCenter(GetCenter()); - m_input.SetCalibrationData(std::move(m_calibration_data)); - m_informative_timer->stop(); - SetupActions(); - }); + setDefaultAction(map_and_calibrate_action); } -void CalibrationWidget::StartCalibration() +void CalibrationWidget::StartMappingAndCalibration() { - m_prev_point = {}; - m_calibration_data.assign(m_input.CALIBRATION_SAMPLE_COUNT, 0.0); + RestartAnimation(); - // Cancel calibration. - const auto cancel_action = new QAction(tr("Cancel Calibration"), this); - connect(cancel_action, &QAction::triggered, [this] { - m_calibration_data.clear(); - m_informative_timer->stop(); - SetupActions(); - }); + // i18n: A button to stop a game controller button mapping process. + auto* const cancel_action = new QAction(tr("Cancel Mapping"), this); + connect(cancel_action, &QAction::triggered, this, &CalibrationWidget::ResetActions); - for (auto* action : actions()) - removeAction(action); + DeleteAllActions(); addAction(cancel_action); - addAction(m_completion_action); setDefaultAction(cancel_action); - // If the user doesn't seem to know what they are doing after a bit inform them. - m_informative_timer->start(2000); + auto* const window = m_mapping_widget.GetParent(); + const auto& default_device = window->GetController()->GetDefaultDevice(); + + std::vector device_strings{default_device.ToString()}; + if (window->IsCreateOtherDeviceMappingsEnabled()) + device_strings = g_controller_interface.GetAllDeviceStrings(); + + const auto lock = window->GetController()->GetStateLock(); + m_mapper = std::make_unique(g_controller_interface, + device_strings); +} + +void CalibrationWidget::StartCalibration(std::optional center) +{ + RestartAnimation(); + m_calibrator = std::make_unique(center); + + // i18n: A button to abort a game controller calibration process. + auto* const cancel_action = new QAction(tr("Cancel Calibration"), this); + connect(cancel_action, &QAction::triggered, this, &CalibrationWidget::ResetActions); + + // i18n: A button to finalize a game controller calibration process. + auto* const finish_action = new QAction(tr("Finish Calibration"), this); + connect(finish_action, &QAction::triggered, this, [this]() { + const auto lock = m_mapping_widget.GetController()->GetStateLock(); + m_calibrator->ApplyResults(&m_input); + ResetActions(); + }); + connect(this, &CalibrationWidget::CalibrationIsSensible, finish_action, + [this, finish_action]() { setDefaultAction(finish_action); }); + + DeleteAllActions(); + + addAction(finish_action); + addAction(cancel_action); + setDefaultAction(cancel_action); } void CalibrationWidget::Update(Common::DVec2 point) { + // FYI: The "StateLock" is always held when this is called. + QFont f = parentWidget()->font(); QPalette p = parentWidget()->palette(); - // Use current point if center is being calibrated. - if (!m_new_center.has_value()) - m_new_center = point; - - if (IsCalibrating()) + if (IsMapping()) { - const auto new_point = point - *m_new_center; - m_input.UpdateCalibrationData(m_calibration_data, m_prev_point, new_point); - m_prev_point = new_point; - - if (IsCalibrationDataSensible(m_calibration_data)) + if (m_mapper->Update()) { - setDefaultAction(m_completion_action); + // Restart the animation for the next direction when progress is made. + RestartAnimation(); + } + + if (m_mapper->IsComplete()) + { + const bool needs_calibration = m_mapper->IsCalibrationNeeded(); + + if (m_mapper->ApplyResults(m_mapping_widget.GetController(), &m_input)) + { + emit m_mapping_widget.ConfigChanged(); + + if (needs_calibration) + { + StartCalibration(); + } + else + { + // Load square calibration for digital inputs. + m_input.SetCalibrationFromGate(ControllerEmu::SquareStickGate{1}); + m_input.SetCenter({}); + + ResetActions(); + } + } + else + { + ResetActions(); + } + + m_mapper.reset(); + } + } + else if (IsCalibrating()) + { + m_calibrator->Update(point); + if (m_calibrator->IsCalibrationDataSensible()) + { + emit CalibrationIsSensible(); } } else if (IsPointOutsideCalibration(point, m_input)) @@ -1075,17 +1210,17 @@ void CalibrationWidget::Update(Common::DVec2 point) setPalette(p); } +bool CalibrationWidget::IsActive() const +{ + return IsMapping() || IsCalibrating(); +} + +bool CalibrationWidget::IsMapping() const +{ + return m_mapper != nullptr; +} + bool CalibrationWidget::IsCalibrating() const { - return !m_calibration_data.empty(); -} - -double CalibrationWidget::GetCalibrationRadiusAtAngle(double angle) const -{ - return m_input.GetCalibrationDataRadiusAtAngle(m_calibration_data, angle); -} - -Common::DVec2 CalibrationWidget::GetCenter() const -{ - return m_new_center.value_or(Common::DVec2{}); + return m_calibrator != nullptr; } diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h index ca6f9b96fd..50e4df8201 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h @@ -26,6 +26,13 @@ class QPaintEvent; class QTimer; class CalibrationWidget; +class MappingWidget; + +namespace ciface::MappingCommon +{ +class ReshapableInputMapper; +class CalibrationBuilder; +} // namespace ciface::MappingCommon class MappingIndicator : public QWidget { @@ -79,15 +86,16 @@ class ReshapableInputIndicator : public SquareIndicator public: void SetCalibrationWidget(CalibrationWidget* widget); + virtual QColor GetGateBrushColor() const = 0; + protected: - void DrawReshapableInput(ControllerEmu::ReshapableInput& group, QColor gate_color, + void DrawReshapableInput(ControllerEmu::ReshapableInput& group, std::optional adj_coord); virtual void DrawUnderGate(QPainter&) {} bool IsCalibrating() const; - void DrawCalibration(QPainter& p, Common::DVec2 point); void UpdateCalibrationWidget(Common::DVec2 point); private: @@ -99,6 +107,8 @@ class AnalogStickIndicator : public ReshapableInputIndicator public: explicit AnalogStickIndicator(ControllerEmu::ReshapableInput& stick) : m_group(stick) {} + QColor GetGateBrushColor() const final; + private: void Draw() override; @@ -110,6 +120,8 @@ class TiltIndicator : public ReshapableInputIndicator public: explicit TiltIndicator(ControllerEmu::Tilt& tilt) : m_group(tilt) {} + QColor GetGateBrushColor() const final; + private: void Draw() override; void Update(float elapsed_seconds) override; @@ -123,6 +135,8 @@ class CursorIndicator : public ReshapableInputIndicator public: explicit CursorIndicator(ControllerEmu::Cursor& cursor) : m_cursor_group(cursor) {} + QColor GetGateBrushColor() const final; + private: void Draw() override; @@ -145,6 +159,8 @@ class SwingIndicator : public ReshapableInputIndicator public: explicit SwingIndicator(ControllerEmu::Force& swing) : m_swing_group(swing) {} + QColor GetGateBrushColor() const final; + private: void Draw() override; void Update(float elapsed_seconds) override; @@ -219,28 +235,46 @@ private: ControllerEmu::IRPassthrough& m_ir_group; }; + class CalibrationWidget : public QToolButton { + Q_OBJECT public: - CalibrationWidget(ControllerEmu::ReshapableInput& input, ReshapableInputIndicator& indicator); + CalibrationWidget(MappingWidget& mapping_widget, ControllerEmu::ReshapableInput& input, + ReshapableInputIndicator& indicator); + ~CalibrationWidget() override; void Update(Common::DVec2 point); - double GetCalibrationRadiusAtAngle(double angle) const; + void Draw(QPainter& p, Common::DVec2 point); - Common::DVec2 GetCenter() const; + bool IsActive() const; - bool IsCalibrating() const; +signals: + void CalibrationIsSensible(); private: - void StartCalibration(); - void SetupActions(); + void DrawInProgressMapping(QPainter& p); + void DrawInProgressCalibration(QPainter& p, Common::DVec2 point); + bool IsMapping() const; + bool IsCalibrating() const; + + void StartMappingAndCalibration(); + void StartCalibration(std::optional center = Common::DVec2{}); + + void ResetActions(); + void DeleteAllActions(); + + MappingWidget& m_mapping_widget; ControllerEmu::ReshapableInput& m_input; ReshapableInputIndicator& m_indicator; - QAction* m_completion_action; - ControllerEmu::ReshapableInput::CalibrationData m_calibration_data; - QTimer* m_informative_timer; - std::optional m_new_center; - Common::DVec2 m_prev_point; + + std::unique_ptr m_mapper; + std::unique_ptr m_calibrator; + + double GetAnimationElapsedSeconds() const; + void RestartAnimation(); + + Clock::time_point m_animation_start_time{}; }; diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp index 4331a0f535..8a5f8c0e47 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp @@ -152,7 +152,7 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con if (need_calibration) { const auto calibrate = - new CalibrationWidget(*static_cast(group), + new CalibrationWidget(*this, *static_cast(group), *static_cast(indicator)); form_layout->addRow(calibrate); diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index dc67819d75..3fb062faf7 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -246,7 +246,7 @@ - + diff --git a/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp b/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp index 505fd0260c..2852291021 100644 --- a/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp +++ b/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp @@ -5,14 +5,21 @@ #include #include +#include #include #include #include #include +#include "Common/MathUtil.h" #include "Common/StringUtil.h" + +#include "InputCommon/ControllerEmu/ControllerEmu.h" +#include "InputCommon/ControllerEmu/StickGate.h" +#include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/ControllerInterface/CoreDevice.h" +#include "InputCommon/InputConfig.h" namespace ciface::MappingCommon { @@ -163,4 +170,127 @@ bool ContainsCompleteDetection(const Core::InputDetector::Results& results) }); } +ReshapableInputMapper::ReshapableInputMapper(const Core::DeviceContainer& container, + std::span device_strings) +{ + m_input_detector.Start(container, device_strings); +} + +bool ReshapableInputMapper::Update() +{ + const auto prev_size = m_input_detector.GetResults().size(); + + constexpr auto wait_time = std::chrono::seconds{4}; + m_input_detector.Update(wait_time, wait_time, wait_time * REQUIRED_INPUT_COUNT); + + return m_input_detector.GetResults().size() != prev_size; +} + +float ReshapableInputMapper::GetCurrentAngle() const +{ + constexpr auto quarter_circle = float(MathUtil::TAU) * 0.25f; + return quarter_circle - (float(m_input_detector.GetResults().size()) * quarter_circle); +} + +bool ReshapableInputMapper::IsComplete() const +{ + return m_input_detector.GetResults().size() >= REQUIRED_INPUT_COUNT || + m_input_detector.IsComplete(); +} + +bool ReshapableInputMapper::IsCalibrationNeeded() const +{ + return std::ranges::any_of(m_input_detector.GetResults() | std::views::take(REQUIRED_INPUT_COUNT), + &ciface::Core::InputDetector::Detection::IsAnalogPress); +} + +bool ReshapableInputMapper::ApplyResults(ControllerEmu::EmulatedController* controller, + ControllerEmu::ReshapableInput* stick) +{ + auto const detections = m_input_detector.TakeResults(); + + if (detections.size() < REQUIRED_INPUT_COUNT) + return false; + + // Transpose URDL to UDLR. + const std::array results{detections[0], detections[2], detections[3], detections[1]}; + + const auto default_device = controller->GetDefaultDevice(); + + for (std::size_t i = 0; i != results.size(); ++i) + { + ciface::Core::DeviceQualifier device_qualifier; + device_qualifier.FromDevice(results[i].device.get()); + + stick->controls[i]->control_ref->SetExpression(ciface::MappingCommon::GetExpressionForControl( + results[i].input->GetName(), device_qualifier, default_device, + ciface::MappingCommon::Quote::On)); + + controller->UpdateSingleControlReference(g_controller_interface, + stick->controls[i]->control_ref.get()); + } + + controller->GetConfig()->GenerateControllerTextures(); + + return true; +} + +CalibrationBuilder::CalibrationBuilder(std::optional center) + : m_calibration_data(ControllerEmu::ReshapableInput::CALIBRATION_SAMPLE_COUNT, 0.0), + m_center{center} +{ +} + +void CalibrationBuilder::Update(Common::DVec2 point) +{ + if (!m_center.has_value()) + m_center = point; + + const auto new_point = point - *m_center; + ControllerEmu::ReshapableInput::UpdateCalibrationData(m_calibration_data, m_prev_point, + new_point); + m_prev_point = new_point; +} + +bool CalibrationBuilder::IsCalibrationDataSensible() const +{ + // Even the GC controller's small range would pass this test. + constexpr double REASONABLE_AVERAGE_RADIUS = 0.6; + + // Test that the average input radius is not below a threshold. + // This will make sure the user has actually moved their stick from neutral. + + MathUtil::RunningVariance stats; + + for (const auto x : m_calibration_data) + stats.Push(x); + + if (stats.Mean() < REASONABLE_AVERAGE_RADIUS) + return false; + + // Test that the standard deviation is below a threshold. + // This will make sure the user has not just filled in one side of their input. + + // Approx. deviation of a square input gate, anything much more than that would be unusual. + constexpr double REASONABLE_DEVIATION = 0.14; + + return stats.StandardDeviation() < REASONABLE_DEVIATION; +} + +ControlState CalibrationBuilder::GetCalibrationRadiusAtAngle(double angle) const +{ + return ControllerEmu::ReshapableInput::GetCalibrationDataRadiusAtAngle(m_calibration_data, angle); +} + +void CalibrationBuilder::ApplyResults(ControllerEmu::ReshapableInput* stick) +{ + stick->SetCenter(GetCenter()); + stick->SetCalibrationData(std::move(m_calibration_data)); +} + +Common::DVec2 CalibrationBuilder::GetCenter() const +{ + return m_center.value_or(Common::DVec2{}); +} + } // namespace ciface::MappingCommon diff --git a/Source/Core/InputCommon/ControllerInterface/MappingCommon.h b/Source/Core/InputCommon/ControllerInterface/MappingCommon.h index 62c1a8f281..a72549c458 100644 --- a/Source/Core/InputCommon/ControllerInterface/MappingCommon.h +++ b/Source/Core/InputCommon/ControllerInterface/MappingCommon.h @@ -5,8 +5,15 @@ #include +#include "Common/Matrix.h" +#include "InputCommon/ControllerEmu/StickGate.h" #include "InputCommon/ControllerInterface/CoreDevice.h" +namespace ControllerEmu +{ +class EmulatedController; +} // namespace ControllerEmu + namespace ciface::MappingCommon { enum class Quote @@ -27,4 +34,73 @@ void RemoveSpuriousTriggerCombinations(Core::InputDetector::Results*); void RemoveDetectionsAfterTimePoint(Core::InputDetector::Results*, Clock::time_point after); bool ContainsCompleteDetection(const Core::InputDetector::Results&); +// class for detecting four directional input mappings in sequence. +class ReshapableInputMapper +{ +public: + // Four cardinal directions. + static constexpr std::size_t REQUIRED_INPUT_COUNT = 4; + + // Caller should hold the "StateLock". + ReshapableInputMapper(const Core::DeviceContainer& container, + std::span device_strings); + + // Reads inputs and updates internal state. + // Returns true if an input was detected in this call. + // (useful for UI animation) + // Caller should hold the "StateLock". + bool Update(); + + // A counter-clockwise angle in radians for the currently desired input direction. + // Used for a graphical indicator in the UI. + // 0 == East + float GetCurrentAngle() const; + + // True if all four directions have been detected or the timer expired. + bool IsComplete() const; + + // Returns true if "analog" inputs were detected and calibration should be performed. + // Must use *before* ApplyResults. + bool IsCalibrationNeeded() const; + + // Use when IsComplete returns true. + // Updates the mappings on the provided ReshapableInput. + // Caller should hold the "StateLock". + bool ApplyResults(ControllerEmu::EmulatedController*, ControllerEmu::ReshapableInput* stick); + +private: + Core::InputDetector m_input_detector; +}; + +class CalibrationBuilder +{ +public: + // Provide nullopt if you want to calibrate the center on first Update. + explicit CalibrationBuilder(std::optional center = Common::DVec2{}); + + // Updates the calibration data using the provided point and the previous point. + void Update(Common::DVec2 point); + + // Returns true when the calibration data seems to be reasonably filled in. + // Used to update the UI to encourage the user to click the "Finish" button. + bool IsCalibrationDataSensible() const; + + // Grabs the calibration value at the provided angle. + // Used to render the calibration in the UI while it's in progress. + ControlState GetCalibrationRadiusAtAngle(double angle) const; + + // Sets the calibration data of the provided ReshapableInput. + // Caller should hold the "StateLock". + void ApplyResults(ControllerEmu::ReshapableInput* stick); + + Common::DVec2 GetCenter() const; + +private: + ControllerEmu::ReshapableInput::CalibrationData m_calibration_data; + + std::optional m_center = std::nullopt; + + Common::DVec2 m_prev_point{}; +}; + } // namespace ciface::MappingCommon