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