diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 5399a6c53..5d421df97 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -23,6 +23,7 @@ void Load(const ConfigOptions& cfg) { EXIT_IF(g_config == nullptr); EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH); EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id)); + EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100); *g_config = cfg; } @@ -51,6 +52,14 @@ const std::optional& GetControllerColor() { return g_config->controller_color; } +uint32_t GetControllerSpeakerVolume() { + return g_config->controller_speaker_volume; +} + +uint32_t GetControllerVibrationIntensity() { + return g_config->controller_vibration_intensity; +} + PresentMode GetPresentMode() { return g_config->present_mode; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 2d369626f..7ace3c175 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -48,6 +48,8 @@ struct ConfigOptions { int32_t user_id = DEFAULT_USER_ID; std::string audio_input_device; std::optional controller_color; + uint32_t controller_speaker_volume = 100; + uint32_t controller_vibration_intensity = 100; PresentMode present_mode = PresentMode::Mailbox; int32_t gpu_index = -1; bool fullscreen_enabled = false; @@ -86,6 +88,8 @@ const std::string& GetUserName(); int32_t GetUserId(); const std::string& GetAudioInputDevice(); const std::optional& GetControllerColor(); +uint32_t GetControllerSpeakerVolume(); +uint32_t GetControllerVibrationIntensity(); PresentMode GetPresentMode(); int32_t GetGpuIndex(); bool FullscreenEnabled(); diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index 660667f6f..05daec599 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -23,7 +23,7 @@ 8 - + 8 @@ -72,27 +72,6 @@ Language - - - DualSense lightbar: - - - - - - - Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update. - Custom - - - - - Let the game control the lightbar - Default - - - - @@ -316,7 +295,82 @@ - + + + Controller + + QFormLayout::AllNonFixedFieldsGrow + + + DualSense lightbar: + + + + + + + Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update. + Custom + + + + + Let the game control the lightbar + Default + + + + + + + Vibration intensity: + + + + + + + Scale DualSense vibration over USB and Bluetooth + 100 + 100 + Qt::Horizontal + + + + + 400 + 100% + + + + + + + Speaker volume: + + + + + + + Scale the DualSense controller speaker over USB and Bluetooth + 100 + 100 + Qt::Horizontal + + + + + 400 + 100% + + + + + + + + diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 411d1a226..638f7ed5d 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -46,6 +46,30 @@ QString EnumToText(T value) { : key); } +struct ControllerSettings { + QString color; + int speaker_volume = 100; + int vibration_intensity = 100; + + void WriteSettings(QSettings* s) const { + s->setValue("controller_color", color); + s->setValue("controller_speaker_volume", speaker_volume); + s->setValue("controller_vibration_intensity", vibration_intensity); + } + + void ReadSettings(QSettings* s) { + const QColor saved_color(s->value("controller_color").toString()); + color = saved_color.isValid() ? saved_color.name(QColor::HexRgb) : QString {}; + const auto read_percent = [s](const char* key) { + bool ok = false; + const int value = s->value(key, 100).toInt(&ok); + return ok ? qBound(0, value, 100) : 100; + }; + speaker_volume = read_percent("controller_speaker_volume"); + vibration_intensity = read_percent("controller_vibration_intensity"); + } +}; + class Configuration: public QObject { Q_OBJECT @@ -85,11 +109,13 @@ public: GameStatus game_status = GameStatus::Unknown; QString game_comment; + // Controller preferences always come from the global configuration. + ControllerSettings controller; + Resolution screen_resolution = Resolution::R1280X720; QString user_name = "Kyty"; int user_id = Config::DEFAULT_USER_ID; QString audio_input_device; - QString controller_color; PresentMode present_mode = PresentMode::Mailbox; int gpu_index = -1; bool fullscreen_enabled = false; @@ -121,7 +147,6 @@ public: user_name = other.user_name; user_id = other.user_id; audio_input_device = other.audio_input_device; - controller_color = other.controller_color; present_mode = other.present_mode; gpu_index = other.gpu_index; fullscreen_enabled = other.fullscreen_enabled; @@ -168,7 +193,6 @@ public: KYTY_CFG_SET(user_name); KYTY_CFG_SET(user_id); KYTY_CFG_SET(audio_input_device); - KYTY_CFG_SET(controller_color); KYTY_CFG_SET(present_mode); KYTY_CFG_SET(gpu_index); KYTY_CFG_SET(fullscreen_enabled); @@ -208,8 +232,6 @@ public: ? saved_user_id : Config::DEFAULT_USER_ID; audio_input_device = s->value("audio_input_device", audio_input_device).toString(); - const QColor color(s->value("controller_color", controller_color).toString()); - controller_color = color.isValid() ? color.name(QColor::HexRgb) : QString {}; KYTY_CFG_GET(present_mode); gpu_index = s->value("gpu_index", -1).toInt(); if (EnumToText(present_mode).isEmpty()) { diff --git a/src/launcher/include/configurationEditDialog.h b/src/launcher/include/configurationEditDialog.h index 25adc8be6..cc441243f 100644 --- a/src/launcher/include/configurationEditDialog.h +++ b/src/launcher/include/configurationEditDialog.h @@ -28,7 +28,7 @@ public: static void WriteSettings(QSettings& s); static void ReadSettings(QSettings& s); - void SetGameDirectories(const QStringList& dirs); + void SetGlobalSettings(const QStringList& dirs); [[nodiscard]] QStringList GetGameDirectories() const; signals: @@ -40,7 +40,7 @@ private: QGroupBox* m_game_dirs_group = nullptr; QListWidget* m_game_dirs_list = nullptr; QToolButton* m_remove_game_dir_button = nullptr; - bool m_show_game_dirs = false; + bool m_global_settings = false; protected: void Init(const Configuration& info); diff --git a/src/launcher/include/configurationListWidget.h b/src/launcher/include/configurationListWidget.h index 53671fc47..34b58ad96 100644 --- a/src/launcher/include/configurationListWidget.h +++ b/src/launcher/include/configurationListWidget.h @@ -34,7 +34,7 @@ public: [[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; } [[nodiscard]] const QString& GetGlobalControllerColor() const { - return m_global_info.controller_color; + return m_global_info.controller.color; } [[nodiscard]] std::unique_ptr CreateConfiguration(const ConfigurationItem& item) const; diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index 1dc0fc7c4..5e1c173df 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,7 @@ constexpr char SETTINGS_CFG_DIALOG[] = "ConfigurationEditDialog"; constexpr char SETTINGS_CFG_LAST_GEOMETRY[] = "geometry"; -constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 560; +constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 400; static void UpdateControllerColorButton(QPushButton* button, const QString& hex) { const QColor color(hex); @@ -123,6 +124,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p m_info(info) { m_ui->setupUi(this); InitGameDirectories(); + m_ui->controller_group->setVisible(false); connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save); connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject); @@ -142,6 +144,12 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p UpdateControllerColorButton(m_ui->button_controller_color, {}); emit PreviewControllerColor({}); }); + connect(m_ui->slider_controller_vibration, &QSlider::valueChanged, this, [this](int value) { + m_ui->label_controller_vibration_value->setText(tr("%1%").arg(value)); + }); + connect(m_ui->slider_controller_volume, &QSlider::valueChanged, this, [this](int value) { + m_ui->label_controller_volume_value->setText(tr("%1%").arg(value)); + }); connect(m_ui->comboBox_shader_log_direction, &QComboBox::currentTextChanged, this, [this](const QString& text) { auto log = TextToEnum(text); @@ -201,7 +209,9 @@ void ConfigurationEditDialog::Init(const Configuration& info) { m_ui->lineEdit_user_name->setMaxLength(static_cast(Config::MAX_USER_NAME_LENGTH)); m_ui->lineEdit_user_name->setText(info.user_name); m_ui->spinBox_user_id->setValue(info.user_id); - UpdateControllerColorButton(m_ui->button_controller_color, info.controller_color); + UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color); + m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity); + m_ui->slider_controller_volume->setValue(info.controller.speaker_volume); auto* microphone = m_ui->comboBox_audio_input_device; microphone->clear(); microphone->addItem(tr("None"), QString {}); @@ -342,8 +352,8 @@ void ConfigurationEditDialog::InitGameDirectories() { update_game_directory_buttons(); } -void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) { - m_show_game_dirs = true; +void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) { + m_global_settings = true; m_game_dirs_list->clear(); m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH); @@ -352,6 +362,7 @@ void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) { } m_game_dirs_group->setVisible(true); + m_ui->controller_group->setVisible(true); update_game_directory_buttons(); layout()->activate(); resize(size().expandedTo(minimumSizeHint())); @@ -359,7 +370,7 @@ void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) { QStringList ConfigurationEditDialog::GetGameDirectories() const { QStringList dirs; - if (!m_show_game_dirs) { + if (!m_global_settings) { return dirs; } @@ -399,11 +410,15 @@ void ConfigurationEditDialog::resizeEvent(QResizeEvent* event) { g_last_geometry = saveGeometry(); } -static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) { +static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, bool global_settings) { info.user_name = ui.lineEdit_user_name->text().trimmed(); info.user_id = ui.spinBox_user_id->value(); info.audio_input_device = ui.comboBox_audio_input_device->currentData().toString(); - info.controller_color = ui.button_controller_color->property("controllerColor").toString(); + if (global_settings) { + info.controller.color = ui.button_controller_color->property("controllerColor").toString(); + info.controller.vibration_intensity = ui.slider_controller_vibration->value(); + info.controller.speaker_volume = ui.slider_controller_volume->value(); + } info.screen_resolution = TextToEnum(ui.comboBox_screen_resolution->currentText()); info.present_mode = @@ -453,7 +468,7 @@ void ConfigurationEditDialog::save() { return; } - UpdateInfo(m_info, *m_ui); + UpdateInfo(m_info, *m_ui, m_global_settings); emit accept(); } @@ -461,9 +476,9 @@ void ConfigurationEditDialog::save() { void ConfigurationEditDialog::clear() { Configuration default_info; Init(default_info); - emit PreviewControllerColor({}); - if (m_show_game_dirs) { + if (m_global_settings) { + emit PreviewControllerColor({}); m_game_dirs_list->clear(); update_game_directory_buttons(); } diff --git a/src/launcher/src/configurationListWidget.cpp b/src/launcher/src/configurationListWidget.cpp index b8a1a569a..76c6fbe38 100644 --- a/src/launcher/src/configurationListWidget.cpp +++ b/src/launcher/src/configurationListWidget.cpp @@ -314,6 +314,7 @@ void ConfigurationListWidget::WriteSettings() { s->remove(CONF_GLOBAL); s->beginGroup(CONF_GLOBAL); m_global_info.WriteSettings(s.get()); + m_global_info.controller.WriteSettings(s.get()); s->endGroup(); s->remove(CONF_SECTION_NAME); @@ -357,6 +358,7 @@ void ConfigurationListWidget::ReadSettings() { if (!s->childKeys().isEmpty()) { m_global_info.ReadSettings(s.get()); } + m_global_info.controller.ReadSettings(s.get()); s->endGroup(); qDeleteAll(m_custom_infos); @@ -409,6 +411,7 @@ ConfigurationListWidget::CreateConfiguration(const ConfigurationItem& item) cons const auto* custom = m_custom_infos.value(item.GetInfo().game_path); info->CopyGameInfoFrom(item.GetInfo()); info->CopyEmulatorSettingsFrom(custom != nullptr ? *custom : m_global_info); + info->controller = m_global_info.controller; if (custom != nullptr && !custom->elf.isEmpty()) { info->elf = custom->elf; } @@ -787,16 +790,18 @@ void ConfigurationListWidget::delete_configuartion() { void ConfigurationListWidget::edit_global_settings() { Configuration info; info.CopyEmulatorSettingsFrom(m_global_info); + info.controller = m_global_info.controller; info.name = tr("Global settings"); ConfigurationEditDialog dlg(info, this); dlg.setWindowTitle(tr("Global settings")); - dlg.SetGameDirectories(m_game_dirs); + dlg.SetGlobalSettings(m_game_dirs); connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this, &ConfigurationListWidget::PreviewControllerColor); if (dlg.exec() == QDialog::Accepted) { m_global_info.CopyEmulatorSettingsFrom(info); + m_global_info.controller = info.controller; const auto game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories()); const bool game_dirs_changed = game_dirs != m_game_dirs; m_game_dirs = game_dirs; diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index ff8a2524c..81d404004 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -228,9 +228,11 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { if (!info.audio_input_device.isEmpty()) { args << "--mic" << info.audio_input_device; } - if (!info.controller_color.isEmpty()) { - args << "--controller-color" << info.controller_color; + if (!info.controller.color.isEmpty()) { + args << "--controller-color" << info.controller.color; } + args << "--controller-volume" << QString::number(info.controller.speaker_volume); + args << "--controller-vibration" << QString::number(info.controller.vibration_intensity); args << "--present-mode" << EnumToText(info.present_mode); if (info.gpu_index >= 0) { args << "--gpu" << QString::number(info.gpu_index); @@ -529,8 +531,7 @@ void MainDialogPrivate::Update() { m_lightbar.Stop(); return; } - m_lightbar.SetColor(item != nullptr ? m_ui->widget->CreateConfiguration(*item)->controller_color - : m_ui->widget->GetGlobalControllerColor()); + m_lightbar.SetColor(m_ui->widget->GetGlobalControllerColor()); } #include "mainDialog.moc" diff --git a/src/libs/controller.cpp b/src/libs/controller.cpp index 2492a4aed..379b807c6 100644 --- a/src/libs/controller.cpp +++ b/src/libs/controller.cpp @@ -223,7 +223,13 @@ void GameController::CycleSetting(Setting setting) { float GameController::GetSettingScale(Setting setting) const { const auto step = m_setting_steps[static_cast(setting)].load(std::memory_order_relaxed); - return setting == Setting::SpeakerVolume ? SPEAKER_VOLUME[step] : INTENSITY[step]; + if (setting == Setting::SpeakerVolume) { + return SPEAKER_VOLUME[step] * (Config::GetControllerSpeakerVolume() / 100.0f); + } + if (setting == Setting::VibrationIntensity) { + return INTENSITY[step] * (Config::GetControllerVibrationIntensity() / 100.0f); + } + return INTENSITY[step]; } static uint8_t Scale(uint8_t value, float scale) { diff --git a/src/main.cpp b/src/main.cpp index a0154411b..be1607bf5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -54,6 +54,8 @@ static void PrintUsage() { Config::DEFAULT_USER_ID); ::printf(" --mic Capture from this microphone; omit for silence.\n"); ::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n"); + ::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 100.\n"); + ::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\n"); ::printf( " --present-mode Fifo, Mailbox, or Immediate. Default: Mailbox.\n"); ::printf( @@ -324,6 +326,18 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he return false; } options.config.controller_color = color; + } else if (arg == "--controller-volume") { + if (!ParseUint32(value, options.config.controller_speaker_volume) || + options.config.controller_speaker_volume > 100) { + ::printf("invalid controller volume: %s\n", value.c_str()); + return false; + } + } else if (arg == "--controller-vibration") { + if (!ParseUint32(value, options.config.controller_vibration_intensity) || + options.config.controller_vibration_intensity > 100) { + ::printf("invalid controller vibration intensity: %s\n", value.c_str()); + return false; + } } else if (arg == "--present-mode") { if (!ParseEnum(value, options.config.present_mode)) { ::printf("invalid present mode: %s\n", value.c_str()); diff --git a/tests/ControllerSettingsTests.cpp b/tests/ControllerSettingsTests.cpp index 513bf22c0..7ca99a65c 100644 --- a/tests/ControllerSettingsTests.cpp +++ b/tests/ControllerSettingsTests.cpp @@ -111,8 +111,10 @@ struct Controller { haptics_handles_rumble = false; Initialize(); Connect(1); - Check(GetSettingScale(Setting::SpeakerVolume) == 1.0f && - GetSettingScale(Setting::VibrationIntensity) == 1.0f && + Check(GetSettingScale(Setting::SpeakerVolume) == + Config::GetControllerSpeakerVolume() / 100.0f && + GetSettingScale(Setting::VibrationIntensity) == + Config::GetControllerVibrationIntensity() / 100.0f && GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f, "controller initialization retained old settings"); rumble.clear(); @@ -161,6 +163,48 @@ void TestSettingCycles() { CycleSetting(Setting::TriggerEffectIntensity); } +void TestGlobalControllerLevels() { + Config::ConfigOptions options; + options.controller_speaker_volume = 50; + options.controller_vibration_intensity = 25; + Config::Load(options); + { + Controller controller; + Check(GetSettingScale(Setting::SpeakerVolume) == 0.5f && + GetSettingScale(Setting::VibrationIntensity) == 0.25f && + GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f, + "global controller levels did not scale their outputs independently"); + SetRumble(200, 100); + Check(rumble.back().large == 50 * 257 && rumble.back().small == 25 * 257, + "global vibration level did not scale rumble"); + CycleSetting(Setting::SpeakerVolume); + CycleSetting(Setting::VibrationIntensity); + Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f && + GetSettingScale(Setting::VibrationIntensity) == 0.0f, + "cycle hotkeys did not mute globally scaled outputs"); + CycleSetting(Setting::SpeakerVolume); + CycleSetting(Setting::VibrationIntensity); + Check(GetSettingScale(Setting::SpeakerVolume) == 0.01f && + GetSettingScale(Setting::VibrationIntensity) == 0.0825f, + "cycle hotkeys did not multiply the global controller levels"); + Check(rumble.back().large == 17 * 257 && rumble.back().small == 8 * 257, + "cycling vibration did not apply the combined intensity to cached rumble"); + } + options.controller_speaker_volume = 0; + options.controller_vibration_intensity = 0; + Config::Load(options); + { + Controller controller; + Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f && + GetSettingScale(Setting::VibrationIntensity) == 0.0f, + "zero global controller levels did not mute outputs"); + SetRumble(200, 100); + Check(rumble.back().large == 0 && rumble.back().small == 0, + "zero global vibration level did not mute motors"); + } + Config::Load(Config::ConfigOptions {}); +} + void TestVibrationLifetime() { Controller controller; SetRumble(255, 1); @@ -287,6 +331,7 @@ void TestIndependentOutputsAndPadSwitch() { int main() { Config::Initialize(); TestSettingCycles(); + TestGlobalControllerLevels(); // Each following test initializes another controller and requires strong defaults. TestVibrationLifetime(); TestMaskedTriggersAndValidation();