Skip to content

Commit 2bccb29

Browse files
committed
Achievements: Add option to prefetch badges
Will pre-download locked badges to avoid load delays when unlocking.
1 parent dc31d7d commit 2bccb29

File tree

7 files changed

+171
-32
lines changed

7 files changed

+171
-32
lines changed

src/core/achievements.cpp

Lines changed: 126 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,15 @@ static void UpdateModeSettings(const Settings& old_config);
132132
static DynamicHeapArray<u8> SaveStateToBuffer();
133133
static void LoadStateFromBuffer(std::span<const u8> data, std::unique_lock<std::recursive_mutex>& lock);
134134
static bool SaveStateToBuffer(std::span<u8> data);
135+
static std::string GetAchievementBadgeURL(const rc_client_achievement_t* achievement, u32 image_type);
135136
static std::string GetImageURL(const char* image_name, u32 type);
136137
static std::string GetLocalImagePath(const std::string_view image_name, u32 type);
137138
static void DownloadImage(std::string url, std::string cache_path);
138139
static void PrefetchNextAchievementBadge();
139140
static void PrefetchNextAchievementBadge(const rc_client_achievement_t* const last_cheevo);
141+
static void PrefetchAllAchievementBadges();
142+
static void SendNextPrefetchBadgeRequest();
143+
static void ClearPrefetchBadgeRequests();
140144

141145
static TinyString DecryptLoginToken(std::string_view encrypted_token, std::string_view username);
142146
static TinyString EncryptLoginToken(std::string_view token, std::string_view username);
@@ -256,6 +260,8 @@ struct State
256260
rc_client_all_user_progress_t* fetch_all_progress_result = nullptr;
257261
rc_client_async_handle_t* refresh_all_progress_request = nullptr;
258262

263+
std::vector<std::pair<std::string, std::string>> prefetch_badge_requests; // (path, url)
264+
259265
#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
260266
rc_client_async_handle_t* load_raintegration_request = nullptr;
261267
bool using_raintegration = false;
@@ -504,6 +510,93 @@ void Achievements::PrefetchNextAchievementBadge(const rc_client_achievement_t* c
504510
GetAchievementBadgePath(next_cheevo, false);
505511
}
506512

