diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp index a09b41e5db..e0b44a2895 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp @@ -14,126 +14,47 @@ #include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControllerEmu/Control/Control.h" -#include "InputCommon/ControllerEmu/ControlGroup/AnalogStick.h" +#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h" +#include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h" #include "InputCommon/ControllerEmu/Setting/NumericSetting.h" #include "InputCommon/ControllerInterface/Device.h" #include "DolphinQt/Settings.h" +// Color constants to keep things looking consistent: +// TODO: could we maybe query theme colors from Qt for the bounding box? +const QColor BBOX_PEN_COLOR = Qt::darkGray; +const QColor BBOX_BRUSH_COLOR = Qt::white; + +const QColor RAW_INPUT_COLOR = Qt::darkGray; +const QColor ADJ_INPUT_COLOR = Qt::red; +const QPen INPUT_SHAPE_PEN(RAW_INPUT_COLOR, 1.0, Qt::DashLine); + +const QColor DEADZONE_COLOR = Qt::darkGray; +const QBrush DEADZONE_BRUSH(DEADZONE_COLOR, Qt::BDiagPattern); + +const QColor TEXT_COLOR = Qt::darkGray; +// Text color that is visible atop ADJ_INPUT_COLOR: +const QColor TEXT_ALT_COLOR = Qt::white; + +const QColor STICK_GATE_COLOR = Qt::lightGray; +const QColor C_STICK_GATE_COLOR = Qt::yellow; +const QColor CURSOR_TV_COLOR = 0xaed6f1; +const QColor TILT_GATE_COLOR = 0xa2d9ce; + +constexpr int INPUT_DOT_RADIUS = 2; + MappingIndicator::MappingIndicator(ControllerEmu::ControlGroup* group) : m_group(group) { setMinimumHeight(128); - switch (m_group->type) - { - case ControllerEmu::GroupType::Cursor: - BindCursorControls(false); - break; - case ControllerEmu::GroupType::Stick: - // Nothing needed: - break; - case ControllerEmu::GroupType::Tilt: - BindCursorControls(true); - break; - case ControllerEmu::GroupType::MixedTriggers: - BindMixedTriggersControls(); - break; - default: - break; - } - m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, [this] { repaint(); }); m_timer->start(1000 / 30); } -void MappingIndicator::BindCursorControls(bool tilt) +namespace { - m_cursor_up = m_group->controls[0]->control_ref.get(); - m_cursor_down = m_group->controls[1]->control_ref.get(); - m_cursor_left = m_group->controls[2]->control_ref.get(); - m_cursor_right = m_group->controls[3]->control_ref.get(); - - if (!tilt) - { - m_cursor_forward = m_group->controls[4]->control_ref.get(); - m_cursor_backward = m_group->controls[5]->control_ref.get(); - - m_cursor_center = m_group->numeric_settings[0].get(); - m_cursor_width = m_group->numeric_settings[1].get(); - m_cursor_height = m_group->numeric_settings[2].get(); - m_cursor_deadzone = m_group->numeric_settings[3].get(); - } - else - { - m_cursor_deadzone = m_group->numeric_settings[0].get(); - } -} - -void MappingIndicator::BindMixedTriggersControls() -{ - m_mixed_triggers_l_button = m_group->controls[0]->control_ref.get(); - m_mixed_triggers_r_button = m_group->controls[1]->control_ref.get(); - m_mixed_triggers_l_analog = m_group->controls[2]->control_ref.get(); - m_mixed_triggers_r_analog = m_group->controls[3]->control_ref.get(); - - m_mixed_triggers_threshold = m_group->numeric_settings[0].get(); -} - -static ControlState PollControlState(ControlReference* ref) -{ - Settings::Instance().SetControllerStateNeeded(true); - - auto state = ref->State(); - - Settings::Instance().SetControllerStateNeeded(false); - - if (state != 0) - return state; - else - return 0; -} - -void MappingIndicator::DrawCursor(bool tilt) -{ - float centerx = width() / 2., centery = height() / 2.; - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing, true); - p.setRenderHint(QPainter::SmoothPixmapTransform, true); - - float width = 64, height = 64; - float deadzone = m_cursor_deadzone->GetValue() * 48; - - if (!tilt) - { - float depth = centery - PollControlState(m_cursor_forward) * this->height() / 2.5 + - PollControlState(m_cursor_backward) * this->height() / 2.5; - - p.fillRect(0, depth, this->width(), 4, Qt::gray); - - width *= m_cursor_width->GetValue(); - height *= m_cursor_height->GetValue(); - } - - float curx = centerx - 4 - std::min(PollControlState(m_cursor_left), 0.5) * width + - std::min(PollControlState(m_cursor_right), 0.5) * width, - cury = centery - 4 - std::min(PollControlState(m_cursor_up), 0.5) * height + - std::min(PollControlState(m_cursor_down), 0.5) * height; - - // Draw background - p.setBrush(Qt::white); - p.setPen(Qt::black); - p.drawRect(centerx - (width / 2), centery - (height / 2), width, height); - - // Draw deadzone - p.setBrush(Qt::lightGray); - p.drawEllipse(centerx - (deadzone / 2), centery - (deadzone / 2), deadzone, deadzone); - - // Draw cursor - p.fillRect(curx, cury, 8, 8, Qt::red); -} - // Constructs a polygon by querying a radius at varying angles: template QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale) @@ -154,34 +75,138 @@ QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale) return shape; } +} // namespace -void MappingIndicator::DrawStick() +void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor) { - // Make the c-stick yellow: - const bool is_c_stick = m_group->name == "C-Stick"; - const QColor gate_brush_color = is_c_stick ? Qt::yellow : Qt::lightGray; - const QColor gate_pen_color = gate_brush_color.darker(125); - - auto& stick = *static_cast(m_group); + const QColor tv_brush_color = CURSOR_TV_COLOR; + const QColor tv_pen_color = tv_brush_color.darker(125); // TODO: This SetControllerStateNeeded interface leaks input into the game // We should probably hold the mutex for UI updates. Settings::Instance().SetControllerStateNeeded(true); - const auto raw_coord = stick.GetState(false); - const auto adj_coord = stick.GetState(true); + const auto raw_coord = cursor.GetState(false); + const auto adj_coord = cursor.GetState(true); Settings::Instance().SetControllerStateNeeded(false); // Bounding box size: const double scale = height() / 2.5; - const float dot_radius = 2; + QPainter p(this); + p.translate(width() / 2, height() / 2); + + // Bounding box. + p.setBrush(BBOX_BRUSH_COLOR); + p.setPen(BBOX_PEN_COLOR); + p.drawRect(-scale - 1, -scale - 1, scale * 2 + 1, scale * 2 + 1); + + // UI y-axis is opposite that of stick. + p.scale(1.0, -1.0); + + // Enable AA after drawing bounding box. + p.setRenderHint(QPainter::Antialiasing, true); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + + // Deadzone for Z (forward/backward): + const double deadzone = cursor.numeric_settings[cursor.SETTING_DEADZONE]->GetValue(); + if (deadzone > 0.0) + { + p.setPen(DEADZONE_COLOR); + p.setBrush(DEADZONE_BRUSH); + p.drawRect(QRectF(-scale, -deadzone * scale, scale * 2, deadzone * scale * 2)); + } + + // Raw Z: + p.setPen(Qt::NoPen); + p.setBrush(RAW_INPUT_COLOR); + p.drawRect( + QRectF(-scale, raw_coord.z * scale - INPUT_DOT_RADIUS / 2, scale * 2, INPUT_DOT_RADIUS)); + + // Adjusted Z (if not hidden): + if (adj_coord.z && adj_coord.x < 10000) + { + p.setBrush(ADJ_INPUT_COLOR); + p.drawRect( + QRectF(-scale, adj_coord.z * scale - INPUT_DOT_RADIUS / 2, scale * 2, INPUT_DOT_RADIUS)); + } + + // TV screen or whatever you want to call this: + constexpr double tv_scale = 0.75; + constexpr double center_scale = 2.0 / 3.0; + + const double tv_center = (cursor.numeric_settings[cursor.SETTING_CENTER]->GetValue() - 0.5); + const double tv_width = cursor.numeric_settings[cursor.SETTING_WIDTH]->GetValue(); + const double tv_height = cursor.numeric_settings[cursor.SETTING_HEIGHT]->GetValue(); + + p.setPen(tv_pen_color); + p.setBrush(tv_brush_color); + auto gate_polygon = GetPolygonFromRadiusGetter( + [&cursor](double ang) { return cursor.GetGateRadiusAtAngle(ang); }, scale); + for (auto& pt : gate_polygon) + { + pt = {pt.x() * tv_width, pt.y() * tv_height + tv_center * center_scale * scale}; + pt *= tv_scale; + } + p.drawPolygon(gate_polygon); + + // Deadzone. + p.setPen(DEADZONE_COLOR); + p.setBrush(DEADZONE_BRUSH); + p.drawPolygon(GetPolygonFromRadiusGetter( + [&cursor](double ang) { return cursor.GetDeadzoneRadiusAtAngle(ang); }, scale)); + + // Input shape. + p.setPen(INPUT_SHAPE_PEN); + p.setBrush(Qt::NoBrush); + p.drawPolygon(GetPolygonFromRadiusGetter( + [&cursor](double ang) { return cursor.GetInputRadiusAtAngle(ang); }, scale)); + + // Raw stick position. + p.setPen(Qt::NoPen); + p.setBrush(RAW_INPUT_COLOR); + p.drawEllipse(QPointF{raw_coord.x, raw_coord.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + + // Adjusted cursor position (if not hidden): + if (adj_coord.x < 10000) + { + p.setPen(Qt::NoPen); + p.setBrush(ADJ_INPUT_COLOR); + const QPointF pt(adj_coord.x / 2.0, (adj_coord.y - tv_center) / 2.0 + tv_center * center_scale); + p.drawEllipse(pt * scale * tv_scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + } +} + +void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick) +{ + // Some hacks for pretty colors: + const bool is_c_stick = m_group->name == "C-Stick"; + const bool is_tilt = m_group->name == "Tilt"; + + QColor gate_brush_color = STICK_GATE_COLOR; + + if (is_c_stick) + gate_brush_color = C_STICK_GATE_COLOR; + else if (is_tilt) + gate_brush_color = TILT_GATE_COLOR; + + const QColor gate_pen_color = gate_brush_color.darker(125); + + // TODO: This SetControllerStateNeeded interface leaks input into the game + // We should probably hold the mutex for UI updates. + Settings::Instance().SetControllerStateNeeded(true); + const auto raw_coord = stick.GetReshapableState(false); + const auto adj_coord = stick.GetReshapableState(true); + Settings::Instance().SetControllerStateNeeded(false); + + // Bounding box size: + const double scale = height() / 2.5; QPainter p(this); p.translate(width() / 2, height() / 2); // Bounding box. - p.setBrush(Qt::white); - p.setPen(Qt::gray); + p.setBrush(BBOX_BRUSH_COLOR); + p.setPen(BBOX_PEN_COLOR); p.drawRect(-scale - 1, -scale - 1, scale * 2 + 1, scale * 2 + 1); // UI y-axis is opposite that of stick. @@ -198,76 +223,126 @@ void MappingIndicator::DrawStick() [&stick](double ang) { return stick.GetGateRadiusAtAngle(ang); }, scale)); // Deadzone. - p.setPen(Qt::darkGray); - p.setBrush(QBrush(Qt::darkGray, Qt::BDiagPattern)); + p.setPen(DEADZONE_COLOR); + p.setBrush(DEADZONE_BRUSH); p.drawPolygon(GetPolygonFromRadiusGetter( [&stick](double ang) { return stick.GetDeadzoneRadiusAtAngle(ang); }, scale)); // Input shape. - p.setPen(QPen(Qt::darkGray, 1.0, Qt::DashLine)); + p.setPen(INPUT_SHAPE_PEN); p.setBrush(Qt::NoBrush); p.drawPolygon(GetPolygonFromRadiusGetter( [&stick](double ang) { return stick.GetInputRadiusAtAngle(ang); }, scale)); // Raw stick position. p.setPen(Qt::NoPen); - p.setBrush(Qt::darkGray); - p.drawEllipse(QPointF{raw_coord.x, raw_coord.y} * scale, dot_radius, dot_radius); + p.setBrush(RAW_INPUT_COLOR); + p.drawEllipse(QPointF{raw_coord.x, raw_coord.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); // Adjusted stick position. if (adj_coord.x || adj_coord.y) { p.setPen(Qt::NoPen); - p.setBrush(Qt::red); - p.drawEllipse(QPointF{adj_coord.x, adj_coord.y} * scale, dot_radius, dot_radius); + p.setBrush(ADJ_INPUT_COLOR); + p.drawEllipse(QPointF{adj_coord.x, adj_coord.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); } } void MappingIndicator::DrawMixedTriggers() { QPainter p(this); - p.setRenderHint(QPainter::Antialiasing, true); p.setRenderHint(QPainter::TextAntialiasing, true); - p.setRenderHint(QPainter::SmoothPixmapTransform, true); - // Polled values - double r_analog = PollControlState(m_mixed_triggers_r_analog); - double r_button = PollControlState(m_mixed_triggers_r_button); - double l_analog = PollControlState(m_mixed_triggers_l_analog); - double l_button = PollControlState(m_mixed_triggers_l_button); - double threshold = m_mixed_triggers_threshold->GetValue(); + const auto& triggers = *static_cast(m_group); + const ControlState threshold = triggers.GetThreshold(); + const ControlState deadzone = triggers.GetDeadzone(); - double r_bar_percent = r_analog; - double l_bar_percent = l_analog; + // MixedTriggers interface is a bit ugly: + constexpr int TRIGGER_COUNT = 2; + std::array raw_analog_state; + std::array adj_analog_state; + const std::array button_masks = {0x1, 0x2}; + u16 button_state = 0; - if ((r_button && r_button != r_analog) || (r_button == r_analog && r_analog > threshold)) - r_bar_percent = 1; - else - r_bar_percent *= 0.8; + Settings::Instance().SetControllerStateNeeded(true); + triggers.GetState(&button_state, button_masks.data(), raw_analog_state.data(), false); + triggers.GetState(&button_state, button_masks.data(), adj_analog_state.data(), true); + Settings::Instance().SetControllerStateNeeded(false); - if ((l_button && l_button != l_analog) || (l_button == l_analog && l_analog > threshold)) - l_bar_percent = 1; - else - l_bar_percent *= 0.8; + // Rectangle sizes: + const int trigger_height = 32; + const int trigger_width = width() - 1; + const int trigger_button_width = 32; + const int trigger_analog_width = trigger_width - trigger_button_width; - p.fillRect(0, 0, width(), 64, Qt::black); + // Bounding box background: + p.setPen(Qt::NoPen); + p.setBrush(BBOX_BRUSH_COLOR); + p.drawRect(0, 0, trigger_width, trigger_height * TRIGGER_COUNT); - p.fillRect(0, 0, l_bar_percent * width(), 32, Qt::red); - p.fillRect(0, 32, r_bar_percent * width(), 32, Qt::red); + for (int t = 0; t != TRIGGER_COUNT; ++t) + { + const double raw_analog = raw_analog_state[t]; + const double adj_analog = adj_analog_state[t]; + const bool trigger_button = button_state & button_masks[t]; + auto const analog_name = QString::fromStdString(triggers.controls[TRIGGER_COUNT + t]->ui_name); + auto const button_name = QString::fromStdString(triggers.controls[t]->ui_name); - p.setPen(Qt::white); - p.drawLine(width() * 0.8, 0, width() * 0.8, 63); - p.drawLine(0, 32, width(), 32); + const QRectF trigger_rect(0, 0, trigger_width, trigger_height); - p.setPen(Qt::green); - p.drawLine(width() * 0.8 * threshold, 0, width() * 0.8 * threshold, 63); + const QRectF analog_rect(0, 0, trigger_analog_width, trigger_height); - p.setBrush(Qt::black); - p.setPen(Qt::white); - p.drawText(width() * 0.225, 20, tr("L-Analog")); - p.drawText(width() * 0.8 + 16, 20, tr("L")); - p.drawText(width() * 0.225, 52, tr("R-Analog")); - p.drawText(width() * 0.8 + 16, 52, tr("R")); + // Unactivated analog text: + p.setPen(TEXT_COLOR); + p.drawText(analog_rect, Qt::AlignCenter, analog_name); + + const QRectF adj_analog_rect(0, 0, adj_analog * trigger_analog_width, trigger_height); + + // Trigger analog: + p.setPen(Qt::NoPen); + p.setBrush(RAW_INPUT_COLOR); + p.drawEllipse(QPoint(raw_analog * trigger_analog_width, trigger_height - INPUT_DOT_RADIUS), + INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + p.setBrush(ADJ_INPUT_COLOR); + p.drawRect(adj_analog_rect); + + // Deadzone: + p.setPen(DEADZONE_COLOR); + p.setBrush(DEADZONE_BRUSH); + p.drawRect(0, 0, trigger_analog_width * deadzone, trigger_height); + + // Threshold setting: + const int threshold_x = trigger_analog_width * threshold; + p.setPen(INPUT_SHAPE_PEN); + p.drawLine(threshold_x, 0, threshold_x, trigger_height); + + const QRectF button_rect(trigger_analog_width, 0, trigger_button_width, trigger_height); + + // Trigger button: + p.setPen(BBOX_PEN_COLOR); + p.setBrush(trigger_button ? ADJ_INPUT_COLOR : BBOX_BRUSH_COLOR); + p.drawRect(button_rect); + + // Bounding box outline: + p.setPen(BBOX_PEN_COLOR); + p.setBrush(Qt::NoBrush); + p.drawRect(trigger_rect); + + // Button text: + p.setPen(TEXT_COLOR); + p.setPen(trigger_button ? TEXT_ALT_COLOR : TEXT_COLOR); + p.drawText(button_rect, Qt::AlignCenter, button_name); + + // Activated analog text: + p.setPen(TEXT_ALT_COLOR); + p.setClipping(true); + p.setClipRect(adj_analog_rect); + p.drawText(analog_rect, Qt::AlignCenter, analog_name); + p.setClipping(false); + + // Move down for next trigger: + p.translate(0.0, trigger_height); + } } void MappingIndicator::paintEvent(QPaintEvent*) @@ -275,13 +350,11 @@ void MappingIndicator::paintEvent(QPaintEvent*) switch (m_group->type) { case ControllerEmu::GroupType::Cursor: - DrawCursor(false); - break; - case ControllerEmu::GroupType::Tilt: - DrawCursor(true); + DrawCursor(*static_cast(m_group)); break; case ControllerEmu::GroupType::Stick: - DrawStick(); + case ControllerEmu::GroupType::Tilt: + DrawReshapableInput(*static_cast(m_group)); break; case ControllerEmu::GroupType::MixedTriggers: DrawMixedTriggers(); diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h index f3bff86798..3a863ec788 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h @@ -10,50 +10,27 @@ namespace ControllerEmu { class Control; class ControlGroup; +class Cursor; class NumericSetting; -} +class ReshapableInput; +} // namespace ControllerEmu class QPaintEvent; class QTimer; -class ControlReference; - class MappingIndicator : public QWidget { public: explicit MappingIndicator(ControllerEmu::ControlGroup* group); private: - void BindCursorControls(bool tilt); - void BindMixedTriggersControls(); - - void DrawCursor(bool tilt); - void DrawStick(); + void DrawCursor(ControllerEmu::Cursor& cursor); + void DrawReshapableInput(ControllerEmu::ReshapableInput& stick); void DrawMixedTriggers(); void paintEvent(QPaintEvent*) override; + ControllerEmu::ControlGroup* m_group; - // Cursor settings - ControlReference* m_cursor_up; - ControlReference* m_cursor_down; - ControlReference* m_cursor_left; - ControlReference* m_cursor_right; - ControlReference* m_cursor_forward; - ControlReference* m_cursor_backward; - - ControllerEmu::NumericSetting* m_cursor_center; - ControllerEmu::NumericSetting* m_cursor_width; - ControllerEmu::NumericSetting* m_cursor_height; - ControllerEmu::NumericSetting* m_cursor_deadzone; - - // Triggers settings - ControlReference* m_mixed_triggers_r_analog; - ControlReference* m_mixed_triggers_r_button; - ControlReference* m_mixed_triggers_l_analog; - ControlReference* m_mixed_triggers_l_button; - - ControllerEmu::NumericSetting* m_mixed_triggers_threshold; - QTimer* m_timer; }; diff --git a/Source/Core/DolphinQt/Config/Mapping/WiimoteEmuExtension.cpp b/Source/Core/DolphinQt/Config/Mapping/WiimoteEmuExtension.cpp index 7d0a402f29..6110fac43b 100644 --- a/Source/Core/DolphinQt/Config/Mapping/WiimoteEmuExtension.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/WiimoteEmuExtension.cpp @@ -54,14 +54,14 @@ void WiimoteEmuExtension::CreateDrumsLayout() auto* hbox = new QHBoxLayout(); m_drums_box = new QGroupBox(tr("Drums"), this); - hbox->addWidget(CreateGroupBox( - tr("Buttons"), Wiimote::GetDrumsGroup(GetPort(), WiimoteEmu::DrumsGroup::Buttons))); + hbox->addWidget(CreateGroupBox(tr("Stick"), + Wiimote::GetDrumsGroup(GetPort(), WiimoteEmu::DrumsGroup::Stick))); auto* vbox = new QVBoxLayout(); vbox->addWidget( CreateGroupBox(tr("Pads"), Wiimote::GetDrumsGroup(GetPort(), WiimoteEmu::DrumsGroup::Pads))); - vbox->addWidget(CreateGroupBox(tr("Stick"), - Wiimote::GetDrumsGroup(GetPort(), WiimoteEmu::DrumsGroup::Stick))); + vbox->addWidget(CreateGroupBox( + tr("Buttons"), Wiimote::GetDrumsGroup(GetPort(), WiimoteEmu::DrumsGroup::Buttons))); hbox->addLayout(vbox); m_drums_box->setLayout(hbox); @@ -107,12 +107,8 @@ void WiimoteEmuExtension::CreateGuitarLayout() m_guitar_box = new QGroupBox(tr("Guitar"), this); auto* vbox = new QVBoxLayout(); - vbox->addWidget(CreateGroupBox( - tr("Buttons"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::Buttons))); vbox->addWidget(CreateGroupBox( tr("Stick"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::Stick))); - vbox->addWidget(CreateGroupBox( - tr("Slider Bar"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::SliderBar))); hbox->addLayout(vbox); auto* vbox2 = new QVBoxLayout(); @@ -120,10 +116,17 @@ void WiimoteEmuExtension::CreateGuitarLayout() tr("Strum"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::Strum))); vbox2->addWidget(CreateGroupBox( tr("Frets"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::Frets))); - vbox2->addWidget(CreateGroupBox( - tr("Whammy"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::Whammy))); hbox->addLayout(vbox2); + auto* vbox3 = new QVBoxLayout(); + vbox3->addWidget(CreateGroupBox( + tr("Buttons"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::Buttons))); + vbox3->addWidget(CreateGroupBox( + tr("Whammy"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::Whammy))); + vbox3->addWidget(CreateGroupBox( + tr("Slider Bar"), Wiimote::GetGuitarGroup(GetPort(), WiimoteEmu::GuitarGroup::SliderBar))); + hbox->addLayout(vbox3); + m_guitar_box->setLayout(hbox); } @@ -134,24 +137,27 @@ void WiimoteEmuExtension::CreateTurntableLayout() hbox->addWidget(CreateGroupBox( tr("Stick"), Wiimote::GetTurntableGroup(GetPort(), WiimoteEmu::TurntableGroup::Stick))); - hbox->addWidget(CreateGroupBox( - tr("Buttons"), Wiimote::GetTurntableGroup(GetPort(), WiimoteEmu::TurntableGroup::Buttons))); auto* vbox = new QVBoxLayout(); + vbox->addWidget(CreateGroupBox( + tr("Buttons"), Wiimote::GetTurntableGroup(GetPort(), WiimoteEmu::TurntableGroup::Buttons))); vbox->addWidget(CreateGroupBox( tr("Effect"), Wiimote::GetTurntableGroup(GetPort(), WiimoteEmu::TurntableGroup::EffectDial))); - vbox->addWidget( + hbox->addLayout(vbox); + + auto* vbox2 = new QVBoxLayout(); + vbox2->addWidget( // i18n: "Table" refers to a turntable CreateGroupBox(tr("Left Table"), Wiimote::GetTurntableGroup(GetPort(), WiimoteEmu::TurntableGroup::LeftTable))); - vbox->addWidget(CreateGroupBox( + vbox2->addWidget(CreateGroupBox( // i18n: "Table" refers to a turntable tr("Right Table"), Wiimote::GetTurntableGroup(GetPort(), WiimoteEmu::TurntableGroup::RightTable))); - vbox->addWidget( + vbox2->addWidget( CreateGroupBox(tr("Crossfade"), Wiimote::GetTurntableGroup(GetPort(), WiimoteEmu::TurntableGroup::Crossfade))); - hbox->addLayout(vbox); + hbox->addLayout(vbox2); m_turntable_box->setLayout(hbox); } diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp index 9db29909fd..0cd5da6ba0 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp @@ -24,67 +24,36 @@ AnalogStick::AnalogStick(const char* const name_, std::unique_ptr&& s AnalogStick::AnalogStick(const char* const name_, const char* const ui_name_, std::unique_ptr&& stick_gate) - : ControlGroup(name_, ui_name_, GroupType::Stick), m_stick_gate(std::move(stick_gate)) + : ReshapableInput(name_, ui_name_, GroupType::Stick), m_stick_gate(std::move(stick_gate)) { for (auto& named_direction : named_directions) controls.emplace_back(std::make_unique(Translate, named_direction)); controls.emplace_back(std::make_unique(Translate, _trans("Modifier"))); - // Set default input radius to that of the gate radius (no resizing) - // Allow radius greater than 1.0 for definitions of rounded squares - // This is ideal for Xbox controllers (and probably others) - numeric_settings.emplace_back( - std::make_unique(_trans("Input Radius"), GetGateRadiusAtAngle(0.0), 0, 140)); - // Set default input shape to an octagon (no reshaping) - numeric_settings.emplace_back( - std::make_unique(_trans("Input Shape"), 0.0, 0, 50)); - numeric_settings.emplace_back(std::make_unique(_trans("Dead Zone"), 0, 0, 50)); + // Default input radius to that of the gate radius (no resizing) + // Default input shape to an octagon (no reshaping) + // Max deadzone to 50% + AddReshapingSettings(GetGateRadiusAtAngle(0.0), 0.0, 50); } -AnalogStick::StateData AnalogStick::GetState(bool adjusted) +AnalogStick::ReshapeData AnalogStick::GetReshapableState(bool adjusted) { - ControlState y = controls[0]->control_ref->State() - controls[1]->control_ref->State(); - ControlState x = controls[3]->control_ref->State() - controls[2]->control_ref->State(); + const ControlState y = controls[0]->control_ref->State() - controls[1]->control_ref->State(); + const ControlState x = controls[3]->control_ref->State() - controls[2]->control_ref->State(); // Return raw values. (used in UI) if (!adjusted) return {x, y}; - // TODO: make the AtAngle functions work with negative angles: - const ControlState ang = std::atan2(y, x) + MathUtil::TAU; - - const ControlState gate_max_dist = GetGateRadiusAtAngle(ang); - const ControlState input_max_dist = GetInputRadiusAtAngle(ang); - - // If input radius is zero we apply no scaling. - // This is useful when mapping native controllers without knowing intimate radius details. - const ControlState max_dist = input_max_dist ? input_max_dist : gate_max_dist; - - ControlState dist = std::sqrt(x * x + y * y) / max_dist; - - // If the modifier is pressed, scale the distance by the modifier's value. - // This is affected by the modifier's "range" setting which defaults to 50%. const ControlState modifier = controls[4]->control_ref->State(); - if (modifier) - { - // TODO: Modifier's range setting gets reset to 100% when the clear button is clicked. - // This causes the modifier to not behave how a user might suspect. - // Retaining the old scale-by-50% behavior until range is fixed to clear to 50%. - dist *= 0.5; - // dist *= modifier; - } - // Apply deadzone as a percentage of the user-defined radius/shape: - const ControlState deadzone = GetDeadzoneRadiusAtAngle(ang); - dist = std::max(0.0, dist - deadzone) / (1.0 - deadzone); + return Reshape(x, y, modifier); +} - // Scale to the gate shape/radius: - dist = dist *= gate_max_dist; - - x = MathUtil::Clamp(std::cos(ang) * dist, -1.0, 1.0); - y = MathUtil::Clamp(std::sin(ang) * dist, -1.0, 1.0); - return {x, y}; +AnalogStick::StateData AnalogStick::GetState() +{ + return GetReshapableState(true); } ControlState AnalogStick::GetGateRadiusAtAngle(double ang) const @@ -92,37 +61,6 @@ ControlState AnalogStick::GetGateRadiusAtAngle(double ang) const return m_stick_gate->GetRadiusAtAngle(ang); } -ControlState AnalogStick::GetDeadzoneRadiusAtAngle(double ang) const -{ - return CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_DEADZONE]->GetValue(); -} - -ControlState AnalogStick::GetInputRadiusAtAngle(double ang) const -{ - const ControlState radius = - CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_INPUT_RADIUS]->GetValue(); - // Clamp within the -1 to +1 square as input radius may be greater than 1.0: - return std::min(radius, SquareStickGate(1).GetRadiusAtAngle(ang)); -} - -ControlState AnalogStick::CalculateInputShapeRadiusAtAngle(double ang) const -{ - const auto shape = numeric_settings[SETTING_INPUT_SHAPE]->GetValue() * 4.0; - - if (shape < 1.0) - { - // Between 0 and 25 return a shape between octagon and circle - const auto amt = shape; - return OctagonStickGate(1).GetRadiusAtAngle(ang) * (1 - amt) + amt; - } - else - { - // Between 25 and 50 return a shape between circle and square - const auto amt = shape - 1.0; - return (1 - amt) + SquareStickGate(1).GetRadiusAtAngle(ang) * amt; - } -} - OctagonAnalogStick::OctagonAnalogStick(const char* name, ControlState gate_radius) : OctagonAnalogStick(name, name, gate_radius) { diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h index 2351189f74..cafb1dad51 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h @@ -10,35 +10,20 @@ namespace ControllerEmu { -class AnalogStick : public ControlGroup +class AnalogStick : public ReshapableInput { public: - enum - { - SETTING_INPUT_RADIUS, - SETTING_INPUT_SHAPE, - SETTING_DEADZONE, - }; - - struct StateData - { - ControlState x{}; - ControlState y{}; - }; + using StateData = ReshapeData; AnalogStick(const char* name, std::unique_ptr&& stick_gate); AnalogStick(const char* name, const char* ui_name, std::unique_ptr&& stick_gate); - StateData GetState(bool adjusted = true); + ReshapeData GetReshapableState(bool adjusted) final override; + ControlState GetGateRadiusAtAngle(double ang) const override; - // Angle is in radians and should be non-negative - ControlState GetGateRadiusAtAngle(double ang) const; - ControlState GetDeadzoneRadiusAtAngle(double ang) const; - ControlState GetInputRadiusAtAngle(double ang) const; + StateData GetState(); private: - ControlState CalculateInputShapeRadiusAtAngle(double ang) const; - std::unique_ptr m_stick_gate; }; diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.cpp index b0633ec6a5..0af6efc734 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.cpp @@ -21,7 +21,8 @@ namespace ControllerEmu { -Cursor::Cursor(const std::string& name_) : ControlGroup(name_, GroupType::Cursor) +Cursor::Cursor(const std::string& name_) + : ReshapableInput(name_, name_, GroupType::Cursor), m_last_update(Clock::now()) { for (auto& named_direction : named_directions) controls.emplace_back(std::make_unique(Translate, named_direction)); @@ -31,89 +32,121 @@ Cursor::Cursor(const std::string& name_) : ControlGroup(name_, GroupType::Cursor controls.emplace_back(std::make_unique(Translate, _trans("Hide"))); controls.emplace_back(std::make_unique(Translate, _trans("Recenter"))); + // Default shape is a 1.0 square (no resizing/reshaping): + AddReshapingSettings(1.0, 0.5, 50); + numeric_settings.emplace_back(std::make_unique(_trans("Center"), 0.5)); numeric_settings.emplace_back(std::make_unique(_trans("Width"), 0.5)); numeric_settings.emplace_back(std::make_unique(_trans("Height"), 0.5)); - numeric_settings.emplace_back(std::make_unique(_trans("Dead Zone"), 0, 0, 20)); + boolean_settings.emplace_back(std::make_unique(_trans("Relative Input"), false)); boolean_settings.emplace_back(std::make_unique(_trans("Auto-Hide"), false)); } +Cursor::ReshapeData Cursor::GetReshapableState(bool adjusted) +{ + const ControlState y = controls[0]->control_ref->State() - controls[1]->control_ref->State(); + const ControlState x = controls[3]->control_ref->State() - controls[2]->control_ref->State(); + + // Return raw values. (used in UI) + if (!adjusted) + return {x, y}; + + return Reshape(x, y, 0.0); +} + +ControlState Cursor::GetGateRadiusAtAngle(double ang) const +{ + // TODO: Change this to 0.5 and adjust the math, + // so pointer doesn't have to be clamped to the configured width/height? + return SquareStickGate(1.0).GetRadiusAtAngle(ang); +} + Cursor::StateData Cursor::GetState(const bool adjusted) { - const ControlState zz = controls[4]->control_ref->State() - controls[5]->control_ref->State(); + ControlState z = controls[4]->control_ref->State() - controls[5]->control_ref->State(); - // silly being here - if (zz > m_state.z) - m_state.z = std::min(m_state.z + 0.1, zz); - else if (zz < m_state.z) - m_state.z = std::max(m_state.z - 0.1, zz); - - StateData result; - result.z = m_state.z; - - if (m_autohide_timer > -1) + if (!adjusted) { - --m_autohide_timer; + const auto raw_input = GetReshapableState(false); + + return {raw_input.x, raw_input.y, z}; } - ControlState yy = controls[0]->control_ref->State() - controls[1]->control_ref->State(); - ControlState xx = controls[3]->control_ref->State() - controls[2]->control_ref->State(); + const auto input = GetReshapableState(true); - const ControlState deadzone = numeric_settings[3]->GetValue(); + // TODO: Using system time is ugly. + // Kill this after state is moved into wiimote rather than this class. + const auto now = Clock::now(); + const auto ms_since_update = + std::chrono::duration_cast(now - m_last_update).count(); + m_last_update = now; - // reset auto-hide timer - if (std::abs(m_prev_xx - xx) > deadzone || std::abs(m_prev_yy - yy) > deadzone) - { - m_autohide_timer = TIMER_VALUE; - } + const double max_step = STEP_PER_SEC / 1000.0 * ms_since_update; + const double max_z_step = STEP_Z_PER_SEC / 1000.0 * ms_since_update; - // hide - const bool autohide = boolean_settings[1]->GetValue() && m_autohide_timer < 0; - if (controls[6]->control_ref->State() > 0.5 || autohide) + // Apply deadzone to z: + const ControlState deadzone = numeric_settings[SETTING_DEADZONE]->GetValue(); + z = std::copysign(std::max(0.0, std::abs(z) - deadzone) / (1.0 - deadzone), z); + + // Smooth out z movement: + // FYI: Not using relative input for Z. + m_state.z += MathUtil::Clamp(z - m_state.z, -max_z_step, max_z_step); + + // Relative input: + if (boolean_settings[0]->GetValue()) { - result.x = 10000; - result.y = 0; - } - else - { - // adjust cursor according to settings - if (adjusted) + // Recenter: + if (controls[7]->control_ref->State() > BUTTON_THRESHOLD) { - xx *= (numeric_settings[1]->GetValue() * 2); - yy *= (numeric_settings[2]->GetValue() * 2); - yy += (numeric_settings[0]->GetValue() - 0.5); - } - - // relative input - if (boolean_settings[0]->GetValue()) - { - // deadzone to avoid the cursor slowly drifting - if (std::abs(xx) > deadzone) - m_state.x = MathUtil::Clamp(m_state.x + xx * SPEED_MULTIPLIER, -1.0, 1.0); - if (std::abs(yy) > deadzone) - m_state.y = MathUtil::Clamp(m_state.y + yy * SPEED_MULTIPLIER, -1.0, 1.0); - - // recenter - if (controls[7]->control_ref->State() > 0.5) - { - m_state.x = 0.0; - m_state.y = 0.0; - } + m_state.x = 0.0; + m_state.y = 0.0; } else { - m_state.x = xx; - m_state.y = yy; + m_state.x = MathUtil::Clamp(m_state.x + input.x * max_step, -1.0, 1.0); + m_state.y = MathUtil::Clamp(m_state.y + input.y * max_step, -1.0, 1.0); } - - result.x = m_state.x; - result.y = m_state.y; + } + // Absolute input: + else + { + m_state.x = input.x; + m_state.y = input.y; } - m_prev_xx = xx; - m_prev_yy = yy; + StateData result = m_state; + + // Adjust cursor according to settings: + result.x *= (numeric_settings[SETTING_WIDTH]->GetValue() * 2); + result.y *= (numeric_settings[SETTING_HEIGHT]->GetValue() * 2); + result.y += (numeric_settings[SETTING_CENTER]->GetValue() - 0.5); + + const bool autohide = boolean_settings[1]->GetValue(); + + // Auto-hide timer: + // TODO: should Z movement reset this? + if (!autohide || std::abs(m_prev_result.x - result.x) > AUTO_HIDE_DEADZONE || + std::abs(m_prev_result.y - result.y) > AUTO_HIDE_DEADZONE) + { + m_auto_hide_timer = AUTO_HIDE_MS; + } + else if (m_auto_hide_timer) + { + m_auto_hide_timer -= std::min(ms_since_update, m_auto_hide_timer); + } + + m_prev_result = result; + + // If auto-hide time is up or hide button is held: + if (!m_auto_hide_timer || controls[6]->control_ref->State() > BUTTON_THRESHOLD) + { + // TODO: Use NaN or something: + result.x = 10000; + result.y = 0; + } return result; } + } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.h index b24cfdcba3..c66b2b896c 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.h @@ -4,13 +4,15 @@ #pragma once +#include #include -#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" + +#include "InputCommon/ControllerEmu/StickGate.h" #include "InputCommon/ControllerInterface/Device.h" namespace ControllerEmu { -class Cursor : public ControlGroup +class Cursor : public ReshapableInput { public: struct StateData @@ -20,22 +22,42 @@ public: ControlState z{}; }; + enum + { + SETTING_CENTER = ReshapableInput::SETTING_COUNT, + SETTING_WIDTH, + SETTING_HEIGHT, + }; + explicit Cursor(const std::string& name); - StateData GetState(bool adjusted = false); + ReshapeData GetReshapableState(bool adjusted) final override; + ControlState GetGateRadiusAtAngle(double ang) const override; + + StateData GetState(bool adjusted); private: // This is used to reduce the cursor speed for relative input // to something that makes sense with the default range. - static constexpr double SPEED_MULTIPLIER = 0.04; + static constexpr double STEP_PER_SEC = 0.04 * 200; - // Sets the length for the auto-hide timer - static constexpr int TIMER_VALUE = 500; + // Smooth out forward/backward movements: + static constexpr double STEP_Z_PER_SEC = 0.05 * 200; + static constexpr int AUTO_HIDE_MS = 2500; + static constexpr double AUTO_HIDE_DEADZONE = 0.001; + + static constexpr double BUTTON_THRESHOLD = 0.5; + + // Not adjusted by width/height/center: StateData m_state; - int m_autohide_timer = TIMER_VALUE; - ControlState m_prev_xx; - ControlState m_prev_yy; + // Adjusted: + StateData m_prev_result; + + int m_auto_hide_timer = AUTO_HIDE_MS; + + using Clock = std::chrono::steady_clock; + Clock::time_point m_last_update; }; } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/MixedTriggers.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/MixedTriggers.cpp index 8f26fde6fc..6ae4dfd668 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/MixedTriggers.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/MixedTriggers.cpp @@ -4,6 +4,7 @@ #include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h" +#include #include #include #include @@ -21,23 +22,53 @@ MixedTriggers::MixedTriggers(const std::string& name_) : ControlGroup(name_, GroupType::MixedTriggers) { numeric_settings.emplace_back(std::make_unique(_trans("Threshold"), 0.9)); + numeric_settings.emplace_back(std::make_unique(_trans("Dead Zone"), 0.0, 0, 25)); } -void MixedTriggers::GetState(u16* const digital, const u16* bitmasks, ControlState* analog) +void MixedTriggers::GetState(u16* const digital, const u16* bitmasks, ControlState* analog, + bool adjusted) const { - const size_t trigger_count = controls.size() / 2; + const ControlState threshold = numeric_settings[SETTING_THRESHOLD]->GetValue(); + ControlState deadzone = numeric_settings[SETTING_DEADZONE]->GetValue(); - for (size_t i = 0; i < trigger_count; ++i, ++bitmasks, ++analog) + // Return raw values. (used in UI) + if (!adjusted) { - if (controls[i]->control_ref->State() > numeric_settings[0]->GetValue()) // threshold + deadzone = 0.0; + } + + const int trigger_count = int(controls.size() / 2); + for (int i = 0; i != trigger_count; ++i) + { + ControlState button_value = controls[i]->control_ref->State(); + ControlState analog_value = controls[trigger_count + i]->control_ref->State(); + + // Apply deadzone: + analog_value = std::max(0.0, analog_value - deadzone) / (1.0 - deadzone); + button_value = std::max(0.0, button_value - deadzone) / (1.0 - deadzone); + + // Apply threshold: + if (button_value > threshold) { - *analog = 1.0; - *digital |= *bitmasks; - } - else - { - *analog = controls[i + trigger_count]->control_ref->State(); + // Fully activate analog: + analog_value = 1.0; + + // Activate button: + *digital |= bitmasks[i]; } + + analog[i] = analog_value; } } + +ControlState MixedTriggers::GetDeadzone() const +{ + return numeric_settings[SETTING_DEADZONE]->GetValue(); +} + +ControlState MixedTriggers::GetThreshold() const +{ + return numeric_settings[SETTING_THRESHOLD]->GetValue(); +} + } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h index cf85ffdc52..4e7b2adafe 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h @@ -15,6 +15,17 @@ class MixedTriggers : public ControlGroup public: explicit MixedTriggers(const std::string& name); - void GetState(u16* digital, const u16* bitmasks, ControlState* analog); + void GetState(u16* digital, const u16* bitmasks, ControlState* analog, + bool adjusted = true) const; + + ControlState GetDeadzone() const; + ControlState GetThreshold() const; + +private: + enum + { + SETTING_THRESHOLD, + SETTING_DEADZONE, + }; }; } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/ModifySettingsButton.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/ModifySettingsButton.cpp index 268d239f0b..d4e8bfa024 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/ModifySettingsButton.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/ModifySettingsButton.cpp @@ -22,7 +22,6 @@ namespace ControllerEmu ModifySettingsButton::ModifySettingsButton(std::string button_name) : Buttons(std::move(button_name)) { - numeric_settings.emplace_back(std::make_unique(_trans("Threshold"), 0.5)); } void ModifySettingsButton::AddInput(std::string button_name, bool toggle) diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.cpp index 9221b90243..2fec9c7bd0 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.cpp @@ -10,6 +10,8 @@ #include #include "Common/Common.h" +#include "Common/MathUtil.h" + #include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControllerEmu/Control/Control.h" #include "InputCommon/ControllerEmu/Control/Input.h" @@ -17,7 +19,8 @@ namespace ControllerEmu { -Tilt::Tilt(const std::string& name_) : ControlGroup(name_, GroupType::Tilt) +Tilt::Tilt(const std::string& name_) + : ReshapableInput(name_, name_, GroupType::Tilt), m_last_update(Clock::now()) { controls.emplace_back(std::make_unique(Translate, _trans("Forward"))); controls.emplace_back(std::make_unique(Translate, _trans("Backward"))); @@ -26,71 +29,63 @@ Tilt::Tilt(const std::string& name_) : ControlGroup(name_, GroupType::Tilt) controls.emplace_back(std::make_unique(Translate, _trans("Modifier"))); - numeric_settings.emplace_back(std::make_unique(_trans("Dead Zone"), 0, 0, 50)); - numeric_settings.emplace_back(std::make_unique(_trans("Circle Stick"), 0)); + // Set default input radius to the full 1.0 (no resizing) + // Set default input shape to a square (no reshaping) + // Max deadzone to 50% + AddReshapingSettings(1.0, 0.5, 50); + numeric_settings.emplace_back(std::make_unique(_trans("Angle"), 0.9, 0, 180)); } -Tilt::StateData Tilt::GetState(const bool step) +Tilt::ReshapeData Tilt::GetReshapableState(bool adjusted) { - // this is all a mess + const ControlState y = controls[0]->control_ref->State() - controls[1]->control_ref->State(); + const ControlState x = controls[3]->control_ref->State() - controls[2]->control_ref->State(); - ControlState yy = controls[0]->control_ref->State() - controls[1]->control_ref->State(); - ControlState xx = controls[3]->control_ref->State() - controls[2]->control_ref->State(); + // Return raw values. (used in UI) + if (!adjusted) + return {x, y}; - ControlState deadzone = numeric_settings[0]->GetValue(); - ControlState circle = numeric_settings[1]->GetValue(); - auto const angle = numeric_settings[2]->GetValue() / 1.8; - ControlState m = controls[4]->control_ref->State(); + const ControlState modifier = controls[4]->control_ref->State(); - // deadzone / circle stick code - // this section might be all wrong, but its working good enough, I think + // Compute desired tilt: + StateData target = Reshape(x, y, modifier); - ControlState ang = atan2(yy, xx); - ControlState ang_sin = sin(ang); - ControlState ang_cos = cos(ang); + // Step the simulation. This is somewhat ugly being here. + // We should be able to GetState without changing state. + // State should be stored outside of this object inside the wiimote, + // and separately inside the UI. - // the amt a full square stick would have at current angle - ControlState square_full = - std::min(ang_sin ? 1 / fabs(ang_sin) : 2, ang_cos ? 1 / fabs(ang_cos) : 2); + // We're using system time rather than ticks to step this. + // I don't think that's too horrible as we can consider this part of user input. + // And at least the Mapping UI will behave sanely this way. + // TODO: when state is moved outside of this class have a separate Step() + // function that takes a ms_passed argument + const auto now = Clock::now(); + const auto ms_since_update = + std::chrono::duration_cast(now - m_last_update).count(); + m_last_update = now; - // the amt a full stick would have that was (user setting circular) at current angle - // I think this is more like a pointed circle rather than a rounded square like it should be - ControlState stick_full = (square_full * (1 - circle)) + (circle); + const double max_step = MAX_DEG_PER_SEC / 180.0 * ms_since_update / 1000; - ControlState dist = sqrt(xx * xx + yy * yy); + // TODO: Allow wrap around from 1.0 to -1.0 + // (take the fastest route to target) - // dead zone code - dist = std::max(0.0, dist - deadzone * stick_full); - dist /= (1 - deadzone); + m_tilt.x += MathUtil::Clamp(target.x - m_tilt.x, -max_step, max_step); + m_tilt.y += MathUtil::Clamp(target.y - m_tilt.y, -max_step, max_step); - // circle stick code - ControlState amt = dist / stick_full; - dist += (square_full - 1) * amt * circle; - - if (m) - dist *= 0.5; - - yy = std::max(-1.0, std::min(1.0, ang_sin * dist)); - xx = std::max(-1.0, std::min(1.0, ang_cos * dist)); - - // this is kinda silly here - // gui being open will make this happen 2x as fast, o well - - // silly - if (step) - { - if (xx > m_tilt.x) - m_tilt.x = std::min(m_tilt.x + 0.1, xx); - else if (xx < m_tilt.x) - m_tilt.x = std::max(m_tilt.x - 0.1, xx); - - if (yy > m_tilt.y) - m_tilt.y = std::min(m_tilt.y + 0.1, yy); - else if (yy < m_tilt.y) - m_tilt.y = std::max(m_tilt.y - 0.1, yy); - } - - return {m_tilt.x * angle, m_tilt.y * angle}; + return m_tilt; } + +Tilt::StateData Tilt::GetState() +{ + return GetReshapableState(true); +} + +ControlState Tilt::GetGateRadiusAtAngle(double ang) const +{ + const ControlState max_tilt_angle = numeric_settings[SETTING_MAX_ANGLE]->GetValue() / 1.8; + return SquareStickGate(max_tilt_angle).GetRadiusAtAngle(ang); +} + } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.h index b03dc5a8b1..55afd43ba3 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.h @@ -4,26 +4,37 @@ #pragma once +#include #include -#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" + +#include "InputCommon/ControllerEmu/StickGate.h" #include "InputCommon/ControllerInterface/Device.h" namespace ControllerEmu { -class Tilt : public ControlGroup +class Tilt : public ReshapableInput { public: - struct StateData - { - ControlState x{}; - ControlState y{}; - }; + using StateData = ReshapeData; explicit Tilt(const std::string& name); - StateData GetState(bool step = true); + ReshapeData GetReshapableState(bool adjusted) final override; + ControlState GetGateRadiusAtAngle(double ang) const override; + + StateData GetState(); private: + enum + { + SETTING_MAX_ANGLE = ReshapableInput::SETTING_COUNT, + }; + + static constexpr int MAX_DEG_PER_SEC = 360 * 6; + StateData m_tilt; + + using Clock = std::chrono::steady_clock; + Clock::time_point m_last_update; }; } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/StickGate.cpp b/Source/Core/InputCommon/ControllerEmu/StickGate.cpp index c0c2a668a3..64cf238e17 100644 --- a/Source/Core/InputCommon/ControllerEmu/StickGate.cpp +++ b/Source/Core/InputCommon/ControllerEmu/StickGate.cpp @@ -6,7 +6,9 @@ #include +#include "Common/Common.h" #include "Common/MathUtil.h" +#include "InputCommon/ControllerEmu/Setting/NumericSetting.h" namespace ControllerEmu { @@ -44,4 +46,90 @@ ControlState SquareStickGate::GetRadiusAtAngle(double ang) const return m_half_width / std::cos(std::fmod(ang + section_ang / 2, section_ang) - section_ang / 2); } +ReshapableInput::ReshapableInput(std::string name, std::string ui_name, GroupType type) + : ControlGroup(std::move(name), std::move(ui_name), type) +{ +} + +ControlState ReshapableInput::GetDeadzoneRadiusAtAngle(double ang) const +{ + return CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_DEADZONE]->GetValue(); +} + +ControlState ReshapableInput::GetInputRadiusAtAngle(double ang) const +{ + const ControlState radius = + CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_INPUT_RADIUS]->GetValue(); + // Clamp within the -1 to +1 square as input radius may be greater than 1.0: + return std::min(radius, SquareStickGate(1).GetRadiusAtAngle(ang)); +} + +void ReshapableInput::AddReshapingSettings(ControlState default_radius, ControlState default_shape, + int max_deadzone) +{ + // Allow radius greater than 1.0 for definitions of rounded squares + // This is ideal for Xbox controllers (and probably others) + numeric_settings.emplace_back( + std::make_unique(_trans("Input Radius"), default_radius, 0, 140)); + numeric_settings.emplace_back( + std::make_unique(_trans("Input Shape"), default_shape, 0, 50)); + numeric_settings.emplace_back(std::make_unique(_trans("Dead Zone"), 0, 0, 50)); +} + +ReshapableInput::ReshapeData ReshapableInput::Reshape(ControlState x, ControlState y, + ControlState modifier) +{ + // TODO: make the AtAngle functions work with negative angles: + const ControlState ang = std::atan2(y, x) + MathUtil::TAU; + + const ControlState gate_max_dist = GetGateRadiusAtAngle(ang); + const ControlState input_max_dist = GetInputRadiusAtAngle(ang); + + // If input radius is zero we apply no scaling. + // This is useful when mapping native controllers without knowing intimate radius details. + const ControlState max_dist = input_max_dist ? input_max_dist : gate_max_dist; + + ControlState dist = std::sqrt(x * x + y * y) / max_dist; + + // If the modifier is pressed, scale the distance by the modifier's value. + // This is affected by the modifier's "range" setting which defaults to 50%. + if (modifier) + { + // TODO: Modifier's range setting gets reset to 100% when the clear button is clicked. + // This causes the modifier to not behave how a user might suspect. + // Retaining the old scale-by-50% behavior until range is fixed to clear to 50%. + dist *= 0.5; + // dist *= modifier; + } + + // Apply deadzone as a percentage of the user-defined radius/shape: + const ControlState deadzone = GetDeadzoneRadiusAtAngle(ang); + dist = std::max(0.0, dist - deadzone) / (1.0 - deadzone); + + // Scale to the gate shape/radius: + dist = dist *= gate_max_dist; + + x = MathUtil::Clamp(std::cos(ang) * dist, -1.0, 1.0); + y = MathUtil::Clamp(std::sin(ang) * dist, -1.0, 1.0); + return {x, y}; +} + +ControlState ReshapableInput::CalculateInputShapeRadiusAtAngle(double ang) const +{ + const auto shape = numeric_settings[SETTING_INPUT_SHAPE]->GetValue() * 4.0; + + if (shape < 1.0) + { + // Between 0 and 25 return a shape between octagon and circle + const auto amt = shape; + return OctagonStickGate(1).GetRadiusAtAngle(ang) * (1 - amt) + amt; + } + else + { + // Between 25 and 50 return a shape between circle and square + const auto amt = shape - 1.0; + return (1 - amt) + SquareStickGate(1).GetRadiusAtAngle(ang) * amt; + } +} + } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/StickGate.h b/Source/Core/InputCommon/ControllerEmu/StickGate.h index decb15d234..93b5c5792c 100644 --- a/Source/Core/InputCommon/ControllerEmu/StickGate.h +++ b/Source/Core/InputCommon/ControllerEmu/StickGate.h @@ -5,6 +5,7 @@ #pragma once #include "InputCommon/ControlReference/ControlReference.h" +#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" namespace ControllerEmu { @@ -52,4 +53,40 @@ private: const ControlState m_half_width; }; +class ReshapableInput : public ControlGroup +{ +public: + ReshapableInput(std::string name, std::string ui_name, GroupType type); + + struct ReshapeData + { + ControlState x{}; + ControlState y{}; + }; + + enum + { + SETTING_INPUT_RADIUS, + SETTING_INPUT_SHAPE, + SETTING_DEADZONE, + SETTING_COUNT, + }; + + // Angle is in radians and should be non-negative + ControlState GetDeadzoneRadiusAtAngle(double ang) const; + ControlState GetInputRadiusAtAngle(double ang) const; + + virtual ControlState GetGateRadiusAtAngle(double ang) const = 0; + virtual ReshapeData GetReshapableState(bool adjusted) = 0; + +protected: + void AddReshapingSettings(ControlState default_radius, ControlState default_shape, + int max_deadzone); + + ReshapeData Reshape(ControlState x, ControlState y, ControlState modifier = 0.0); + +private: + ControlState CalculateInputShapeRadiusAtAngle(double ang) const; +}; + } // namespace ControllerEmu