Path: blob/master/src/duckstation-qt/achievementsettingswidget.cpp
7487 views
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <[email protected]>1// SPDX-License-Identifier: CC-BY-NC-ND-4.023#include "achievementsettingswidget.h"4#include "achievementlogindialog.h"5#include "mainwindow.h"6#include "qtutils.h"7#include "settingswindow.h"8#include "settingwidgetbinder.h"910#include "core/achievements.h"11#include "core/core.h"12#include "core/system.h"1314#include "util/translation.h"1516#include "common/bitutils.h"17#include "common/string_util.h"1819#include <QtCore/QDateTime>2021#include "moc_achievementsettingswidget.cpp"2223AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWidget* parent)24: QWidget(parent), m_dialog(dialog)25{26SettingsInterface* sif = dialog->getSettingsInterface();2728m_ui.setupUi(this);29setupAdditionalUi();3031SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enable, "Cheevos", "Enabled", false);32SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hardcoreMode, "Cheevos", "ChallengeMode", false);33SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.encoreMode, "Cheevos", "EncoreMode", false);34SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.spectatorMode, "Cheevos", "SpectatorMode", false);35SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.unofficialAchievements, "Cheevos", "UnofficialTestMode",36false);37SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.achievementNotifications, "Cheevos", "Notifications", true);38SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.achievementNotificationsDuration, "Cheevos",39"NotificationsDuration",40Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME);41SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardNotifications, "Cheevos",42"LeaderboardNotifications", true);43SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.leaderboardNotificationsDuration, "Cheevos",44"LeaderboardsDuration",45Settings::DEFAULT_LEADERBOARD_NOTIFICATION_TIME);46SettingWidgetBinder::BindWidgetToEnumSetting(47sif, m_ui.notificationLocation, "Cheevos", "NotificationLocation", &Settings::ParseNotificationLocation,48&Settings::GetNotificationLocationName, &Settings::GetNotificationLocationDisplayName,49Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_LOCATION, NotificationLocation::MaxCount);50SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardTrackers, "Cheevos", "LeaderboardTrackers", true);51SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Cheevos", "SoundEffects", true);52SettingWidgetBinder::BindWidgetToEnumSetting(53sif, m_ui.challengeIndicatorMode, "Cheevos", "ChallengeIndicatorMode",54&Settings::ParseAchievementChallengeIndicatorMode, &Settings::GetAchievementChallengeIndicatorModeName,55&Settings::GetAchievementChallengeIndicatorModeDisplayName, Settings::DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE,56AchievementChallengeIndicatorMode::MaxCount);57SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.progressIndicators, "Cheevos", "ProgressIndicators", true);58SettingWidgetBinder::BindWidgetToEnumSetting(59sif, m_ui.indicatorLocation, "Cheevos", "IndicatorLocation", &Settings::ParseNotificationLocation,60&Settings::GetNotificationLocationName, &Settings::GetNotificationLocationDisplayName,61Settings::DEFAULT_ACHIEVEMENT_INDICATOR_LOCATION, NotificationLocation::MaxCount);6263m_ui.changeSoundsLink->setText(64QStringLiteral("<a href=\"https://github.com/stenzek/duckstation/wiki/Resource-Overrides\"><span "65"style=\"text-decoration: none;\">%1</span></a>")66.arg(tr("Change Sounds")));6768dialog->registerWidgetHelp(m_ui.enable, tr("Enable Achievements"), tr("Unchecked"),69tr("When enabled and logged in, DuckStation will scan for achievements on startup."));70dialog->registerWidgetHelp(m_ui.hardcoreMode, tr("Enable Hardcore Mode"), tr("Unchecked"),71tr("\"Challenge\" mode for achievements, including leaderboard tracking. Disables save "72"state, cheats, and slowdown functions."));73dialog->registerWidgetHelp(m_ui.encoreMode, tr("Enable Encore Mode"), tr("Unchecked"),74tr("When enabled, each session will behave as if no achievements have been unlocked."));75dialog->registerWidgetHelp(m_ui.spectatorMode, tr("Enable Spectator Mode"), tr("Unchecked"),76tr("When enabled, DuckStation will assume all achievements are locked and not send any "77"unlock notifications to the server."));78dialog->registerWidgetHelp(79m_ui.unofficialAchievements, tr("Test Unofficial Achievements"), tr("Unchecked"),80tr("When enabled, DuckStation will list achievements from unofficial sets. Please note that these achievements are "81"not tracked by RetroAchievements, so they unlock every time."));82dialog->registerWidgetHelp(m_ui.achievementNotifications, tr("Show Achievement Notifications"), tr("Checked"),83tr("Displays popup messages on events such as achievement unlocks and game completion."));84dialog->registerWidgetHelp(85m_ui.leaderboardNotifications, tr("Show Leaderboard Notifications"), tr("Checked"),86tr("Displays popup messages when starting, submitting, or failing a leaderboard challenge."));87dialog->registerWidgetHelp(m_ui.leaderboardTrackers, tr("Show Leaderboard Trackers"), tr("Checked"),88tr("Shows a timer in the selected location when leaderboard challenges are active."));89dialog->registerWidgetHelp(90m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"),91tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions."));92dialog->registerWidgetHelp(m_ui.notificationLocation, tr("Notification Location"), tr("Top Left"),93tr("Selects the screen location for achievement and leaderboard notifications."));94dialog->registerWidgetHelp(m_ui.notificationScale, tr("Notification Scale"), tr("Automatic"),95tr("Determines the size of achievement notification popups. Automatic will use the same "96"scaling as the Big Picture UI."));97dialog->registerWidgetHelp(m_ui.notificationScaleCustom, tr("Custom Notification Scale"), tr("100%"),98tr("Sets the custom scale percentage for achievement notifications."));99dialog->registerWidgetHelp(100m_ui.challengeIndicatorMode, tr("Challenge Indicators"), tr("Show Notifications"),101tr("Shows a notification or icons in the selected location when a challenge/primed achievement is active."));102dialog->registerWidgetHelp(103m_ui.indicatorLocation, tr("Indicator Location"), tr("Bottom Right"),104tr("Selects the screen location for challenge/progress indicators, and leaderboard trackers."));105dialog->registerWidgetHelp(m_ui.indicatorScale, tr("Indicator Scale"), tr("Automatic"),106tr("Determines the size of challenge/progress indicators. Automatic will use the same "107"scaling as the Big Picture UI."));108dialog->registerWidgetHelp(m_ui.indicatorScaleCustom, tr("Custom Indicator Scale"), tr("100%"),109tr("Sets the custom scale percentage for challenge/progress indicators."));110dialog->registerWidgetHelp(111m_ui.progressIndicators, tr("Show Progress Indicators"), tr("Checked"),112tr("Shows a popup in the selected location when progress towards a measured achievement changes."));113114connect(m_ui.enable, &QCheckBox::checkStateChanged, this, &AchievementSettingsWidget::updateEnableState);115connect(m_ui.hardcoreMode, &QCheckBox::checkStateChanged, this,116&AchievementSettingsWidget::onHardcoreModeStateChanged);117connect(m_ui.achievementNotifications, &QCheckBox::checkStateChanged, this,118&AchievementSettingsWidget::updateEnableState);119connect(m_ui.leaderboardNotifications, &QCheckBox::checkStateChanged, this,120&AchievementSettingsWidget::updateEnableState);121connect(m_ui.achievementNotificationsDuration, &QSlider::valueChanged, this,122&AchievementSettingsWidget::onAchievementsNotificationDurationSliderChanged);123connect(m_ui.leaderboardNotificationsDuration, &QSlider::valueChanged, this,124&AchievementSettingsWidget::onLeaderboardsNotificationDurationSliderChanged);125126if (!m_dialog->isPerGameSettings())127{128connect(m_ui.loginButton, &QPushButton::clicked, this, &AchievementSettingsWidget::onLoginLogoutPressed);129connect(m_ui.viewProfile, &QPushButton::clicked, this, &AchievementSettingsWidget::onViewProfilePressed);130connect(g_core_thread, &CoreThread::achievementsLoginSuccess, this, &AchievementSettingsWidget::updateLoginState);131updateLoginState();132}133else134{135// remove login, not relevant for per-game136m_ui.verticalLayout->removeWidget(m_ui.loginBox);137m_ui.loginBox->deleteLater();138m_ui.loginBox = nullptr;139}140141// RAIntegration is not available on non-win32/x64.142#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION143if (Achievements::IsRAIntegrationAvailable())144SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useRAIntegration, "Cheevos", "UseRAIntegration", false);145else146m_ui.useRAIntegration->setEnabled(false);147148dialog->registerWidgetHelp(149m_ui.useRAIntegration, tr("Enable RAIntegration (Development Only)"), tr("Unchecked"),150tr("When enabled, DuckStation will load the RAIntegration DLL which allows for achievement development.<br>The "151"RA_Integration.dll file must be placed in the same directory as the DuckStation executable."));152#else153m_ui.settingsLayout->removeWidget(m_ui.useRAIntegration);154delete m_ui.useRAIntegration;155m_ui.useRAIntegration = nullptr;156#endif157158updateEnableState();159onAchievementsNotificationDurationSliderChanged();160onLeaderboardsNotificationDurationSliderChanged();161}162163AchievementSettingsWidget::~AchievementSettingsWidget() = default;164165void AchievementSettingsWidget::setupAdditionalUi()166{167const auto setup_scale_option = [this](const char* key, QComboBox* cb, QSpinBox* sb) {168if (m_dialog->isPerGameSettings())169{170const int global_value = Core::GetIntSettingValue("Cheevos", key, Settings::ACHIEVEMENT_NOTIFICATION_SCALE_AUTO);171cb->addItem(172qApp->translate("SettingsDialog", "Use Global Setting [%1]")173.arg((global_value < 0) ? tr("Use OSD Scale") : ((global_value == 0) ? tr("Automatic") : tr("Custom"))));174}175176cb->addItem(tr("Automatic"));177cb->addItem(tr("Use OSD Scale"));178cb->addItem(tr("Custom"));179180const int option_offset = static_cast<int>(BoolToUInt32(m_dialog->isPerGameSettings()));181if (const std::optional<int> custom_scale = m_dialog->getIntValue(182"Cheevos", key,183m_dialog->isPerGameSettings() ? std::nullopt :184std::optional<int>(Settings::ACHIEVEMENT_NOTIFICATION_SCALE_AUTO));185custom_scale.has_value())186{187if (custom_scale.value() <= 0.0f)188{189cb->setCurrentIndex(((custom_scale.value() < 0.0f) ? 1 : 0) + option_offset);190sb->setVisible(false);191sb->setValue(100); // good initial value for custom scale if the user switches to it192}193else194{195cb->setCurrentIndex(2 + option_offset);196sb->setVisible(true);197sb->setValue(custom_scale.value());198}199}200else201{202cb->setCurrentIndex(0);203sb->setVisible(false);204sb->setValue(100);205}206207connect(cb, &QComboBox::currentIndexChanged, this, [this, key, sb, option_offset](int index) {208if (index == option_offset + 0)209{210m_dialog->setIntSettingValue("Cheevos", key, Settings::ACHIEVEMENT_NOTIFICATION_SCALE_AUTO);211sb->setVisible(false);212}213else if (index == option_offset + 1)214{215m_dialog->setIntSettingValue("Cheevos", key, Settings::ACHIEVEMENT_NOTIFICATION_SCALE_OSD_SCALE);216sb->setVisible(false);217}218else if (index == option_offset + 2)219{220m_dialog->setIntSettingValue("Cheevos", key, sb->value());221sb->setVisible(true);222}223else224{225m_dialog->removeSettingValue("Cheevos", key);226sb->setVisible(false);227}228});229230connect(sb, &QSpinBox::valueChanged, this,231[this, key](int value) { m_dialog->setIntSettingValue("Cheevos", key, value); });232};233234setup_scale_option("NotificationScale", m_ui.notificationScale, m_ui.notificationScaleCustom);235setup_scale_option("IndicatorScale", m_ui.indicatorScale, m_ui.indicatorScaleCustom);236}237238void AchievementSettingsWidget::updateEnableState()239{240const bool enabled = m_dialog->getEffectiveBoolValue("Cheevos", "Enabled", false);241m_ui.hardcoreMode->setEnabled(enabled);242m_ui.encoreMode->setEnabled(enabled);243m_ui.spectatorMode->setEnabled(enabled);244m_ui.unofficialAchievements->setEnabled(enabled);245m_ui.notificationsGroup->setEnabled(enabled);246m_ui.progressTrackingGroup->setEnabled(enabled);247248const bool notifications = enabled && m_dialog->getEffectiveBoolValue("Cheevos", "Notifications", true);249const bool lb_notifications = enabled && m_dialog->getEffectiveBoolValue("Cheevos", "LeaderboardNotifications", true);250m_ui.achievementNotificationsDuration->setEnabled(notifications);251m_ui.achievementNotificationsDurationLabel->setEnabled(notifications);252m_ui.leaderboardNotificationsDuration->setEnabled(lb_notifications);253m_ui.leaderboardNotificationsDurationLabel->setEnabled(lb_notifications);254}255256void AchievementSettingsWidget::onHardcoreModeStateChanged()257{258if (!QtHost::IsSystemValid())259return;260261const bool enabled = m_dialog->getEffectiveBoolValue("Cheevos", "Enabled", false);262const bool challenge = m_dialog->getEffectiveBoolValue("Cheevos", "ChallengeMode", false);263if (!enabled || !challenge)264return;265266// don't bother prompting if the game doesn't have achievements267{268auto lock = Achievements::GetLock();269if (!Achievements::HasActiveGame())270return;271}272273QMessageBox* const msgbox = QtUtils::NewMessageBox(274this, QMessageBox::Question, tr("Restart Game"),275tr("Hardcore mode will not be enabled until the game is restarted. Do you want to restart the game now?"),276QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton);277msgbox->connect(msgbox, &QMessageBox::accepted, this, []() { g_core_thread->resetSystem(true); });278msgbox->open();279}280281void AchievementSettingsWidget::onAchievementsNotificationDurationSliderChanged()282{283const int duration =284m_dialog->getEffectiveIntValue("Cheevos", "NotificationsDuration", Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME);285m_ui.achievementNotificationsDurationLabel->setText(tr("%n seconds", nullptr, duration));286}287288void AchievementSettingsWidget::onLeaderboardsNotificationDurationSliderChanged()289{290const int duration =291m_dialog->getEffectiveIntValue("Cheevos", "LeaderboardsDuration", Settings::DEFAULT_LEADERBOARD_NOTIFICATION_TIME);292m_ui.leaderboardNotificationsDurationLabel->setText(tr("%n seconds", nullptr, duration));293}294295void AchievementSettingsWidget::updateLoginState()296{297std::string username;298std::string badge_path;299300{301const auto lock = Achievements::GetLock();302if (Achievements::IsLoggedIn())303{304if (const char* username_ptr = Achievements::GetLoggedInUserName())305username = username_ptr;306307badge_path = Achievements::GetLoggedInUserBadgePath();308}309else310{311username = Core::GetBaseStringSettingValue("Cheevos", "Username");312}313}314315if (badge_path.empty())316badge_path = QtHost::GetResourcePath("images/ra-generic-user.png", true);317318m_ui.userBadge->setPixmap(QPixmap(QString::fromStdString(badge_path)));319320const bool logged_in = !username.empty();321322if (logged_in)323{324const u64 login_unix_timestamp =325StringUtil::FromChars<u64>(Core::GetBaseStringSettingValue("Cheevos", "LoginTimestamp", "0")).value_or(0);326const QString login_timestamp =327QtHost::FormatNumber(Host::NumberFormatType::ShortDateTime, static_cast<s64>(login_unix_timestamp));328m_ui.loginStatus->setText(329tr("Logged in as %1\nToken generated at %2").arg(QString::fromStdString(username)).arg(login_timestamp));330m_ui.loginButton->setText(tr("Logout"));331}332else333{334m_ui.loginStatus->setText(tr("Not Logged In."));335m_ui.loginButton->setText(tr("Login..."));336}337338m_ui.viewProfile->setEnabled(logged_in);339}340341void AchievementSettingsWidget::onLoginLogoutPressed()342{343if (!Core::GetBaseStringSettingValue("Cheevos", "Username").empty())344{345Host::RunOnCoreThread([]() { Achievements::Logout(); }, true);346updateLoginState();347return;348}349350AchievementLoginDialog* login = new AchievementLoginDialog(this, Achievements::LoginRequestReason::UserInitiated);351connect(login, &AchievementLoginDialog::accepted, this, &AchievementSettingsWidget::onLoginCompleted);352login->open();353}354355void AchievementSettingsWidget::onLoginCompleted()356{357updateLoginState();358359// Login can enable achievements/hardcore.360if (!m_ui.enable->isChecked() && Core::GetBaseBoolSettingValue("Cheevos", "Enabled", false))361{362m_ui.enable->setChecked(true);363updateEnableState();364}365if (!m_ui.hardcoreMode->isChecked() && Core::GetBaseBoolSettingValue("Cheevos", "ChallengeMode", false))366m_ui.hardcoreMode->setChecked(true);367}368369void AchievementSettingsWidget::onViewProfilePressed()370{371const std::string username(Core::GetBaseStringSettingValue("Cheevos", "Username"));372if (username.empty())373return;374375const QByteArray encoded_username(QUrl::toPercentEncoding(QString::fromStdString(username)));376QtUtils::OpenURL(377this, QUrl(QStringLiteral("https://retroachievements.org/user/%1").arg(QString::fromUtf8(encoded_username))));378}379380381