diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index dec0ca201..167a61f5e 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -35,6 +35,7 @@ #include "common/scoped_guard.h" #include "common/sha256_digest.h" #include "common/small_string.h" +#include "common/string_pool.h" #include "common/string_util.h" #include "common/timer.h" @@ -203,6 +204,10 @@ static void BuildProgressDatabase(const rc_client_all_user_progress_t* allprog); static void UpdateProgressDatabase(); static void ClearProgressDatabase(); +static std::string GetGameBadgeNamesCachePath(); +static void LoadGameBadgeNames(); +static void UpdateGameBadgeName(u32 game_id, std::string_view badge_name); + static std::string GetPinnedAchievementsPath(u32 game_id); static void LoadPinnedAchievements(); static void SavePinnedAchievements(); @@ -226,6 +231,7 @@ struct State bool has_rich_presence : 1 = false; bool has_saved_credentials : 1 = false; bool hashdb_loaded : 1 = false; + bool game_badge_names_loaded : 1 = false; bool reload_game_on_reset : 1 = false; std::recursive_mutex mutex; // large @@ -256,6 +262,10 @@ struct State std::vector hashdb_entries; + // TODO: Turn this into a proper cache of achievement data, not just the badge names. + std::vector> game_badge_names; // game_id, string_pool_offset + BumpStringPool game_badge_name_strings; + rc_client_async_handle_t* fetch_hash_library_request = nullptr; rc_client_hash_library_t* fetch_hash_library_result = nullptr; rc_client_async_handle_t* fetch_all_progress_request = nullptr; @@ -1322,7 +1332,7 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, s_state.game_badge_url = info->badge_url ? std::string(info->badge_url) : GetImageURL(info->badge_name, RC_IMAGE_TYPE_GAME); if (info->badge_name) - GameList::UpdateAchievementBadgeName(info->id, info->badge_name); + UpdateGameBadgeName(info->id, info->badge_name); // update progress database on first load, in case it was played on another PC UpdateGameSummary(true); @@ -2261,11 +2271,6 @@ SmallString Achievements::GetLoggedInUserPointsSummary() return ret; } -std::string Achievements::GetGameIconURL(const char* badge_name) -{ - return GetImageURL(badge_name, RC_IMAGE_TYPE_GAME); -} - bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error) { // Collect all unique game IDs that don't have icons yet @@ -2277,7 +2282,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::GetAchievementGameBadgeURL(entry.achievements_game_id); + const std::string existing_badge = GetGameBadgeURL(entry.achievements_game_id); if (existing_badge.empty() && std::find(game_ids.begin(), game_ids.end(), entry.achievements_game_id) == game_ids.end()) { @@ -2333,7 +2338,7 @@ bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error) if (badge_name.empty()) continue; - GameList::UpdateAchievementBadgeName(entry.game_id, badge_name); + UpdateGameBadgeName(entry.game_id, badge_name); const std::string url = entry.badge_url ? std::string(entry.badge_url) : GetImageURL(entry.badge_name, RC_IMAGE_TYPE_GAME); @@ -3345,6 +3350,144 @@ const Achievements::ProgressDatabase::Entry* Achievements::ProgressDatabase::Loo return (iter != m_entries.end() && iter->game_id == game_id) ? &(*iter) : nullptr; } +std::string Achievements::GetGameBadgeNamesCachePath() +{ + return Path::Combine(EmuFolders::Cache, "achievement_game_badges.cache"); +} + +std::string Achievements::GetGameBadgeURL(u32 game_id) +{ + const auto lock = GetLock(); + + // don't allow use of achievement badges if we're not logged in to achievements + if (!s_state.has_saved_credentials) + return {}; + + LoadGameBadgeNames(); + + const auto iter = std::lower_bound(s_state.game_badge_names.begin(), s_state.game_badge_names.end(), game_id, + [](const auto& entry, u32 search) { return entry.first < search; }); + if (iter != s_state.game_badge_names.end() && iter->first == game_id) + { + const std::string_view badge_name = s_state.game_badge_name_strings.GetString(iter->second); + if (!badge_name.empty()) + return GetImageURL(TinyString(badge_name).c_str(), RC_IMAGE_TYPE_GAME); + } + + return {}; +} + +void Achievements::LoadGameBadgeNames() +{ + if (s_state.game_badge_names_loaded) + return; + + s_state.game_badge_names_loaded = true; + + Error error; + FileSystem::LockedFile fp = FileSystem::OpenLockedFile(GetGameBadgeNamesCachePath().c_str(), false, &error); + if (!fp) + { + ERROR_LOG("Failed to load cache: {}", error.GetDescription()); + return; + } + + // avoid heap allocations by using the file size as a guide + static constexpr u32 MAX_RESERVE_SIZE = 1 * 1024 * 1024; + s_state.game_badge_name_strings.Reserve( + static_cast(std::clamp(FileSystem::FSize64(fp.get()), 0, MAX_RESERVE_SIZE))); + + char line[256]; + while (std::fgets(line, sizeof(line), fp.get())) + { + const std::string_view line_sv = StringUtil::StripWhitespace(line); + if (line_sv.empty()) + continue; + + const std::string_view::size_type pos = line_sv.find(','); + if (pos != std::string_view::npos) + { + const std::optional game_id = StringUtil::FromChars(line_sv.substr(0, pos)); + const std::string_view badge_name = StringUtil::StripWhitespace(line_sv.substr(pos + 1)); + if (game_id.has_value() && !badge_name.empty()) + { + s_state.game_badge_names.emplace_back(game_id.value(), + static_cast(s_state.game_badge_name_strings.AddString(badge_name))); + continue; + } + } + + WARNING_LOG("Malformed line in cache: '{}'", line_sv); + } + + DEV_LOG("Loaded {} achievement badge names", s_state.game_badge_names.size()); + + // the file may not be sorted, so sort it now. + std::sort(s_state.game_badge_names.begin(), s_state.game_badge_names.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); +} + +void Achievements::UpdateGameBadgeName(u32 game_id, std::string_view badge_name) +{ + if (game_id == 0) + return; + + LoadGameBadgeNames(); + + const auto iter = std::lower_bound(s_state.game_badge_names.begin(), s_state.game_badge_names.end(), game_id, + [](const auto& entry, u32 search) { return entry.first < search; }); + bool game_exists = false; + if (iter != s_state.game_badge_names.end() && iter->first == game_id) + { + if (s_state.game_badge_name_strings.GetString(iter->second) == badge_name) + return; + + iter->second = static_cast(s_state.game_badge_name_strings.AddString(badge_name)); + game_exists = true; + } + else + { + s_state.game_badge_names.insert(iter, + {game_id, static_cast(s_state.game_badge_name_strings.AddString(badge_name))}); + } + + Error error; + FileSystem::LockedFile fp = FileSystem::OpenLockedFile(GetGameBadgeNamesCachePath().c_str(), true, &error); + if (!fp) + { + ERROR_LOG("Failed to open cache for update: {}", error.GetDescription()); + return; + } + + // this is really terrible, but the case where a badge name changes is so rare that it's not worth handling well + if (game_exists) + { + if (!FileSystem::FTruncate64(fp.get(), 0, &error)) + { + ERROR_LOG("Failed to truncate cache: {}", error.GetDescription()); + return; + } + + for (const auto& entry : s_state.game_badge_names) + { + const std::string_view entry_badge = s_state.game_badge_name_strings.GetString(entry.second); + if (std::fprintf(fp.get(), "%u,%.*s\n", entry.first, static_cast(entry_badge.size()), entry_badge.data()) < + 0) + { + ERROR_LOG("Failed to rewrite cache: errno {}", errno); + } + } + } + else + { + if (!FileSystem::FSeek64(fp.get(), 0, SEEK_END, &error) || + std::fprintf(fp.get(), "%u,%.*s\n", game_id, static_cast(badge_name.size()), badge_name.data()) < 0) + { + ERROR_LOG("Failed to append to cache: errno {}", errno); + } + } +} + std::string Achievements::GetPinnedAchievementsPath(u32 game_id) { return Path::Combine(EmuFolders::GameSettings, fmt::format("{}_achievements.ini", game_id)); diff --git a/src/core/achievements.h b/src/core/achievements.h index aeae9d179..381f6b311 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -182,8 +182,8 @@ const std::string& GetLoggedInUserIconURL(); /// Should be called with the lock held. SmallString GetLoggedInUserPointsSummary(); -/// Returns the URL for the specified game icon. -std::string GetGameIconURL(const char* badge_name); +/// Returns the URL for the specified game icon, using the game ID. +std::string GetGameBadgeURL(u32 game_id); /// 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/game_list.cpp b/src/core/game_list.cpp index b2d961ef3..53dab47be 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -145,22 +145,13 @@ static bool PutCustomPropertiesField(INISettingsInterface& ini, const std::strin static std::string GetMemcardTimestampCachePath(); static bool UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& entry); -static std::string GetAchievementGameBadgeCachePath(); -static void LoadAchievementGameBadges(); - struct State { EntryList entries; std::recursive_mutex mutex; CacheMap cache_map; std::vector memcard_timestamp_cache_entries; - - // TODO: Turn this into a proper cache of achievement data, not just the badge names. - std::vector> achievement_game_id_badges; // game_id, string_pool_offset - BumpStringPool achievement_game_badge_names; - bool game_list_loaded = false; - bool achievement_game_badges_loaded = false; }; ALIGN_TO_CACHE_LINE static State s_state; @@ -208,8 +199,7 @@ bool GameList::ShouldLoadAchievementsProgress() bool GameList::PreferAchievementGameBadgesForIcons() { - return (ShouldLoadAchievementsProgress() && - Core::GetBaseBoolSettingValue("UI", "GameListPreferAchievementGameBadgesForIcons", false)); + return (Core::GetBaseBoolSettingValue("UI", "GameListPreferAchievementGameBadgesForIcons", false)); } bool GameList::IsScannableFilename(std::string_view path) @@ -2166,7 +2156,7 @@ std::string GameList::GetGameIconPath(std::string_view custom_title, std::string std::string fallback_path; if (achievements_game_id != 0) { - fallback_path = GetAchievementGameBadgeURL(achievements_game_id); + fallback_path = Achievements::GetGameBadgeURL(achievements_game_id); if (!fallback_path.empty() && PreferAchievementGameBadgesForIcons()) return (ret = std::move(fallback_path)); } @@ -2306,143 +2296,3 @@ bool GameList::UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& ent // append it. return (std::fwrite(&entry, sizeof(entry), 1, fp.get()) == 1); } - -std::string GameList::GetAchievementGameBadgeCachePath() -{ - return Path::Combine(EmuFolders::Cache, "achievement_game_badges.cache"); -} - -std::string GameList::GetAchievementGameBadgeURL(u32 game_id) -{ - LoadAchievementGameBadges(); - - // don't allow use of achievement badges if we're not logged in to achievements - if (!Achievements::HasSavedCredentials()) - return {}; - - 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; }); - if (iter != s_state.achievement_game_id_badges.end() && iter->first == game_id) - { - const std::string_view badge_name = s_state.achievement_game_badge_names.GetString(iter->second); - if (!badge_name.empty()) - return Achievements::GetGameIconURL(TinyString(badge_name).c_str()); - } - - return {}; -} - -void GameList::LoadAchievementGameBadges() -{ - if (s_state.achievement_game_badges_loaded) - return; - - s_state.achievement_game_badges_loaded = true; - - Error error; - FileSystem::LockedFile fp = FileSystem::OpenLockedFile(GetAchievementGameBadgeCachePath().c_str(), false, &error); - if (!fp) - { - ERROR_LOG("Failed to load cache: {}", error.GetDescription()); - return; - } - - // avoid heap allocations by using the file size as a guide - static constexpr u32 MAX_RESERVE_SIZE = 1 * 1024 * 1024; - s_state.achievement_game_badge_names.Reserve( - static_cast(std::clamp(FileSystem::FSize64(fp.get()), 0, MAX_RESERVE_SIZE))); - - char line[256]; - while (std::fgets(line, sizeof(line), fp.get())) - { - const std::string_view line_sv = StringUtil::StripWhitespace(line); - if (line_sv.empty()) - continue; - - const std::string_view::size_type pos = line_sv.find(','); - if (pos != std::string_view::npos) - { - const std::optional game_id = StringUtil::FromChars(line_sv.substr(0, pos)); - const std::string_view badge_name = StringUtil::StripWhitespace(line_sv.substr(pos + 1)); - if (game_id.has_value() && !badge_name.empty()) - { - s_state.achievement_game_id_badges.emplace_back( - game_id.value(), static_cast(s_state.achievement_game_badge_names.AddString(badge_name))); - continue; - } - } - - WARNING_LOG("Malformed line in cache: '{}'", line_sv); - } - - DEV_LOG("Loaded {} achievement badge names", s_state.achievement_game_id_badges.size()); - - // the file may not be sorted, so sort it now. - std::sort(s_state.achievement_game_id_badges.begin(), s_state.achievement_game_id_badges.end(), - [](const auto& a, const auto& b) { return a.first < b.first; }); -} - -void GameList::UpdateAchievementBadgeName(u32 game_id, std::string_view badge_name) -{ - if (game_id == 0) - return; - - std::unique_lock lock(s_state.mutex); - - LoadAchievementGameBadges(); - - 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; }); - bool game_exists = false; - if (iter != s_state.achievement_game_id_badges.end() && iter->first == game_id) - { - if (s_state.achievement_game_badge_names.GetString(iter->second) == badge_name) - return; - - iter->second = static_cast(s_state.achievement_game_badge_names.AddString(badge_name)); - game_exists = true; - } - else - { - s_state.achievement_game_id_badges.insert( - iter, {game_id, static_cast(s_state.achievement_game_badge_names.AddString(badge_name))}); - } - - Error error; - FileSystem::LockedFile fp = FileSystem::OpenLockedFile(GetAchievementGameBadgeCachePath().c_str(), true, &error); - if (!fp) - { - ERROR_LOG("Failed to open cache for update: {}", error.GetDescription()); - return; - } - - // this is really terrible, but the case where a badge name changes is so rare that it's not worth handling well - if (game_exists) - { - if (!FileSystem::FTruncate64(fp.get(), 0, &error)) - { - ERROR_LOG("Failed to truncate cache: {}", error.GetDescription()); - return; - } - - for (const auto& entry : s_state.achievement_game_id_badges) - { - const std::string_view entry_badge = s_state.achievement_game_badge_names.GetString(entry.second); - if (std::fprintf(fp.get(), "%u,%.*s\n", entry.first, static_cast(entry_badge.size()), entry_badge.data()) < - 0) - { - ERROR_LOG("Failed to rewrite cache: errno {}", errno); - } - } - } - else - { - if (!FileSystem::FSeek64(fp.get(), 0, SEEK_END, &error) || - std::fprintf(fp.get(), "%u,%.*s\n", game_id, static_cast(badge_name.size()), badge_name.data()) < 0) - { - ERROR_LOG("Failed to append to cache: errno {}", errno); - } - } -} diff --git a/src/core/game_list.h b/src/core/game_list.h index 2f31b2b6b..fa0785da3 100644 --- a/src/core/game_list.h +++ b/src/core/game_list.h @@ -185,8 +185,6 @@ void UpdateAllAchievementData(); /// Accesses achievement game badges. Assumes the lock is held. bool PreferAchievementGameBadgesForIcons(); -std::string GetAchievementGameBadgeURL(u32 game_id); -void UpdateAchievementBadgeName(u32 game_id, std::string_view badge_name); } // namespace GameList diff --git a/src/core/system.cpp b/src/core/system.cpp index dffb0eb57..89629d4c8 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -6389,7 +6389,7 @@ std::string System::GetImageForLoadingScreen(const std::string& game_path, if (fallback_to_achievement_game_icon && entry->achievements_game_id != 0) { - path = GameList::GetAchievementGameBadgeURL(entry->achievements_game_id); + path = Achievements::GetGameBadgeURL(entry->achievements_game_id); if (!path.empty()) return path; }