513+
void Achievements::PrefetchAllAchievementBadges()
514+
{
515+
static constexpr u32 PREFETCH_IMAGE_TYPE = RC_IMAGE_TYPE_ACHIEVEMENT;
516+
517+
// This is here so that we can hopefully avoid the delay in downloading the badge image on unlock.
518+
if (!HasAchievements())
519+
return;
520+
521+
rc_client_achievement_list_t* const achievements =
522+
rc_client_create_achievement_list(Achievements::GetClient(), RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
523+
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE);
524+
if (!achievements)
525+
return;
526+
527+
for (u32 i = 0; i < achievements->num_buckets; i++)
528+
{
529+
// Ignore unlocked achievements, since we're not going to be showing a notification for them.
530+
const rc_client_achievement_bucket_t& bucket = achievements->buckets[i];
531+
if (bucket.bucket_type != RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED)
532+
continue;
533+
534+
for (u32 j = 0; j < bucket.num_achievements; j++)
535+
{
536+
const rc_client_achievement_t* const cheevo = bucket.achievements[j];
537+
std::string path = GetLocalImagePath(cheevo->badge_name, PREFETCH_IMAGE_TYPE);
538+
if (path.empty() || FileSystem::FileExists(path.c_str()))
539+
continue;
540+
541+
std::string url = GetAchievementBadgeURL(cheevo, PREFETCH_IMAGE_TYPE);
542+
VERBOSE_LOG("Prefetching badge for locked achievement '{}' ({})", cheevo->title, cheevo->badge_url);
543+
s_state.prefetch_badge_requests.emplace_back(std::move(path), std::move(url));
544+
}
545+
}
546+
rc_client_destroy_achievement_list(achievements);
547+
if (s_state.prefetch_badge_requests.empty())
548+
return;
549+
550+
// reverse the list, fetch the first achievement first since it's the most likely to be unlocked next
551+
std::ranges::reverse(s_state.prefetch_badge_requests);
552+
SendNextPrefetchBadgeRequest();
553+
}
554+
555+
void Achievements::SendNextPrefetchBadgeRequest()
556+
{
557+
if (s_state.prefetch_badge_requests.empty())
558+
return;
559+
560+
std::string cache_path = std::move(s_state.prefetch_badge_requests.back().first);
561+
std::string url = std::move(s_state.prefetch_badge_requests.back().second);
562+
s_state.prefetch_badge_requests.pop_back();
563+
564+
// free memory when done
565+
if (s_state.prefetch_badge_requests.empty())
566+
s_state.prefetch_badge_requests = {};
567+
568+
auto callback = [cache_path = std::move(cache_path)](s32 status_code, const Error& error,
569+
const std::string& content_type,
570+
HTTPDownloader::Request::Data data) mutable {
571+
if (status_code != HTTPDownloader::HTTP_STATUS_OK)
572+
{
573+
ERROR_LOG("Failed to download badge '{}': {}", Path::GetFileName(cache_path), error.GetDescription());
574+
return;
575+
}
576+
577+
Error write_error;
578+
if (!FileSystem::WriteBinaryFile(cache_path.c_str(), data, &write_error))
579+
{
580+
ERROR_LOG("Failed to write badge image to '{}': {}", cache_path, write_error.GetDescription());
581+
return;
582+
}
583+
584+
VideoThread::RunOnThread(
585+
[cache_path = std::move(cache_path)]() { FullscreenUI::InvalidateCachedTexture(cache_path); });
586+
587+
SendNextPrefetchBadgeRequest();
588+
};
589+
590+
s_state.http_downloader->CreateRequest(std::move(url), std::move(callback));
591+
if (!s_state.prefetch_badge_requests.empty())
592+
VERBOSE_LOG("{} badge requests remaining", s_state.prefetch_badge_requests.size());
593+
}
594+
595+
void Achievements::ClearPrefetchBadgeRequests()
596+
{
597+
s_state.prefetch_badge_requests = {};
598+
}
599+
507600
bool Achievements::IsActive()
508601
{
509602
return (s_state.client != nullptr);
@@ -1130,6 +1223,8 @@ void Achievements::GameChanged(CDImage* image)
11301223
if (!IdentifyGame(image))
11311224
return;
11321225

1226+
ClearPrefetchBadgeRequests();
1227+
11331228
// cancel previous requests
11341229
if (s_state.load_game_request)
11351230
{
@@ -1200,6 +1295,9 @@ void Achievements::BeginLoadGame()
12001295
return;
12011296
}
12021297

1298+
// Clear prefetch requests, since if we're loading state we'll get blocked until they all download otherwise.
1299+
ClearPrefetchBadgeRequests();
1300+
12031301
s_state.load_game_request = rc_client_begin_load_game(s_state.client, GameHashToString(s_state.game_hash).c_str(),
12041302
ClientLoadGameCallback, nullptr);
12051303
}
@@ -1294,7 +1392,12 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message,
12941392

12951393
// update progress database on first load, in case it was played on another PC
12961394
UpdateGameSummary(true);
1297-
PrefetchNextAchievementBadge();
1395+
1396+
// Defer starting the prefetch, because otherwise when loading state we'll block until it's all downloaded.
1397+
if (g_settings.achievements_prefetch_badges)
1398+
Host::RunOnCoreThread(&Achievements::PrefetchAllAchievementBadges);
1399+
else
1400+
PrefetchNextAchievementBadge();
12981401

12991402
// needed for notifications
13001403
SoundEffectManager::EnsureInitialized();
@@ -1307,6 +1410,8 @@ void Achievements::ClearGameInfo()
13071410
{
13081411
FullscreenUI::ClearAchievementsState();
13091412

1413+
ClearPrefetchBadgeRequests();
1414+
13101415
if (s_state.load_game_request)
13111416
{
13121417
rc_client_abort_async(s_state.client, s_state.load_game_request);
@@ -1958,30 +2063,40 @@ bool Achievements::DoState(StateWrapper& sw)
19582063
}
19592064
}
19602065

