Achievements: Use HTTP cache instead of loose files

pull/3715/head
Stenzek 6 months ago
parent 10a8f5e195
commit 32ab1b70f9
No known key found for this signature in database

@ -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<u8> SaveStateToBuffer();
static void LoadStateFromBuffer(std::span<const u8> data, std::unique_lock<std::recursive_mutex>& lock);
static bool SaveStateToBuffer(std::span<u8> 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<LeaderboardTrackerIndicator> active_leaderboard_trackers;
std::vector<ActiveChallengeIndicator> active_challenge_indicators;
std::optional<AchievementProgressIndicator> active_progress_indicator;
std::vector<PinnedAchievementIndicator> 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<GameHash> 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<GameHash> 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<std::pair<std::string, std::string>> 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<float>(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<float>(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<float>(g_settings.achievements_leaderboard_duration), s_state.game_icon,
static_cast<float>(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<float>(g_settings.achievements_leaderboard_duration), s_state.game_icon,
static_cast<float>(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<const u8> 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

@ -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.

@ -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<PinnedAchievementIndicator>& 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

@ -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 =

@ -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<typename T>
static bool IsBucketVisibleInCurrentSubset(const T& bucket);
static void SortLockedAchievements();
static const std::string& GetCachedAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked);
template<typename T>
static void CachePauseMenuAchievementInfo(const rc_client_achievement_t* achievement, std::optional<T>& value);
@ -164,8 +163,6 @@ struct AchievementsLocals
std::vector<SubsetInfo> subset_info_list;
const SubsetInfo* open_subset = nullptr;
std::vector<std::tuple<const void*, std::string, bool>> achievement_badge_paths;
std::optional<PauseMenuAchievementInfoWithPoints> most_recent_unlock;
std::optional<PauseMenuMeasuredAchievementInfo> achievement_nearest_completion;
std::optional<PauseMenuTimedMeasuredAchievementInfo> 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<rc_client_leaderboard_entry_list_t*> leaderboard_entry_lists;
std::vector<std::pair<const rc_client_leaderboard_entry_t*, std::string>> 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<u32>(note_text_size), static_cast<u32>(note_text_size));
note_image =
GetCachedTextureAsync(notif.note, static_cast<u32>(note_text_size), static_cast<u32>(note_text_size));
note_size = (note_image && note_image->GetWidth() > note_image->GetHeight()) ?
ImVec2(note_text_size * (static_cast<float>(note_image->GetWidth()) /
static_cast<float>(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<u32>(badge_size), static_cast<u32>(badge_size));
GPUTexture* tex =
GetCachedTextureAsync(notif.image_url, static_cast<u32>(badge_size), static_cast<u32>(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<typename T>
void FullscreenUI::CachePauseMenuAchievementInfo(const rc_client_achievement_t* achievement, std::optional<T>& 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)
{

@ -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();
}
}

@ -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);

@ -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())
{

@ -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()

@ -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

@ -2808,7 +2808,6 @@ void EmuFolders::EnsureFoldersExist()
{
EnsureFolderExists(Bios);
EnsureFolderExists(Cache);
EnsureFolderExists(Path::Combine(Cache, "achievement_images"));
EnsureFolderExists(Cheats);
EnsureFolderExists(Covers);
EnsureFolderExists(GameIcons);

@ -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);

@ -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<u64>(Core::GetBaseStringSettingValue("Cheevos", "LoginTimestamp", "0")).value_or(0);
const QString login_timestamp =
QtHost::FormatNumber(Host::NumberFormatType::ShortDateTime, static_cast<s64>(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

@ -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<int>(i), Column_Icon);
emit const_cast<GameListModel*>(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<int>(result->size()), extension.c_str()))
ret = QIcon(pm);
}
}
else if (!icon_path.empty())
{
ret = QIcon(QString::fromStdString(icon_path));
}
return ret;
}

Loading…
Cancel
Save