diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index bcb0b41da..cb1e5f847 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -73,7 +73,6 @@ namespace Achievements { static constexpr const char* INFO_SOUND_NAME = "sounds/achievements/message.wav"; static constexpr const char* UNLOCK_SOUND_NAME = "sounds/achievements/unlock.wav"; static constexpr const char* LBSUBMIT_SOUND_NAME = "sounds/achievements/lbsubmit.wav"; -static constexpr const char* CACHE_SUBDIRECTORY_NAME = "achievement_images"; constexpr const char* const RA_LOGO_ICON_NAME = "images/ra-icon.webp"; constexpr const char* const RA_LOGO_SVG_ICON_NAME = "images/ra-icon.svg"; @@ -132,15 +131,11 @@ static void UpdateModeSettings(const Settings& old_config); static DynamicHeapArray SaveStateToBuffer(); static void LoadStateFromBuffer(std::span data, std::unique_lock& lock); static bool SaveStateToBuffer(std::span data); -static std::string GetAchievementBadgeURL(const rc_client_achievement_t* achievement, u32 image_type); static std::string GetImageURL(const char* image_name, u32 type); -static std::string GetLocalImagePath(const std::string_view image_name, u32 type); -static void DownloadImage(std::string url, std::string cache_path); static void PrefetchNextAchievementBadge(); static void PrefetchNextAchievementBadge(const rc_client_achievement_t* const last_cheevo); static void PrefetchAllAchievementBadges(); -static void SendNextPrefetchBadgeRequest(); -static void ClearPrefetchBadgeRequests(); +static void UpdatePrefetchAchievementBadgesOSDMessage(); static TinyString DecryptLoginToken(std::string_view encrypted_token, std::string_view username); static TinyString EncryptLoginToken(std::string_view token, std::string_view username); @@ -226,34 +221,35 @@ struct State { rc_client_t* client = nullptr; u16 pending_server_calls = 0; - bool has_achievements = false; - bool has_leaderboards = false; - bool has_rich_presence = false; - bool reload_game_on_reset = false; - bool hashdb_loaded = false; - - std::string http_user_agent_header; + u16 pending_badge_downloads = 0; + bool has_achievements : 1 = false; + bool has_leaderboards : 1 = false; + bool has_rich_presence : 1 = false; + bool hashdb_loaded : 1 = false; + bool reload_game_on_reset : 1 = false; std::recursive_mutex mutex; // large - std::string user_badge_path; - - std::string rich_presence_string; - Timer::Value rich_presence_poll_time = 0; - std::vector active_leaderboard_trackers; std::vector active_challenge_indicators; std::optional active_progress_indicator; std::vector pinned_achievement_indicators; - rc_client_user_game_summary_t game_summary = {}; + std::string http_user_agent_header; + + std::string logged_in_username; + std::string logged_in_user_icon_url; + + std::string rich_presence_string; + Timer::Value rich_presence_poll_time = 0; + + std::optional game_hash; u32 game_id = 0; std::string game_path; std::string game_title; - std::string game_icon; std::string game_icon_url; - std::optional game_hash; + rc_client_user_game_summary_t game_summary = {}; rc_client_async_handle_t* login_request = nullptr; rc_client_async_handle_t* load_game_request = nullptr; @@ -266,7 +262,8 @@ struct State rc_client_all_user_progress_t* fetch_all_progress_result = nullptr; rc_client_async_handle_t* refresh_all_progress_request = nullptr; - std::vector> prefetch_badge_requests; // (path, url) + // used for GetAchievementBadgeURL() when the url fields aren't populated + std::string temporary_url; #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION rc_client_async_handle_t* load_raintegration_request = nullptr; @@ -414,70 +411,6 @@ std::string Achievements::GetImageURL(const char* image_name, u32 type) return ret; } -std::string Achievements::GetLocalImagePath(const std::string_view image_name, u32 type) -{ - std::string_view prefix; - std::string_view suffix; - switch (type) - { - case RC_IMAGE_TYPE_GAME: - prefix = "image"; // https://media.retroachievements.org/Images/{}.png - break; - - case RC_IMAGE_TYPE_USER: - prefix = "user"; // https://media.retroachievements.org/UserPic/{}.png - break; - - case RC_IMAGE_TYPE_ACHIEVEMENT: // https://media.retroachievements.org/Badge/{}.png - prefix = "badge"; - break; - - case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED: - prefix = "badge"; - suffix = "_lock"; - break; - - default: - prefix = "badge"; - break; - } - - std::string ret; - if (!image_name.empty()) - { - ret = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}" FS_OSPATH_SEPARATOR_STR "{}_{}{}.png", EmuFolders::Cache, - CACHE_SUBDIRECTORY_NAME, prefix, Path::SanitizeFileName(image_name), suffix); - } - - return ret; -} - -void Achievements::DownloadImage(std::string url, std::string cache_path) -{ - auto callback = [cache_path = std::move(cache_path)](s32 status_code, const Error& error, - const std::string& content_type, - HTTPDownloader::Request::Data data) mutable { - if (status_code != HTTPDownloader::HTTP_STATUS_OK) - { - ERROR_LOG("Failed to download badge '{}': {}", Path::GetFileName(cache_path), error.GetDescription()); - return; - } - - Error write_error; - if (!FileSystem::WriteBinaryFile(cache_path.c_str(), data, &write_error)) - { - ERROR_LOG("Failed to write badge image to '{}': {}", cache_path, write_error.GetDescription()); - return; - } - - VideoThread::RunOnThread( - [cache_path = std::move(cache_path)]() { FullscreenUI::InvalidateCachedTexture(cache_path); }); - }; - - if (const auto downloader = HTTPCache::GetDownloader()) - downloader->CreateRequest(std::move(url), std::move(callback)); -} - void Achievements::PrefetchNextAchievementBadge() { if (!HasAchievements()) @@ -519,13 +452,14 @@ void Achievements::PrefetchNextAchievementBadge(const rc_client_achievement_t* c return; VERBOSE_LOG("Prefetching badge for likely next achievement '{}' ({})", next_cheevo->title, next_cheevo->badge_url); - GetAchievementBadgePath(next_cheevo, false); + + const std::string_view url = GetAchievementBadgeURL(next_cheevo, false); + if (!url.empty()) + HTTPCache::Prefetch(url); } void Achievements::PrefetchAllAchievementBadges() { - static constexpr u32 PREFETCH_IMAGE_TYPE = RC_IMAGE_TYPE_ACHIEVEMENT; - // This is here so that we can hopefully avoid the delay in downloading the badge image on unlock. if (!HasAchievements()) return; @@ -546,68 +480,41 @@ void Achievements::PrefetchAllAchievementBadges() for (u32 j = 0; j < bucket.num_achievements; j++) { const rc_client_achievement_t* const cheevo = bucket.achievements[j]; - std::string path = GetLocalImagePath(cheevo->badge_name, PREFETCH_IMAGE_TYPE); - if (path.empty() || FileSystem::FileExists(path.c_str())) - continue; + const std::string_view url = GetAchievementBadgeURL(cheevo, false); + if (!url.empty() && !HTTPCache::Contains(url)) + { + s_state.pending_badge_downloads++; - std::string url = GetAchievementBadgeURL(cheevo, PREFETCH_IMAGE_TYPE); - VERBOSE_LOG("Prefetching badge for locked achievement '{}' ({})", cheevo->title, cheevo->badge_url); - s_state.prefetch_badge_requests.emplace_back(std::move(path), std::move(url)); + HTTPCache::Prefetch(url, [](bool) { + const auto lock = GetLock(); + if (s_state.pending_badge_downloads > 0) + { + s_state.pending_badge_downloads--; + UpdatePrefetchAchievementBadgesOSDMessage(); + } + }); + } } } rc_client_destroy_achievement_list(achievements); - if (s_state.prefetch_badge_requests.empty()) - return; - - // reverse the list, fetch the first achievement first since it's the most likely to be unlocked next - std::ranges::reverse(s_state.prefetch_badge_requests); - SendNextPrefetchBadgeRequest(); -} - -void Achievements::SendNextPrefetchBadgeRequest() -{ - if (s_state.prefetch_badge_requests.empty()) - return; - - std::string cache_path = std::move(s_state.prefetch_badge_requests.back().first); - std::string url = std::move(s_state.prefetch_badge_requests.back().second); - s_state.prefetch_badge_requests.pop_back(); - - // free memory when done - if (s_state.prefetch_badge_requests.empty()) - s_state.prefetch_badge_requests = {}; - - auto callback = [cache_path = std::move(cache_path)](s32 status_code, const Error& error, - const std::string& content_type, - HTTPDownloader::Request::Data data) mutable { - if (status_code != HTTPDownloader::HTTP_STATUS_OK) - { - ERROR_LOG("Failed to download badge '{}': {}", Path::GetFileName(cache_path), error.GetDescription()); - return; - } - Error write_error; - if (!FileSystem::WriteBinaryFile(cache_path.c_str(), data, &write_error)) - { - ERROR_LOG("Failed to write badge image to '{}': {}", cache_path, write_error.GetDescription()); - return; - } - - VideoThread::RunOnThread( - [cache_path = std::move(cache_path)]() { FullscreenUI::InvalidateCachedTexture(cache_path); }); - - SendNextPrefetchBadgeRequest(); - }; - - if (const auto downloader = HTTPCache::GetDownloader()) - downloader->CreateRequest(std::move(url), std::move(callback)); - if (!s_state.prefetch_badge_requests.empty()) - VERBOSE_LOG("{} badge requests remaining", s_state.prefetch_badge_requests.size()); + if (s_state.pending_badge_downloads > 0) + UpdatePrefetchAchievementBadgesOSDMessage(); } -void Achievements::ClearPrefetchBadgeRequests() +void Achievements::UpdatePrefetchAchievementBadgesOSDMessage() { - s_state.prefetch_badge_requests = {}; + if (s_state.pending_badge_downloads > 0) + { + Host::AddIconOSDMessage(OSDMessageType::Persistent, "AchievementsBadgePrefetch", OSDMessageIconType::Spinner, {}, + {}, + TRANSLATE_PLURAL_STR("Achievements", "Prefetching achievement badges (%n remaining)...", + "Achievement badge prefetch count", s_state.pending_badge_downloads)); + } + else + { + Host::RemoveKeyedOSDMessage("AchievementsBadgePrefetch"); + } } bool Achievements::IsActive() @@ -654,22 +561,17 @@ bool Achievements::HasRichPresence() return s_state.has_rich_presence; } -const std::string& Achievements::GetGameTitle() +const std::string& Achievements::GetCurrentGameTitle() { return s_state.game_title; } -const std::string& Achievements::GetGamePath() +const std::string& Achievements::GetCurrentGamePath() { return s_state.game_path; } -const std::string& Achievements::GetGameIconPath() -{ - return s_state.game_icon; -} - -const std::string& Achievements::GetGameIconURL() +const std::string& Achievements::GetCurrentGameIconURL() { return s_state.game_icon_url; } @@ -1262,8 +1164,6 @@ void Achievements::GameChanged(CDImage* image) if (!IdentifyGame(image)) return; - ClearPrefetchBadgeRequests(); - // cancel previous requests if (s_state.load_game_request) { @@ -1334,9 +1234,6 @@ void Achievements::BeginLoadGame() return; } - // Clear prefetch requests, since if we're loading state we'll get blocked until they all download otherwise. - ClearPrefetchBadgeRequests(); - s_state.load_game_request = rc_client_begin_load_game(s_state.client, GameHashToString(s_state.game_hash).c_str(), ClientLoadGameCallback, nullptr); } @@ -1420,19 +1317,14 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, s_state.has_rich_presence = rc_client_has_rich_presence(client); s_state.game_icon_url = info->badge_url ? std::string(info->badge_url) : GetImageURL(info->badge_name, RC_IMAGE_TYPE_GAME); - s_state.game_icon = GetLocalImagePath(info->badge_name, RC_IMAGE_TYPE_GAME); - if (!s_state.game_icon.empty() && !s_state.game_icon_url.empty()) - { - if (!FileSystem::FileExists(s_state.game_icon.c_str())) - DownloadImage(s_state.game_icon_url, s_state.game_icon); - + if (info->badge_name) GameList::UpdateAchievementBadgeName(info->id, info->badge_name); - } // update progress database on first load, in case it was played on another PC UpdateGameSummary(true); // Defer starting the prefetch, because otherwise when loading state we'll block until it's all downloaded. + // TODO: This can be removed once we're counting requests. if (g_settings.achievements_prefetch_badges) Host::RunOnCoreThread(&Achievements::PrefetchAllAchievementBadges); else @@ -1451,8 +1343,6 @@ void Achievements::ClearGameInfo() { FullscreenUI::ClearAchievementsState(); - ClearPrefetchBadgeRequests(); - s_state.active_leaderboard_trackers = {}; s_state.active_challenge_indicators = {}; s_state.active_progress_indicator.reset(); @@ -1467,7 +1357,6 @@ void Achievements::ClearGameInfo() s_state.game_id = 0; s_state.game_title = {}; - s_state.game_icon = {}; s_state.game_icon_url = {}; s_state.reload_game_on_reset = false; s_state.has_achievements = false; @@ -1518,7 +1407,7 @@ void Achievements::DisplayAchievementSummary() FullscreenUI::AddAchievementNotification("AchievementsSummary", IsHardcoreModeActive() ? ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME_HC : ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME, - s_state.game_icon, s_state.game_title, std::string(summary), + s_state.game_icon_url, s_state.game_title, std::string(summary), RA_LOGO_ICON_NAME, FullscreenUI::AchievementNotificationNoteType::Image); if (s_state.game_summary.num_unsupported_achievements > 0) @@ -1598,12 +1487,13 @@ void Achievements::HandleUnlockEvent(const rc_client_event_t* event) if (cheevo->points > 0) note = fmt::format(ICON_EMOJI_TROPHY " {}", cheevo->points); - FullscreenUI::AddAchievementNotification( - fmt::format("achievement_unlock_{}", cheevo->id), - static_cast(g_settings.achievements_notification_duration), GetAchievementBadgePath(cheevo, false), - std::move(title), std::string(cheevo->description), std::move(note), - (cheevo->points > 0) ? FullscreenUI::AchievementNotificationNoteType::Text : - FullscreenUI::AchievementNotificationNoteType::None); + FullscreenUI::AddAchievementNotification(fmt::format("achievement_unlock_{}", cheevo->id), + static_cast(g_settings.achievements_notification_duration), + std::string(GetAchievementBadgeURL(cheevo, false)), std::move(title), + std::string(cheevo->description), std::move(note), + (cheevo->points > 0) ? + FullscreenUI::AchievementNotificationNoteType::Text : + FullscreenUI::AchievementNotificationNoteType::None); PrefetchNextAchievementBadge(cheevo); } @@ -1625,9 +1515,9 @@ void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event) s_state.game_summary.num_unlocked_achievements), TRANSLATE_PLURAL_STR("Achievements", "%n points", "Achievement points", s_state.game_summary.points_unlocked)); - FullscreenUI::AddAchievementNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, s_state.game_icon, - s_state.game_title, std::move(message), ICON_EMOJI_TROPHY, - FullscreenUI::AchievementNotificationNoteType::IconText); + FullscreenUI::AddAchievementNotification( + "achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, s_state.game_icon_url, s_state.game_title, + std::move(message), ICON_EMOJI_TROPHY, FullscreenUI::AchievementNotificationNoteType::IconText); } } @@ -1639,7 +1529,7 @@ void Achievements::HandleSubsetCompleteEvent(const rc_client_event_t* event) if (g_settings.achievements_notifications && event->subset->badge_name[0] != '\0') { // Need to grab the icon for the subset. - std::string badge_path = GetSubsetBadgePath(event->subset); + std::string badge_path = GetSubsetBadgeURL(event->subset); std::string message = fmt::format( TRANSLATE_FS("Achievements", "Subset complete.\n{0} and {1}."), @@ -1660,9 +1550,10 @@ void Achievements::HandleLeaderboardStartedEvent(const rc_client_event_t* event) if (g_settings.achievements_leaderboard_notifications) { FullscreenUI::AddAchievementNotification( - fmt::format("leaderboard_{}", event->leaderboard->id), LEADERBOARD_STARTED_NOTIFICATION_TIME, s_state.game_icon, - std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt started."), - ICON_EMOJI_RED_FLAG, FullscreenUI::AchievementNotificationNoteType::IconText); + fmt::format("leaderboard_{}", event->leaderboard->id), LEADERBOARD_STARTED_NOTIFICATION_TIME, + s_state.game_icon_url, std::string(event->leaderboard->title), + TRANSLATE_STR("Achievements", "Leaderboard attempt started."), ICON_EMOJI_RED_FLAG, + FullscreenUI::AchievementNotificationNoteType::IconText); } } @@ -1673,9 +1564,10 @@ void Achievements::HandleLeaderboardFailedEvent(const rc_client_event_t* event) if (g_settings.achievements_leaderboard_notifications) { FullscreenUI::AddAchievementNotification( - fmt::format("leaderboard_{}", event->leaderboard->id), LEADERBOARD_FAILED_NOTIFICATION_TIME, s_state.game_icon, - std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt failed."), - ICON_EMOJI_CROSS_MARK_BUTTON, FullscreenUI::AchievementNotificationNoteType::IconText); + fmt::format("leaderboard_{}", event->leaderboard->id), LEADERBOARD_FAILED_NOTIFICATION_TIME, + s_state.game_icon_url, std::string(event->leaderboard->title), + TRANSLATE_STR("Achievements", "Leaderboard attempt failed."), ICON_EMOJI_CROSS_MARK_BUTTON, + FullscreenUI::AchievementNotificationNoteType::IconText); } } @@ -1712,7 +1604,7 @@ void Achievements::HandleLeaderboardSubmittedEvent(const rc_client_event_t* even FullscreenUI::AddAchievementNotification( fmt::format("leaderboard_{}", event->leaderboard->id), - static_cast(g_settings.achievements_leaderboard_duration), s_state.game_icon, + static_cast(g_settings.achievements_leaderboard_duration), s_state.game_icon_url, std::string(event->leaderboard->title), std::move(message), g_settings.achievements_spectator_mode ? std::string(ICON_EMOJI_CHART_UPWARDS_TREND) : std::string(), g_settings.achievements_spectator_mode ? FullscreenUI::AchievementNotificationNoteType::IconText : @@ -1749,7 +1641,7 @@ void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* eve FullscreenUI::AddAchievementNotification( fmt::format("leaderboard_{}", event->leaderboard->id), - static_cast(g_settings.achievements_leaderboard_duration), s_state.game_icon, + static_cast(g_settings.achievements_leaderboard_duration), s_state.game_icon_url, std::string(event->leaderboard->title), std::move(message), ICON_EMOJI_CHECKMARK_BUTTON, FullscreenUI::AchievementNotificationNoteType::IconText, LEADERBOARD_NOTIFICATION_MIN_WIDTH); } @@ -1820,13 +1712,14 @@ void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_ return; } - std::string badge_path = GetAchievementBadgePath(event->achievement, false); + const std::string_view badge_url = GetAchievementBadgeURL(event->achievement, false); // we still track these even if the option is disabled, so that they can be displayed in the pause menu if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification) { FullscreenUI::AddAchievementNotification( - fmt::format("AchievementChallenge{}", event->achievement->id), CHALLENGE_STARTED_NOTIFICATION_TIME, badge_path, + fmt::format("AchievementChallenge{}", event->achievement->id), CHALLENGE_STARTED_NOTIFICATION_TIME, + std::string(badge_url), fmt::format(TRANSLATE_FS("Achievements", "Challenge Started: {}"), event->achievement->title ? event->achievement->title : ""), fmt::format(ICON_EMOJI_DIRECT_HIT " {}", event->achievement->description ? event->achievement->description : ""), @@ -1835,7 +1728,7 @@ void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_ s_state.active_challenge_indicators.push_back( ActiveChallengeIndicator{.achievement = event->achievement, - .badge_path = std::move(badge_path), + .badge_url = std::string(badge_url), .time_remaining = LEADERBOARD_STARTED_NOTIFICATION_TIME, .opacity = 0.0f, .active = true}); @@ -1856,7 +1749,7 @@ void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_ event->achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { FullscreenUI::AddAchievementNotification( - fmt::format("AchievementChallenge{}", event->achievement->id), CHALLENGE_FAILED_NOTIFICATION_TIME, it->badge_path, + fmt::format("AchievementChallenge{}", event->achievement->id), CHALLENGE_FAILED_NOTIFICATION_TIME, it->badge_url, fmt::format(TRANSLATE_FS("Achievements", "Challenge Failed: {}"), event->achievement->title ? event->achievement->title : ""), fmt::format(ICON_EMOJI_CROSS_MARK_BUTTON " {}", @@ -1893,7 +1786,7 @@ void Achievements::HandleAchievementProgressIndicatorShowEvent(const rc_client_e s_state.active_progress_indicator.emplace(); s_state.active_progress_indicator->achievement = event->achievement; - s_state.active_progress_indicator->badge_path = GetAchievementBadgePath(event->achievement, false); + s_state.active_progress_indicator->badge_url = GetAchievementBadgeURL(event->achievement, false); s_state.active_progress_indicator->time = 0.0f; s_state.active_progress_indicator->active = true; FullscreenUI::UpdateAchievementsLastProgressUpdate(event->achievement); @@ -2031,7 +1924,7 @@ void Achievements::LoadStateFromBuffer(std::span data, std::unique_loc { // Fallback to game icon if we don't have a cover. std::string image = System::GetImageForLoadingScreen(System::GetGamePath()); - FullscreenUI::OpenOrUpdateLoadingScreen(image.empty() ? GetGameIconPath() : image, + FullscreenUI::OpenOrUpdateLoadingScreen(image.empty() ? s_state.game_icon_url : image, TRANSLATE_SV("Achievements", "Downloading achievements data...")); WaitForServerCallsWithYield(lock); @@ -2133,73 +2026,36 @@ bool Achievements::DoState(StateWrapper& sw) } } -std::string Achievements::GetAchievementBadgeURL(const rc_client_achievement_t* achievement, u32 image_type) +std::string_view Achievements::GetAchievementBadgeURL(const rc_client_achievement_t* achievement, bool locked) { - std::string url; - const char* url_ptr; - // RAIntegration doesn't set the URL fields. - if (IsUsingRAIntegration() || - !(url_ptr = - (image_type == RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED) ? achievement->badge_locked_url : achievement->badge_url)) + if (const char* url_ptr = locked ? achievement->badge_locked_url : achievement->badge_url) { - return GetImageURL(achievement->badge_name, image_type); + const std::string_view url(url_ptr); + if (url.empty()) [[unlikely]] + ReportFmtError("Achievement {} with badge name {} has no badge URL", achievement->id, achievement->badge_name); + + return url; } else { - return std::string(url_ptr); + s_state.temporary_url = + GetImageURL(achievement->badge_name, locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT); + return s_state.temporary_url; } } -std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked, - bool download_if_missing) +std::string Achievements::GetUserBadgeURL(const char* username) { - const u32 image_type = locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT; - const std::string path = GetLocalImagePath(achievement->badge_name, image_type); - if (download_if_missing && !path.empty() && !FileSystem::FileExists(path.c_str())) - { - std::string url = GetAchievementBadgeURL(achievement, image_type); - if (url.empty()) [[unlikely]] - { - ReportFmtError("Achievement {} with badge name {} has no badge URL", achievement->id, achievement->badge_name); - } - else - { - DEV_LOG("Downloading badge for achievement {} from URL: {}", achievement->id, url); - DownloadImage(std::move(url), path); - } - } - - return path; + return GetImageURL(username, RC_IMAGE_TYPE_USER); } -std::string Achievements::GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry) +std::string Achievements::GetSubsetBadgeURL(const rc_client_subset_t* subset) { - const std::string path = GetLocalImagePath(entry->user, RC_IMAGE_TYPE_USER); - if (!FileSystem::FileExists(path.c_str())) - { - std::string url = GetImageURL(entry->user, RC_IMAGE_TYPE_USER); - if (!url.empty()) - DownloadImage(std::move(url), path); - } - - return path; -} - -std::string Achievements::GetSubsetBadgePath(const rc_client_subset_t* subset) -{ - std::string badge_path = GetLocalImagePath(subset->badge_name, RC_IMAGE_TYPE_GAME); - if (!FileSystem::FileExists(badge_path.c_str())) - { - std::string url; - if (IsUsingRAIntegration() || !subset->badge_url) - url = GetImageURL(subset->badge_name, RC_IMAGE_TYPE_GAME); - else - url = subset->badge_url; - DownloadImage(std::move(url), badge_path); - } - - return badge_path; + if (!subset->badge_url) + return GetImageURL(subset->badge_name, RC_IMAGE_TYPE_GAME); + else + return subset->badge_url; } bool Achievements::IsLoggedIn() @@ -2364,17 +2220,10 @@ void Achievements::FinishLogin() if (!user) return; - s_state.user_badge_path = GetLocalImagePath(user->username, RC_IMAGE_TYPE_USER); - if (!s_state.user_badge_path.empty() && !FileSystem::FileExists(s_state.user_badge_path.c_str())) - { - std::string url; - if (IsUsingRAIntegration() || !user->avatar_url) - url = GetImageURL(user->username, RC_IMAGE_TYPE_USER); - else - url = user->avatar_url; - - DownloadImage(std::move(url), s_state.user_badge_path); - } + s_state.logged_in_username = user->username ? std::string(user->username) : std::string(); + s_state.logged_in_user_icon_url = (user->avatar_url && user->avatar_url[0] != '\0') ? + std::string(user->avatar_url) : + GetImageURL(user->username, RC_IMAGE_TYPE_USER); PreloadHashDatabase(); @@ -2386,24 +2235,20 @@ void Achievements::FinishLogin() std::string summary = fmt::format(TRANSLATE_FS("Achievements", "Score: {} ({} softcore)\nUnread messages: {}"), user->score, user->score_softcore, user->num_unread_messages); - FullscreenUI::AddAchievementNotification("achievements_login", LOGIN_NOTIFICATION_TIME, s_state.user_badge_path, - user->display_name, std::move(summary), RA_LOGO_ICON_NAME, - FullscreenUI::AchievementNotificationNoteType::Image); + FullscreenUI::AddAchievementNotification("achievements_login", LOGIN_NOTIFICATION_TIME, + s_state.logged_in_user_icon_url, user->display_name, std::move(summary), + RA_LOGO_ICON_NAME, FullscreenUI::AchievementNotificationNoteType::Image); } } -const char* Achievements::GetLoggedInUserName() +const std::string& Achievements::GetLoggedInUserName() { - const rc_client_user_t* user = rc_client_get_user_info(s_state.client); - if (!user) [[unlikely]] - return nullptr; - - return user->username; + return s_state.logged_in_username; } -const std::string& Achievements::GetLoggedInUserBadgePath() +const std::string& Achievements::GetLoggedInUserIconURL() { - return s_state.user_badge_path; + return s_state.logged_in_user_icon_url; } SmallString Achievements::GetLoggedInUserPointsSummary() @@ -2419,9 +2264,9 @@ SmallString Achievements::GetLoggedInUserPointsSummary() return ret; } -std::string Achievements::GetGameBadgePath(std::string_view badge_name) +std::string Achievements::GetGameIconURL(const char* badge_name) { - return GetLocalImagePath(badge_name, RC_IMAGE_TYPE_GAME); + return GetImageURL(badge_name, RC_IMAGE_TYPE_GAME); } bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error) @@ -2435,7 +2280,7 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error) if (entry.achievements_game_id != 0) { // Check if we already have this badge - const std::string existing_badge = GameList::GetAchievementGameBadgePath(entry.achievements_game_id); + const std::string existing_badge = GameList::GetAchievementGameBadgeURL(entry.achievements_game_id); if (existing_badge.empty() && std::find(game_ids.begin(), game_ids.end(), entry.achievements_game_id) == game_ids.end()) { @@ -2487,45 +2332,19 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error) for (u32 i = 0; i < params.list->num_entries; i++) { const rc_client_game_title_entry_t& entry = params.list->entries[i]; - - if (entry.badge_name[0] == '\0') + const std::string_view badge_name = entry.badge_name; + if (badge_name.empty()) continue; - std::string path = GetLocalImagePath(entry.badge_name, RC_IMAGE_TYPE_GAME); - if (FileSystem::FileExists(path.c_str())) - { - // Already have this icon, just update the cache - GameList::UpdateAchievementBadgeName(entry.game_id, entry.badge_name); - continue; - } + GameList::UpdateAchievementBadgeName(entry.game_id, badge_name); - std::string url = + const std::string url = entry.badge_url ? std::string(entry.badge_url) : GetImageURL(entry.badge_name, RC_IMAGE_TYPE_GAME); - if (url.empty()) + if (url.empty() || HTTPCache::Contains(url)) continue; badges_to_download++; - HTTPCache::GetDownloader()->CreateRequest( - std::move(url), [path = std::move(path), progress](s32 status_code, const Error& http_error, const std::string&, - HTTPDownloader::Request::Data data) { - if (status_code == HTTPDownloader::HTTP_STATUS_OK) - { - INFO_LOG("Writing badge to {}...", Path::GetFileName(path)); - - Error write_error; - if (!FileSystem::FileExists(path.c_str()) && !FileSystem::WriteBinaryFile(path.c_str(), data, &write_error)) - { - ERROR_LOG("Failed to write badge to {}: {}", Path::GetFileName(path), write_error.GetDescription()); - FileSystem::DeleteFile(path.c_str()); - } - } - else - { - ERROR_LOG("Failed to download badge: HTTP {}: {}", status_code, http_error.GetDescription()); - } - - progress->IncrementProgressValue(); - }); + HTTPCache::Prefetch(url, [progress](bool) { progress->IncrementProgressValue(); }); } if (badges_to_download == 0) @@ -2579,6 +2398,8 @@ void Achievements::Logout() INFO_LOG("Logging out..."); rc_client_logout(s_state.client); + s_state.logged_in_username = {}; + s_state.logged_in_user_icon_url = {}; } INFO_LOG("Clearing credentials..."); @@ -3566,7 +3387,7 @@ void Achievements::LoadPinnedAchievements() PinnedAchievementIndicator indicator; indicator.achievement_id = id.value(); - indicator.badge_path = GetAchievementBadgePath(achievement, false); + indicator.badge_url = GetAchievementBadgeURL(achievement, false); s_state.pinned_achievement_indicators.push_back(std::move(indicator)); std::sort(s_state.pinned_achievement_indicators.begin(), s_state.pinned_achievement_indicators.end(), [](const PinnedAchievementIndicator& lhs, const PinnedAchievementIndicator& rhs) { @@ -3650,7 +3471,7 @@ void Achievements::SetAchievementPinned(u32 achievement_id, bool pinned) DEV_LOG("Pinning achievement {}", achievement_id); PinnedAchievementIndicator indicator; indicator.achievement_id = achievement_id; - indicator.badge_path = GetAchievementBadgePath(achievement, false); + indicator.badge_url = GetAchievementBadgeURL(achievement, false); s_state.pinned_achievement_indicators.insert(it, std::move(indicator)); // Hide progress indicator if it was set diff --git a/src/core/achievements.h b/src/core/achievements.h index c3e26ef88..302c87b60 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -153,17 +153,14 @@ bool HasRichPresence(); const std::string& GetRichPresenceString(); /// Returns the URL for the current icon of the game -const std::string& GetGameIconURL(); - -/// Returns the path for the current icon of the game -const std::string& GetGameIconPath(); +const std::string& GetCurrentGameIconURL(); /// Returns the RetroAchievements title for the current game. /// Should be called with the lock held. -const std::string& GetGameTitle(); +const std::string& GetCurrentGameTitle(); /// Returns the path for the game that is current hashed/running. -const std::string& GetGamePath(); +const std::string& GetCurrentGamePath(); /// Returns true if the user has been successfully logged in. bool IsLoggedIn(); @@ -172,18 +169,18 @@ bool IsLoggedIn(); bool IsLoggedInOrLoggingIn(); /// Returns the logged-in user name. -const char* GetLoggedInUserName(); +const std::string& GetLoggedInUserName(); /// Returns the path to the user's profile avatar. /// Should be called with the lock held. -const std::string& GetLoggedInUserBadgePath(); +const std::string& GetLoggedInUserIconURL(); /// Returns a summary of the user's points. /// Should be called with the lock held. SmallString GetLoggedInUserPointsSummary(); -/// Returns the path to the local cache for the specified badge name. -std::string GetGameBadgePath(std::string_view badge_name); +/// Returns the URL for the specified game icon. +std::string GetGameIconURL(const char* badge_name); /// Downloads game icons from RetroAchievements for all games that have an achievements_game_id. /// This fetches the game badge images that are normally downloaded when a game is opened. diff --git a/src/core/achievements_private.h b/src/core/achievements_private.h index 591aeb331..a5ebf0b99 100644 --- a/src/core/achievements_private.h +++ b/src/core/achievements_private.h @@ -28,7 +28,7 @@ struct LeaderboardTrackerIndicator struct ActiveChallengeIndicator { const rc_client_achievement_t* achievement; - std::string badge_path; + std::string badge_url; float time_remaining; float opacity; bool active; @@ -37,7 +37,7 @@ struct ActiveChallengeIndicator struct AchievementProgressIndicator { const rc_client_achievement_t* achievement; - std::string badge_path; + std::string badge_url; float time; bool active; }; @@ -45,7 +45,7 @@ struct AchievementProgressIndicator struct PinnedAchievementIndicator { u32 achievement_id; - std::string badge_path; + std::string badge_url; }; /// Returns the rc_client instance. Should have the lock held. @@ -61,12 +61,10 @@ std::vector& GetPinnedAchievementIndicators(); bool IsAchievementPinned(u32 achievement_id); void SetAchievementPinned(u32 achievement_id, bool pinned); -std::string GetAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked, - bool download_if_missing = true); -std::string GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry); +std::string_view GetAchievementBadgeURL(const rc_client_achievement_t* achievement, bool locked); std::string_view GetLeaderboardFormatIcon(u32 format); - -std::string GetSubsetBadgePath(const rc_client_subset_t* subset); +std::string GetUserBadgeURL(const char* username); +std::string GetSubsetBadgeURL(const rc_client_subset_t* subset); } // namespace Achievements diff --git a/src/core/fullscreenui.cpp b/src/core/fullscreenui.cpp index 89644f161..3c4b27e8e 100644 --- a/src/core/fullscreenui.cpp +++ b/src/core/fullscreenui.cpp @@ -1324,17 +1324,16 @@ void FullscreenUI::DrawLandingTemplate(ImVec2* menu_pos, ImVec2* menu_size) if (Achievements::IsActive()) { const auto lock = Achievements::GetLock(); - const char* username = Achievements::GetLoggedInUserName(); - if (username) + if (const std::string& username = Achievements::GetLoggedInUserName(); !username.empty()) { const ImVec2 name_size = - heading_font->CalcTextSizeA(heading_font_size, heading_font_weight, FLT_MAX, 0.0f, username); + heading_font->CalcTextSizeA(heading_font_size, heading_font_weight, FLT_MAX, 0.0f, IMSTR_START_END(username)); const ImVec2 name_pos = ImVec2(time_pos.x - name_size.x - LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING), time_pos.y); RenderShadowedTextClipped(heading_font, heading_font_size, heading_font_weight, name_pos, name_pos + name_size, text_color, username, &name_size); - if (const std::string& badge_path = Achievements::GetLoggedInUserBadgePath(); !badge_path.empty()) + if (const std::string& badge_path = Achievements::GetLoggedInUserIconURL(); !badge_path.empty()) { const ImVec2 badge_size = ImVec2(UIStyle.LargeFontSize, UIStyle.LargeFontSize); const ImVec2 badge_pos = diff --git a/src/core/fullscreenui_achievements.cpp b/src/core/fullscreenui_achievements.cpp index da0b3d089..d2eb35428 100644 --- a/src/core/fullscreenui_achievements.cpp +++ b/src/core/fullscreenui_achievements.cpp @@ -9,6 +9,7 @@ #include "util/gpu_device.h" #include "util/gpu_texture.h" +#include "util/http_cache.h" #include "util/imgui_manager.h" #include "util/translation.h" @@ -57,7 +58,7 @@ struct Notification std::string title; std::string text; std::string note; - std::string badge_path; + std::string image_url; u64 start_time; u64 move_time; float duration; @@ -72,7 +73,7 @@ struct PauseMenuAchievementInfo { std::string title; std::string description; - std::string badge_path; + std::string badge_url; u32 achievement_id; float measured_percent; }; @@ -119,8 +120,6 @@ template static bool IsBucketVisibleInCurrentSubset(const T& bucket); static void SortLockedAchievements(); -static const std::string& GetCachedAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked); - template static void CachePauseMenuAchievementInfo(const rc_client_achievement_t* achievement, std::optional& value); @@ -164,8 +163,6 @@ struct AchievementsLocals std::vector subset_info_list; const SubsetInfo* open_subset = nullptr; - std::vector> achievement_badge_paths; - std::optional most_recent_unlock; std::optional achievement_nearest_completion; std::optional most_recent_progress_update; @@ -179,7 +176,6 @@ struct AchievementsLocals const rc_client_leaderboard_t* open_leaderboard = nullptr; rc_client_async_handle_t* leaderboard_fetch_handle = nullptr; std::vector leaderboard_entry_lists; - std::vector> leaderboard_user_icon_paths; rc_client_leaderboard_entry_list_t* leaderboard_nearby_entries; bool is_showing_all_leaderboard_entries = false; bool has_fetched_all_leaderboard_entries = false; @@ -204,9 +200,6 @@ void FullscreenUI::ClearAchievementsState() s_achievements_locals.notifications = {}; - s_achievements_locals.achievement_badge_paths = {}; - - s_achievements_locals.leaderboard_user_icon_paths = {}; s_achievements_locals.leaderboard_entry_lists = {}; if (s_achievements_locals.leaderboard_list) { @@ -251,8 +244,8 @@ void FullscreenUI::DrawAchievementsOverlays() } } -void FullscreenUI::AddAchievementNotification(std::string key, float duration, std::string image_path, - std::string title, std::string text, std::string note, +void FullscreenUI::AddAchievementNotification(std::string key, float duration, std::string image_url, std::string title, + std::string text, std::string note, AchievementNotificationNoteType note_type, u16 min_width, bool small_font) { const bool prev_had_notifications = s_achievements_locals.notifications.empty(); @@ -268,7 +261,7 @@ void FullscreenUI::AddAchievementNotification(std::string key, float duration, s it->title = std::move(title); it->text = std::move(text); it->note = std::move(note); - it->badge_path = std::move(image_path); + it->image_url = std::move(image_url); it->min_width = min_width; it->note_type = note_type; it->small_font = small_font; @@ -288,7 +281,7 @@ void FullscreenUI::AddAchievementNotification(std::string key, float duration, s notif.title = std::move(title); notif.text = std::move(text); notif.note = std::move(note); - notif.badge_path = std::move(image_path); + notif.image_url = std::move(image_url); notif.start_time = current_time; notif.move_time = current_time; notif.target_y = -1.0f; @@ -399,7 +392,8 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout) case AchievementNotificationNoteType::Image: note_font_size = 0.0f; note_font_weight = 0.0f; - note_image = GetCachedTexture(notif.note, static_cast(note_text_size), static_cast(note_text_size)); + note_image = + GetCachedTextureAsync(notif.note, static_cast(note_text_size), static_cast(note_text_size)); note_size = (note_image && note_image->GetWidth() > note_image->GetHeight()) ? ImVec2(note_text_size * (static_cast(note_image->GetWidth()) / static_cast(note_image->GetHeight())), @@ -496,9 +490,10 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout) const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding); const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size); - if (!notif.badge_path.empty()) + if (!notif.image_url.empty()) { - GPUTexture* tex = GetCachedTexture(notif.badge_path, static_cast(badge_size), static_cast(badge_size)); + GPUTexture* tex = + GetCachedTextureAsync(notif.image_url, static_cast(badge_size), static_cast(badge_size)); if (tex) { dl->AddImage(tex, badge_min, badge_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), @@ -607,7 +602,7 @@ void FullscreenUI::DrawIndicators(NotificationLayout& layout) indicator.opacity = (indicator.opacity != target_opacity) ? ImSaturate(indicator.opacity + (io.DeltaTime / rate)) : target_opacity; - GPUTexture* badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_path); + GPUTexture* badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_url); if (badge) { dl->AddImage(badge, current_position, current_position + ImVec2(image_size, image_size), ImVec2(0.0f, 0.0f), @@ -658,7 +653,7 @@ void FullscreenUI::DrawIndicators(NotificationLayout& layout) ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding); } - GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator->badge_path); + GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator->badge_url); if (badge) { const ImVec2 badge_pos = box_min + padding; @@ -843,7 +838,7 @@ void FullscreenUI::DrawIndicators(NotificationLayout& layout) const float total_width = (pinned_image_size + spacing + text_size.x); const float start_x = pos.x + ImFloor((box_width - total_width) * 0.5f); - GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_path); + GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_url); if (badge) { const ImVec2 badge_pos = ImVec2(start_x, pos.y); @@ -884,18 +879,6 @@ void FullscreenUI::UpdateAchievementOverlaysRunIdle() }); } -const std::string& FullscreenUI::GetCachedAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked) -{ - for (const auto& [l_cheevo, l_path, l_state] : s_achievements_locals.achievement_badge_paths) - { - if (l_cheevo == achievement && l_state == locked) - return l_path; - } - - std::string path = Achievements::GetAchievementBadgePath(achievement, locked); - return std::get<1>(s_achievements_locals.achievement_badge_paths.emplace_back(achievement, std::move(path), locked)); -} - template void FullscreenUI::CachePauseMenuAchievementInfo(const rc_client_achievement_t* achievement, std::optional& value) { @@ -911,7 +894,7 @@ void FullscreenUI::CachePauseMenuAchievementInfo(const rc_client_achievement_t* // have to take a copy because with RAIntegration the achievement pointer does not persist value->title = achievement->title; value->description = achievement->description; - value->badge_path = Achievements::GetAchievementBadgePath(achievement, false); + value->badge_url = Achievements::GetAchievementBadgeURL(achievement, false); value->measured_percent = achievement->measured_percent; value->achievement_id = achievement->id; @@ -1295,7 +1278,7 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) buffer.format(ICON_FA_LOCK_OPEN " {}", TRANSLATE_DISAMBIG_SV("Achievements", "Most Recent", "Pause Menu")); draw_achievement_in_box( buffer, s_achievements_locals.most_recent_unlock->title, s_achievements_locals.most_recent_unlock->description, - s_achievements_locals.most_recent_unlock->badge_path, {}, 0.0f, s_achievements_locals.most_recent_unlock->points); + s_achievements_locals.most_recent_unlock->badge_url, {}, 0.0f, s_achievements_locals.most_recent_unlock->points); // extra spacing if we have two text_pos.y += s_achievements_locals.achievement_nearest_completion ? (paragraph_spacing + paragraph_spacing) : 0.0f; @@ -1311,7 +1294,7 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) TRANSLATE_DISAMBIG_SV("Achievements", "Nearest Completion", "Pause Menu")); draw_achievement_in_box(buffer, s_achievements_locals.achievement_nearest_completion->title, s_achievements_locals.achievement_nearest_completion->description, - s_achievements_locals.achievement_nearest_completion->badge_path, + s_achievements_locals.achievement_nearest_completion->badge_url, s_achievements_locals.achievement_nearest_completion->measured_progress, s_achievements_locals.achievement_nearest_completion->measured_percent, 0); text_pos.y += paragraph_spacing; @@ -1327,7 +1310,7 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) TRANSLATE_DISAMBIG_SV("Achievements", "Last Progress Update", "Pause Menu")); draw_achievement_in_box(buffer, s_achievements_locals.most_recent_progress_update->title, s_achievements_locals.most_recent_progress_update->description, - s_achievements_locals.most_recent_progress_update->badge_path, + s_achievements_locals.most_recent_progress_update->badge_url, s_achievements_locals.most_recent_progress_update->measured_progress, s_achievements_locals.most_recent_progress_update->measured_percent, 0); text_pos.y += paragraph_spacing; @@ -1376,7 +1359,7 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) { text_pos.y += paragraph_spacing; draw_achievement_with_summary(indicator.achievement->title, indicator.achievement->description, - indicator.badge_path); + indicator.badge_url); text_pos.y += paragraph_spacing; } } @@ -1528,7 +1511,7 @@ void FullscreenUI::OpenAchievementsWindow() void FullscreenUI::AddSubsetInfo(const rc_client_subset_t* subset) { - const std::string_view game_title = Achievements::GetGameTitle(); + const std::string_view game_title = Achievements::GetCurrentGameTitle(); SubsetInfo info; info.subset_id = subset->id; @@ -1555,7 +1538,7 @@ void FullscreenUI::AddSubsetInfo(const rc_client_subset_t* subset) info.short_name = subset_title; } - info.badge_path = Achievements::GetSubsetBadgePath(subset); + info.badge_path = Achievements::GetSubsetBadgeURL(subset); info.num_leaderboards = subset->num_leaderboards; info.summary = {}; @@ -1745,8 +1728,6 @@ void FullscreenUI::SwitchToAchievements() return; } - s_achievements_locals.achievement_badge_paths = {}; - if (s_achievements_locals.achievement_list) rc_client_destroy_achievement_list(s_achievements_locals.achievement_list); s_achievements_locals.achievement_list = @@ -1813,9 +1794,9 @@ void FullscreenUI::DrawAchievementsWindow() const float spacing = LayoutScale(LAYOUT_MENU_ITEM_TITLE_SUMMARY_SPACING); const float image_size = LayoutScale(75.0f); - if (const std::string& path = Achievements::GetGameIconPath(); !path.empty()) + if (const std::string& icon = Achievements::GetCurrentGameIconURL(); !icon.empty()) { - GPUTexture* badge = GetCachedTextureAsync(path); + GPUTexture* badge = GetCachedTextureAsync(icon); if (badge) { ImGui::GetWindowDrawList()->AddImage(badge, pos, pos + ImVec2(image_size, image_size), ImVec2(0.0f, 0.0f), @@ -1837,7 +1818,7 @@ void FullscreenUI::DrawAchievementsWindow() (FloatingButton(ICON_FA_XMARK, 10.0f, 10.0f, 1.0f, 0.0f, true) || (!AreAnyDialogsOpen() && WantsToCloseMenu())); const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.LargeFontSize)); - text.assign(Achievements::GetGameTitle()); + text.assign(Achievements::GetCurrentGameTitle()); if (rc_client_get_hardcore_enabled(Achievements::GetClient())) text.append(TRANSLATE_SV("Achievements", " (Hardcore Mode)")); @@ -2194,7 +2175,14 @@ void FullscreenUI::DrawAchievement(const rc_client_achievement_t* cheevo, const if (!visible) { if (pos_y >= prefetch_range.x && pos_y <= prefetch_range.y) - GetCachedAchievementBadgePath(cheevo, !is_unlocked); + { + if (const std::string_view badge_url = Achievements::GetAchievementBadgeURL(cheevo, !is_unlocked); + !badge_url.empty()) + { + // prefill the cache + GetCachedTextureAsync(badge_url); + } + } return; } @@ -2207,9 +2195,9 @@ void FullscreenUI::DrawAchievement(const rc_client_achievement_t* cheevo, const ImDrawList* const dl = ImGui::GetWindowDrawList(); - if (const std::string& badge_path = GetCachedAchievementBadgePath(cheevo, !is_unlocked); !badge_path.empty()) + if (const std::string_view badge_url = Achievements::GetAchievementBadgeURL(cheevo, !is_unlocked); !badge_url.empty()) { - GPUTexture* badge = GetCachedTextureAsync(badge_path); + GPUTexture* badge = GetCachedTextureAsync(badge_url); if (badge) { const ImRect image_bb = CenterImage(ImRect(bb.Min, bb.Min + image_size), badge); @@ -2441,7 +2429,6 @@ void FullscreenUI::SwitchToLeaderboards() return; } - s_achievements_locals.achievement_badge_paths = {}; CloseLeaderboard(); if (s_achievements_locals.leaderboard_list) rc_client_destroy_leaderboard_list(s_achievements_locals.leaderboard_list); @@ -2490,7 +2477,7 @@ void FullscreenUI::DrawLeaderboardsWindow() const ImVec2 heading_pos = ImGui::GetCursorScreenPos() + ImGui::GetStyle().FramePadding; const float image_size = LayoutScale(75.0f); - if (const std::string& icon = Achievements::GetGameIconPath(); !icon.empty()) + if (const std::string& icon = Achievements::GetCurrentGameIconURL(); !icon.empty()) { GPUTexture* badge = GetCachedTextureAsync(icon); if (badge) @@ -2508,7 +2495,7 @@ void FullscreenUI::DrawLeaderboardsWindow() if (s_achievements_locals.open_subset) text.assign(s_achievements_locals.open_subset->full_name); else - text.assign(Achievements::GetGameTitle()); + text.assign(Achievements::GetCurrentGameTitle()); top += UIStyle.LargeFontSize + spacing_small; @@ -2899,23 +2886,15 @@ bool FullscreenUI::DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& ent const float icon_size = bb.Max.y - bb.Min.y; const ImRect icon_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); - GPUTexture* icon_tex = nullptr; - if (auto it = std::find_if(s_achievements_locals.leaderboard_user_icon_paths.begin(), - s_achievements_locals.leaderboard_user_icon_paths.end(), - [&entry](const auto& it) { return it.first == &entry; }); - it != s_achievements_locals.leaderboard_user_icon_paths.end()) - { - if (!it->second.empty()) - icon_tex = GetCachedTextureAsync(it->second); - } - else + + // Use an alias to avoid allocating a string for the URL every time. + text.format("__lb_user_{}", entry.user); + GPUTexture* icon_tex = FindCachedTexture(text); + if (!icon_tex) { - std::string path = Achievements::GetLeaderboardUserBadgePath(&entry); - if (!path.empty()) - { - icon_tex = GetCachedTextureAsync(path); - s_achievements_locals.leaderboard_user_icon_paths.emplace_back(&entry, std::move(path)); - } + std::string url = Achievements::GetUserBadgeURL(entry.user); + if (!url.empty()) + icon_tex = GetCachedTextureAsync(std::move(url), text); } if (icon_tex) { @@ -3177,8 +3156,6 @@ void FullscreenUI::FetchNextLeaderboardEntries() void FullscreenUI::CloseLeaderboard() { - s_achievements_locals.leaderboard_user_icon_paths.clear(); - for (auto iter = s_achievements_locals.leaderboard_entry_lists.rbegin(); iter != s_achievements_locals.leaderboard_entry_lists.rend(); ++iter) { diff --git a/src/core/fullscreenui_game_list.cpp b/src/core/fullscreenui_game_list.cpp index d69f9b392..dd848a58f 100644 --- a/src/core/fullscreenui_game_list.cpp +++ b/src/core/fullscreenui_game_list.cpp @@ -1057,8 +1057,8 @@ GPUTexture* FullscreenUI::GetGameListCover(const GameList::Entry* entry, bool fa if (fallback_to_achievements_icon && cover_it->second.empty() && Achievements::IsActive()) { const auto lock = Achievements::GetLock(); - if (Achievements::GetGamePath() == entry->path) - cover_it->second = Achievements::GetGameIconPath(); + if (Achievements::GetCurrentGamePath() == entry->path) + cover_it->second = Achievements::GetCurrentGameIconURL(); } } diff --git a/src/core/fullscreenui_private.h b/src/core/fullscreenui_private.h index 1d6fe68ab..b76730661 100644 --- a/src/core/fullscreenui_private.h +++ b/src/core/fullscreenui_private.h @@ -132,7 +132,7 @@ enum class AchievementNotificationNoteType : u8 }; /// Schedules an achievement notification to be shown. -void AddAchievementNotification(std::string key, float duration, std::string image_path, std::string title, +void AddAchievementNotification(std::string key, float duration, std::string image_url, std::string title, std::string text, std::string note = {}, AchievementNotificationNoteType note_type = AchievementNotificationNoteType::None, u16 min_width = 0, bool small_font = false); diff --git a/src/core/fullscreenui_settings.cpp b/src/core/fullscreenui_settings.cpp index 8b16a34e1..671c915f6 100644 --- a/src/core/fullscreenui_settings.cpp +++ b/src/core/fullscreenui_settings.cpp @@ -4924,15 +4924,15 @@ void FullscreenUI::DrawAchievementsSettingsHeader(SettingsInterface* bsi, std::u settings_lock.unlock(); { const auto lock = Achievements::GetLock(); - std::string_view badge_path = Achievements::GetLoggedInUserBadgePath(); + std::string_view badge_path = Achievements::GetLoggedInUserIconURL(); if (badge_path.empty()) badge_path = "images/ra-generic-user.png"; if (Achievements::IsLoggedIn()) { - const char* username_ptr = Achievements::GetLoggedInUserName(); - if (username_ptr) - tstr = username_ptr; + std::string_view username_sv = Achievements::GetLoggedInUserName(); + if (!username_sv.empty()) + tstr = username_sv; } else if (Achievements::IsLoggedInOrLoggingIn()) { diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 1a560ad3d..beb62f20b 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -2166,7 +2166,7 @@ std::string GameList::GetGameIconPath(std::string_view custom_title, std::string std::string fallback_path; if (achievements_game_id != 0) { - fallback_path = GetAchievementGameBadgePath(achievements_game_id); + fallback_path = GetAchievementGameBadgeURL(achievements_game_id); if (!fallback_path.empty() && PreferAchievementGameBadgesForIcons()) return (ret = std::move(fallback_path)); } @@ -2312,12 +2312,10 @@ std::string GameList::GetAchievementGameBadgeCachePath() return Path::Combine(EmuFolders::Cache, "achievement_game_badges.cache"); } -std::string GameList::GetAchievementGameBadgePath(u32 game_id) +std::string GameList::GetAchievementGameBadgeURL(u32 game_id) { LoadAchievementGameBadges(); - std::string ret; - const auto iter = std::lower_bound(s_state.achievement_game_id_badges.begin(), s_state.achievement_game_id_badges.end(), game_id, [](const auto& entry, u32 search) { return entry.first < search; }); @@ -2325,14 +2323,10 @@ std::string GameList::GetAchievementGameBadgePath(u32 game_id) { const std::string_view badge_name = s_state.achievement_game_badge_names.GetString(iter->second); if (!badge_name.empty()) - { - ret = Achievements::GetGameBadgePath(badge_name); - if (!FileSystem::FileExists(ret.c_str())) - ret.clear(); - } + return Achievements::GetGameIconURL(TinyString(badge_name).c_str()); } - return ret; + return {}; } void GameList::LoadAchievementGameBadges() diff --git a/src/core/game_list.h b/src/core/game_list.h index 858ca16c3..2f31b2b6b 100644 --- a/src/core/game_list.h +++ b/src/core/game_list.h @@ -185,7 +185,7 @@ void UpdateAllAchievementData(); /// Accesses achievement game badges. Assumes the lock is held. bool PreferAchievementGameBadgesForIcons(); -std::string GetAchievementGameBadgePath(u32 game_id); +std::string GetAchievementGameBadgeURL(u32 game_id); void UpdateAchievementBadgeName(u32 game_id, std::string_view badge_name); } // namespace GameList diff --git a/src/core/settings.cpp b/src/core/settings.cpp index d2a7d89d8..180b675ec 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -2808,7 +2808,6 @@ void EmuFolders::EnsureFoldersExist() { EnsureFolderExists(Bios); EnsureFolderExists(Cache); - EnsureFolderExists(Path::Combine(Cache, "achievement_images")); EnsureFolderExists(Cheats); EnsureFolderExists(Covers); EnsureFolderExists(GameIcons); diff --git a/src/core/system.cpp b/src/core/system.cpp index cadc451e9..11922bfb3 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -6391,7 +6391,7 @@ std::string System::GetImageForLoadingScreen(const std::string& game_path, if (fallback_to_achievement_game_icon && entry->achievements_game_id != 0) { - path = GameList::GetAchievementGameBadgePath(entry->achievements_game_id); + path = GameList::GetAchievementGameBadgeURL(entry->achievements_game_id); if (!path.empty()) return path; } @@ -6580,7 +6580,7 @@ void System::UpdateRichPresence(bool update_session_time) if (Achievements::HasRichPresence()) rp.state = (state_string = StringUtil::Ellipsise(Achievements::GetRichPresenceString(), 128)).c_str(); - if (const std::string& icon_url = Achievements::GetGameIconURL(); !icon_url.empty()) + if (const std::string& icon_url = Achievements::GetCurrentGameIconURL(); !icon_url.empty()) rp.largeImageKey = icon_url.c_str(); dyn_libs::Discord_UpdatePresence(&rp); diff --git a/src/duckstation-qt/achievementsettingswidget.cpp b/src/duckstation-qt/achievementsettingswidget.cpp index a1906d058..6b1c91b8c 100644 --- a/src/duckstation-qt/achievementsettingswidget.cpp +++ b/src/duckstation-qt/achievementsettingswidget.cpp @@ -302,30 +302,25 @@ void AchievementSettingsWidget::onLeaderboardsNotificationDurationSliderChanged( void AchievementSettingsWidget::updateLoginState() { - std::string username; - std::string badge_path; + m_ui.userBadge->setPixmap(QPixmap(QtHost::GetResourceQPath("images/ra-generic-user.png", true))); + + QString qusername; + QString qbadge_path; { const auto lock = Achievements::GetLock(); if (Achievements::IsLoggedIn()) { - if (const char* username_ptr = Achievements::GetLoggedInUserName()) - username = username_ptr; - - badge_path = Achievements::GetLoggedInUserBadgePath(); + qusername = QString::fromStdString(Achievements::GetLoggedInUserName()); + QtUtils::SetLabelPixmapPathOrURL(m_ui.userBadge, Achievements::GetLoggedInUserIconURL(), true); } else { - username = Core::GetBaseStringSettingValue("Cheevos", "Username"); + qusername = QString::fromStdString(Core::GetBaseStringSettingValue("Cheevos", "Username")); } } - if (badge_path.empty()) - badge_path = QtHost::GetResourcePath("images/ra-generic-user.png", true); - - m_ui.userBadge->setPixmap(QPixmap(QString::fromStdString(badge_path))); - - const bool logged_in = !username.empty(); + const bool logged_in = !qusername.isEmpty(); if (logged_in) { @@ -333,8 +328,7 @@ void AchievementSettingsWidget::updateLoginState() StringUtil::FromChars(Core::GetBaseStringSettingValue("Cheevos", "LoginTimestamp", "0")).value_or(0); const QString login_timestamp = QtHost::FormatNumber(Host::NumberFormatType::ShortDateTime, static_cast(login_unix_timestamp)); - m_ui.loginStatus->setText( - tr("Logged in as %1\nToken generated at %2").arg(QString::fromStdString(username)).arg(login_timestamp)); + m_ui.loginStatus->setText(tr("Logged in as %1\nToken generated at %2").arg(qusername).arg(login_timestamp)); m_ui.loginButton->setText(tr("Logout")); } else diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index 4a8ebf2f3..65cb8537f 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "gamelistwidget.h" +#include "asyncpixmaploader.h" #include "mainwindow.h" #include "qthost.h" #include "qtprogresscallback.h" @@ -17,6 +18,7 @@ #include "core/system.h" #include "util/animated_image.h" +#include "util/http_cache.h" #include "util/translation.h" #include "common/assert.h" @@ -653,16 +655,62 @@ const QPixmap* GameListModel::lookupIconPixmapForEntry(const GameList::Entry* ge { // Assumes game list lock is held. const std::string path = GameList::GetGameIconPath(ge); - QPixmap pm; - if (!path.empty() && pm.load(QString::fromStdString(path))) + if (HTTPCache::IsHTTPURL(path)) { - pm.setDevicePixelRatio(m_device_pixel_ratio); - resizeGameIcon(pm, m_icon_size); - return m_icon_pixmap_cache.Insert(ge->serial, std::move(pm)); + if (AsyncPixmapLoader::isQueueNeeded(path)) + { + // callback can fire immediately, so fill it first + m_icon_pixmap_cache.Insert(ge->serial, {}); + + AsyncPixmapLoader* loader = new AsyncPixmapLoader(); + connect(loader, &AsyncPixmapLoader::pixmapLoaded, this, [this, serial = ge->serial](QPixmap& pm) mutable { + if (pm.isNull()) + return; + + pm.setDevicePixelRatio(m_device_pixel_ratio); + resizeGameIcon(pm, m_icon_size); + m_icon_pixmap_cache.Insert(serial, pm); + + // invalidate rows with this serial + const auto lock = GameList::GetLock(); + for (size_t i = 0, count = GameList::GetEntryCount(); i < count; i++) + { + const GameList::Entry* entry = GameList::GetEntryByIndex(i); + if (entry->serial != serial) + continue; + + const QModelIndex idx = index(static_cast(i), Column_Icon); + emit const_cast(this)->dataChanged(idx, idx, getRolesToInvalidate(Column_Icon)); + } + }); + + loader->enqueue(path); + + // just in case it fires immediately + item = m_icon_pixmap_cache.Lookup(ge->serial); + return (item && !item->isNull()) ? item : nullptr; + } + else + { + QPixmap pm = AsyncPixmapLoader::load(path); + pm.setDevicePixelRatio(m_device_pixel_ratio); + resizeGameIcon(pm, m_icon_size); + return m_icon_pixmap_cache.Insert(ge->serial, std::move(pm)); + } } + else + { + QPixmap pm; + if (!path.empty() && pm.load(QString::fromStdString(path))) + { + pm.setDevicePixelRatio(m_device_pixel_ratio); + resizeGameIcon(pm, m_icon_size); + return m_icon_pixmap_cache.Insert(ge->serial, std::move(pm)); + } - // Stop it trying again in the future. - m_icon_pixmap_cache.Insert(ge->serial, {}); + // Stop it trying again in the future. + m_icon_pixmap_cache.Insert(ge->serial, {}); + } } } @@ -729,9 +777,24 @@ QIcon GameListModel::getIconForGame(const QString& path) } } + // If it's not a HTTP URL, this is straightforward. const std::string icon_path = GameList::GetGameIconPath(entry); - if (!icon_path.empty()) + if (HTTPCache::IsHTTPURL(icon_path)) + { + // Can't really download here since it's not asynchronous. But we can still check the cache. + const HTTPCache::LookupResult result = HTTPCache::Lookup(icon_path, nullptr); + if (result.has_value()) + { + QPixmap pm; + const TinyString extension(Path::GetExtension(HTTPCache::GetURLFilename(icon_path))); + if (pm.loadFromData(result->data(), static_cast(result->size()), extension.c_str())) + ret = QIcon(pm); + } + } + else if (!icon_path.empty()) + { ret = QIcon(QString::fromStdString(icon_path)); + } return ret; }