2066+
std::string Achievements::GetAchievementBadgeURL(const rc_client_achievement_t* achievement, u32 image_type)
2067+
{
2068+
std::string url;
2069+
const char* url_ptr;
2070+
2071+
// RAIntegration doesn't set the URL fields.
2072+
if (IsUsingRAIntegration() ||
2073+
!(url_ptr =
2074+
(image_type == RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED) ? achievement->badge_locked_url : achievement->badge_url))
2075+
{
2076+
return GetImageURL(achievement->badge_name, image_type);
2077+
}
2078+
else
2079+
{
2080+
return std::string(url_ptr);
2081+
}
2082+
}
2083+
19612084
std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked,
19622085
bool download_if_missing)
19632086
{
19642087
const u32 image_type = locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT;
19652088
const std::string path = GetLocalImagePath(achievement->badge_name, image_type);
19662089
if (download_if_missing && !path.empty() && !FileSystem::FileExists(path.c_str()))
19672090
{
1968-
std::string url;
1969-
const char* url_ptr;
1970-
1971-
// RAIntegration doesn't set the URL fields.
1972-
if (IsUsingRAIntegration() || !(url_ptr = locked ? achievement->badge_locked_url : achievement->badge_url))
1973-
url = GetImageURL(achievement->badge_name, image_type);
1974-
else
1975-
url = std::string(url_ptr);
1976-
2091+
std::string url = GetAchievementBadgeURL(achievement, image_type);
19772092
if (url.empty()) [[unlikely]]
19782093
{
19792094
ReportFmtError("Achievement {} with badge name {} has no badge URL", achievement->id, achievement->badge_name);
19802095
}
19812096
else
19822097
{
19832098
DEV_LOG("Downloading badge for achievement {} from URL: {}", achievement->id, url);
1984-
DownloadImage(std::string(url), path);
2099+
DownloadImage(std::move(url), path);
19852100
}
19862101
}
19872102

src/core/fullscreenui_settings.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4979,6 +4979,10 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock<std::mutex>& se
49794979
bsi, FSUI_ICONVSTR(ICON_FA_MUSIC, "Sound Effects"),
49804980
FSUI_VSTR("Plays sound effects for events such as achievement unlocks and leaderboard submissions."), "Cheevos",
49814981
"SoundEffects", true, enabled);
4982+
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_DOWNLOAD, "Prefetch Badges"),
4983+
FSUI_VSTR("Downloads all locked achievement badges while starting the game. This will reduce "
4984+
"delays in the images being shown when unlocking achievements."),
4985+
"Cheevos", "PrefetchBadges", false, enabled);
49824986

49834987
DrawEnumSetting(bsi, FSUI_ICONVSTR(ICON_FA_ENVELOPE, "Notification Location"),
49844988
FSUI_VSTR("Selects the screen location for achievement and leaderboard notifications."), "Cheevos",
@@ -5038,7 +5042,7 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock<std::mutex>& se
50385042
if (is_custom)
50395043
DrawIntSpinBoxSetting(bsi, custom_title, custom_summary, "Cheevos", key, 100, 1, 500, 1, "%d%%", enabled);
50405044
};
5041-
draw_scale_setting("NotificationScale", FSUI_ICONVSTR(ICON_FA_MAGNIFYING_GLASS, "Notification Scale"),
5045+
draw_scale_setting("NotificationScale", FSUI_ICONVSTR(ICON_FA_MAGNIFYING_GLASS, "Notification Size"),
50425046
FSUI_VSTR("Determines the size of achievement notification popups."),
50435047
FSUI_ICONVSTR(ICON_FA_EXPAND, "Custom Notification Scale"),
50445048
FSUI_VSTR("Sets the custom scale percentage for achievement notifications."));

src/core/fullscreenui_strings.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ TRANSLATE_NOOP("FullscreenUI", "Do you want to continue from the automatic save
285285
TRANSLATE_NOOP("FullscreenUI", "Double-Click Toggles Fullscreen");
286286
TRANSLATE_NOOP("FullscreenUI", "Download Covers");
287287
TRANSLATE_NOOP("FullscreenUI", "Download Game Icons");
288+
TRANSLATE_NOOP("FullscreenUI", "Downloads all locked achievement badges while starting the game. This will reduce delays in the images being shown when unlocking achievements.");
288289
TRANSLATE_NOOP("FullscreenUI", "Downloads covers from a user-specified URL template.");
289290
TRANSLATE_NOOP("FullscreenUI", "Downloads icons for all games from RetroAchievements.");
290291
TRANSLATE_NOOP("FullscreenUI", "Downsamples the rendered image prior to displaying it. Can improve overall image quality in mixed 2D/3D games.");
@@ -519,7 +520,7 @@ TRANSLATE_NOOP("FullscreenUI", "None (Normal Speed)");
519520
TRANSLATE_NOOP("FullscreenUI", "Not Logged In");
520521
TRANSLATE_NOOP("FullscreenUI", "Not Scanning Subdirectories");
521522
TRANSLATE_NOOP("FullscreenUI", "Notification Location");
522-
TRANSLATE_NOOP("FullscreenUI", "Notification Scale");
523+
TRANSLATE_NOOP("FullscreenUI", "Notification Size");
523524
TRANSLATE_NOOP("FullscreenUI", "Notifications");
524525
TRANSLATE_NOOP("FullscreenUI", "OK");
525526
TRANSLATE_NOOP("FullscreenUI", "OSD Scale");
@@ -563,6 +564,7 @@ TRANSLATE_NOOP("FullscreenUI", "Post-Processing Settings");
563564
TRANSLATE_NOOP("FullscreenUI", "Post-processing chain cleared.");
564565
TRANSLATE_NOOP("FullscreenUI", "Post-processing shaders reloaded.");
565566
TRANSLATE_NOOP("FullscreenUI", "Prefer OpenGL ES Context");
567+
TRANSLATE_NOOP("FullscreenUI", "Prefetch Badges");
566568
TRANSLATE_NOOP("FullscreenUI", "Preload Images to RAM");
567569
TRANSLATE_NOOP("FullscreenUI", "Preload Replacement Textures");
568570
TRANSLATE_NOOP("FullscreenUI", "Preserve Projection Precision");

src/core/settings.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro
496496
achievements_leaderboard_trackers = si.GetBoolValue("Cheevos", "LeaderboardTrackers", true);
497497
achievements_sound_effects = si.GetBoolValue("Cheevos", "SoundEffects", true);
498498
achievements_progress_indicators = si.GetBoolValue("Cheevos", "ProgressIndicators", true);
499+
achievements_prefetch_badges = si.GetBoolValue("Cheevos", "PrefetchBadges", false);
499500
achievements_notification_location =
500501
ParseNotificationLocation(si.GetStringValue("Cheevos", "NotificationLocation").c_str())
501502
.value_or(DEFAULT_ACHIEVEMENT_NOTIFICATION_LOCATION);
@@ -830,6 +831,7 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
830831
si.SetBoolValue("Cheevos", "LeaderboardTrackers", achievements_leaderboard_trackers);
831832
si.SetBoolValue("Cheevos", "SoundEffects", achievements_sound_effects);
832833
si.SetBoolValue("Cheevos", "ProgressIndicators", achievements_progress_indicators);
834+
si.SetBoolValue("Cheevos", "PrefetchBadges", achievements_prefetch_badges);
833835
si.SetStringValue("Cheevos", "NotificationLocation", GetNotificationLocationName(achievements_notification_location));
834836
si.SetStringValue("Cheevos", "IndicatorLocation", GetNotificationLocationName(achievements_indicator_location));
835837
si.SetStringValue("Cheevos", "ChallengeIndicatorMode",

src/core/settings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ struct Settings : public GPUSettings
369369
bool achievements_leaderboard_trackers : 1 = true;
370370
bool achievements_sound_effects : 1 = true;
371371
bool achievements_progress_indicators : 1 = true;
372+
bool achievements_prefetch_badges : 1 = false;
372373
u8 achievements_notification_duration = DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME;
373374
u8 achievements_leaderboard_duration = DEFAULT_LEADERBOARD_NOTIFICATION_TIME;
374375

src/duckstation-qt/achievementsettingswidget.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
5050
Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_LOCATION, NotificationLocation::MaxCount);
5151
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardTrackers, "Cheevos", "LeaderboardTrackers", true);
5252
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Cheevos", "SoundEffects", true);
53+
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.prefetchBadges, "Cheevos", "PrefetchBadges", false);
5354
SettingWidgetBinder::BindWidgetToEnumSetting(
5455
sif, m_ui.challengeIndicatorMode, "Cheevos", "ChallengeIndicatorMode",
5556
&Settings::ParseAchievementChallengeIndicatorMode, &Settings::GetAchievementChallengeIndicatorModeName,
@@ -63,8 +64,8 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
6364

6465
m_ui.changeSoundsLink->setText(
6566
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")));
67+
"style=\"text-decoration: none;\">&nbsp;%1</span></a>")
68+
.arg(tr("(Customize)")));
6869

6970
dialog->registerWidgetHelp(m_ui.enable, tr("Enable Achievements"), tr("Unchecked"),
7071
tr("When enabled and logged in, DuckStation will scan for achievements on startup."));
@@ -90,9 +91,12 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
9091
dialog->registerWidgetHelp(
9192
m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"),
9293
tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions."));
94+
dialog->registerWidgetHelp(m_ui.prefetchBadges, tr("Prefetch Badges"), tr("Unchecked"),
95+
tr("Downloads all locked achievement badges while starting the game. This will reduce "
96+
"delays in the images being shown when unlocking achievements."));
9397
dialog->registerWidgetHelp(m_ui.notificationLocation, tr("Notification Location"), tr("Top Left"),
9498
tr("Selects the screen location for achievement and leaderboard notifications."));
95-
dialog->registerWidgetHelp(m_ui.notificationScale, tr("Notification Scale"), tr("Automatic"),
99+
dialog->registerWidgetHelp(m_ui.notificationScale, tr("Notification Size"), tr("Automatic"),
96100
tr("Determines the size of achievement notification popups. Automatic will use the same "
97101
"scaling as the Big Picture UI."));
98102
dialog->registerWidgetHelp(m_ui.notificationScaleCustom, tr("Custom Notification Scale"), tr("100%"),
@@ -103,7 +107,7 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
103107
dialog->registerWidgetHelp(
104108
m_ui.indicatorLocation, tr("Indicator Location"), tr("Bottom Right"),
105109
tr("Selects the screen location for challenge/progress indicators, and leaderboard trackers."));
106-
dialog->registerWidgetHelp(m_ui.indicatorScale, tr("Indicator Scale"), tr("Automatic"),
110+
dialog->registerWidgetHelp(m_ui.indicatorScale, tr("Indicator Size"), tr("Automatic"),
107111
tr("Determines the size of challenge/progress indicators. Automatic will use the same "
108112
"scaling as the Big Picture UI."));
109113
dialog->registerWidgetHelp(m_ui.indicatorScaleCustom, tr("Custom Indicator Scale"), tr("100%"),

src/duckstation-qt/achievementsettingswidget.ui

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -228,22 +228,33 @@
228228
</layout>
229229
</item>
230230
<item row="2" column="0">
231-
<widget class="QCheckBox" name="soundEffects">
232-
<property name="text">
233-
<string>Enable Sound Effects</string>
231+
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,1">
232+
<property name="spacing">
233+
<number>0</number>
234234
</property>
235-
</widget>
235+
<item>
236+
<widget class="QCheckBox" name="soundEffects">
237+
<property name="text">
238+
<string>Enable Sound Effects</string>
239+
</property>
240+
</widget>
241+
</item>
242+
<item>
243+
<widget class="QLabel" name="changeSoundsLink">
244+
<property name="openExternalLinks">
245+
<bool>true</bool>
246+
</property>
247+
<property name="textInteractionFlags">
248+
<set>Qt::TextInteractionFlag::LinksAccessibleByKeyboard|Qt::TextInteractionFlag::LinksAccessibleByMouse</set>
249+
</property>
250+
</widget>
251+
</item>
252+
</layout>
236253
</item>
237254
<item row="2" column="1">
238-
<widget class="QLabel" name="changeSoundsLink">
239-
<property name="alignment">
240-
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
241-
</property>
242-
<property name="openExternalLinks">
243-
<bool>true</bool>
244-
</property>
245-
<property name="textInteractionFlags">
246-
<set>Qt::TextInteractionFlag::LinksAccessibleByKeyboard|Qt::TextInteractionFlag::LinksAccessibleByMouse</set>
255+
<widget class="QCheckBox" name="prefetchBadges">
256+
<property name="text">
257+
<string>Prefetch Badges</string>
247258
</property>
248259
</widget>
249260
</item>
@@ -260,7 +271,7 @@
260271
<item row="4" column="0">
261272
<widget class="QLabel" name="notificationScaleLabel">
262273
<property name="text">
263-
<string>Notification Scale:</string>
274+
<string>Notification Size:</string>
264275
</property>
265276
</widget>
266277
</item>
@@ -316,7 +327,7 @@
316327
<item row="2" column="0">
317328
<widget class="QLabel" name="indicatorScaleLabel">
318329
<property name="text">
319-
<string>Indicator Scale:</string>
330+
<string>Indicator Size:</string>
320331
</property>
321332
</widget>
322333
</item>

0 commit comments

Comments
 (0)