GameList: Fall back to achievement badges for game icons

pull/3588/head
Stenzek 12 months ago
parent ac4634c289
commit a035c48721
No known key found for this signature in database

@ -1325,9 +1325,13 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message,
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() &&
!FileSystem::FileExists(s_state.game_icon.c_str()))
DownloadImage(s_state.game_icon_url, s_state.game_icon);
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);
GameList::UpdateAchievementBadgeName(info->id, info->badge_name);
}
// update progress database on first load, in case it was played on another PC
UpdateGameSummary(true);
@ -2303,6 +2307,11 @@ SmallString Achievements::GetLoggedInUserPointsSummary()
return ret;
}
std::string Achievements::GetGameBadgePath(std::string_view badge_name)
{
return GetLocalImagePath(badge_name, RC_IMAGE_TYPE_GAME);
}
u32 Achievements::GetPauseThrottleFrames()
{
if (!IsActive() || !IsHardcoreModeActive())

@ -182,6 +182,9 @@ std::string GetLoggedInUserBadgePath();
/// 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 0 if pausing is allowed, otherwise the number of frames until pausing is allowed.
u32 GetPauseThrottleFrames();

@ -8994,7 +8994,7 @@ GPUTexture* FullscreenUI::GetGameListCover(const GameList::Entry* entry, bool fa
cover_it = s_state.icon_image_map.find(entry->serial);
if (cover_it == s_state.icon_image_map.end())
{
std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path);
std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path, entry->achievements_game_id);
cover_it = s_state.icon_image_map.emplace(entry->serial, std::move(icon_path)).first;
}
}

@ -26,6 +26,7 @@
#include "common/log.h"
#include "common/path.h"
#include "common/progress_callback.h"
#include "common/string_pool.h"
#include "common/string_util.h"
#include "common/thirdparty/SmallVector.h"
#include "common/time_helpers.h"
@ -137,6 +138,9 @@ 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;
@ -144,7 +148,12 @@ struct State
CacheMap cache_map;
std::vector<MemcardTimestampCacheEntry> memcard_timestamp_cache_entries;
// TODO: Turn this into a proper cache of achievement data, not just the badge names.
std::vector<std::pair<u32, u32>> 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;
@ -185,6 +194,17 @@ bool GameList::ShouldShowLocalizedTitles()
return Host::GetBaseBoolSettingValue("UI", "GameListShowLocalizedTitles", true);
}
bool GameList::ShouldLoadAchievementsProgress()
{
return Host::ContainsBaseSettingValue("Cheevos", "Token");
}
bool GameList::PreferAchievementGameBadgesForIcons()
{
return (ShouldLoadAchievementsProgress() &&
Host::GetBaseBoolSettingValue("UI", "GameListPreferAchievementGameBadgesForIcons", false));
}
bool GameList::IsScannableFilename(std::string_view path)
{
// we don't scan bin files because they'll duplicate
@ -194,11 +214,6 @@ bool GameList::IsScannableFilename(std::string_view path)
return (System::IsDiscPath(path) || System::IsExePath(path) || System::IsPsfPath(path));
}
bool GameList::ShouldLoadAchievementsProgress()
{
return Host::ContainsBaseSettingValue("Cheevos", "Token");
}
bool GameList::GetExeListEntry(const std::string& path, GameList::Entry* entry)
{
const auto fp = FileSystem::OpenManagedCFile(path.c_str(), "rb");
@ -2004,12 +2019,20 @@ void GameList::ReloadMemcardTimestampCache()
entry.serial[sizeof(entry.serial) - 1] = 0;
}
std::string GameList::GetGameIconPath(std::string_view serial, std::string_view path)
std::string GameList::GetGameIconPath(std::string_view serial, std::string_view path, u32 achievements_game_id)
{
std::string ret;
std::string fallback_path;
if (achievements_game_id != 0)
{
fallback_path = GetAchievementGameBadgePath(achievements_game_id);
if (!fallback_path.empty() && PreferAchievementGameBadgesForIcons())
return (ret = std::move(fallback_path));
}
if (serial.empty())
return ret;
return (ret = std::move(fallback_path));
// might exist already, or the user used a custom icon
ret = Path::Combine(EmuFolders::GameIcons, TinyString::from_format("{}.png", serial));
@ -2022,8 +2045,7 @@ std::string GameList::GetGameIconPath(std::string_view serial, std::string_view
if (memcard_path.empty() || type == MemoryCardType::Shared ||
!FileSystem::StatFile(memcard_path.c_str(), &memcard_sd))
{
ret = {};
return ret;
return (ret = std::move(fallback_path));
}
const s64 timestamp = memcard_sd.ModificationTime;
@ -2039,10 +2061,7 @@ std::string GameList::GetGameIconPath(std::string_view serial, std::string_view
// user might've deleted the file, so re-extract it if so
// otherwise, card hasn't changed, still no icon
if (entry.memcard_timestamp == timestamp && !entry.icon_was_extracted)
{
ret = {};
return ret;
}
return (ret = std::move(fallback_path));
serial_entry = &entry;
break;
@ -2092,9 +2111,8 @@ std::string GameList::GetGameIconPath(std::string_view serial, std::string_view
ERROR_LOG("Failed to load memory card '{}': {}", Path::GetFileName(memcard_path), error.GetDescription());
}
ret = {};
UpdateMemcardTimestampCache(*serial_entry);
return ret;
return (ret = std::move(fallback_path));
}
bool GameList::UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& entry)
@ -2148,3 +2166,145 @@ 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::GetAchievementGameBadgePath(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; });
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())
{
ret = Achievements::GetGameBadgePath(badge_name);
if (!FileSystem::FileExists(ret.c_str()))
ret.clear();
}
}
return ret;
}
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<size_t>(std::clamp<s64>(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<u32> game_id = StringUtil::FromChars<u32>(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<u32>(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<u32>(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<u32>(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<int>(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<int>(badge_name.size()), badge_name.data()) < 0)
{
ERROR_LOG("Failed to append to cache: errno {}", errno);
}
}
}

@ -168,7 +168,7 @@ std::optional<DiscRegion> GetCustomRegionForPath(const std::string_view path);
/// The purpose of this cache is to stop us trying to constantly extract memory card icons, when we know a game
/// doesn't have any saves yet. It caches the serial:memcard_timestamp pair, and only tries extraction when the
/// timestamp of the memory card has changed.
std::string GetGameIconPath(std::string_view serial, std::string_view path);
std::string GetGameIconPath(std::string_view serial, std::string_view path, u32 achievements_game_id);
void ReloadMemcardTimestampCache();
/// Updates game list with new achievement unlocks.
@ -176,6 +176,11 @@ void UpdateAchievementData(const std::span<u8, 16> hash, u32 game_id, u32 num_ac
u32 num_unlocked_hardcore);
void UpdateAllAchievementData();
/// Accesses achievement game badges. Assumes the lock is held.
bool PreferAchievementGameBadgesForIcons();
std::string GetAchievementGameBadgePath(u32 game_id);
void UpdateAchievementBadgeName(u32 game_id, std::string_view badge_name);
} // namespace GameList
namespace Host {

@ -529,7 +529,7 @@ const QPixmap* GameListModel::lookupIconPixmapForEntry(const GameList::Entry* ge
else
{
// Assumes game list lock is held.
const std::string path = GameList::GetGameIconPath(ge->serial, ge->path);
const std::string path = GameList::GetGameIconPath(ge->serial, ge->path, ge->achievements_game_id);
QPixmap pm;
if (!path.empty() && pm.load(QString::fromStdString(path)))
{
@ -593,7 +593,7 @@ QIcon GameListModel::getIconForGame(const QString& path)
}
}
const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path);
const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path, entry->achievements_game_id);
if (!icon_path.empty())
ret = QIcon(QString::fromStdString(icon_path));
@ -1423,7 +1423,7 @@ public:
{
DebugAssert(source_row >= 0);
const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path);
const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path, entry->achievements_game_id);
if (icon_path.empty())
{
clearEntry();
@ -1513,8 +1513,8 @@ private:
GameListWidget::GameListWidget(QWidget* parent, QAction* action_view_list, QAction* action_view_grid,
QAction* action_merge_disc_sets, QAction* action_show_list_icons,
QAction* action_animate_list_icons, QAction* action_show_grid_titles,
QAction* action_show_localized_titles)
QAction* action_animate_list_icons, QAction* action_prefer_achievement_game_icons,
QAction* action_show_grid_titles, QAction* action_show_localized_titles)
: QWidget(parent)
{
m_model = new GameListModel(this);
@ -1594,6 +1594,7 @@ GameListWidget::GameListWidget(QWidget* parent, QAction* action_view_list, QActi
action_show_localized_titles->setChecked(m_model->getShowLocalizedTitles());
action_show_list_icons->setChecked(m_model->getShowGameIcons());
action_animate_list_icons->setChecked(m_list_view->isAnimatingGameIcons());
action_prefer_achievement_game_icons->setChecked(GameList::PreferAchievementGameBadgesForIcons());
action_show_grid_titles->setChecked(m_model->getShowCoverTitles());
onIconSizeChanged(m_model->getIconSize());
@ -1933,6 +1934,17 @@ void GameListWidget::setAnimateGameIcons(bool enabled)
m_list_view->updateAnimatedGameIconDelegate();
}
void GameListWidget::setPreferAchievementGameIcons(bool enabled)
{
Host::SetBaseBoolSettingValue("UI", "GameListPreferAchievementGameBadgesForIcons", enabled);
Host::CommitBaseSettingChanges();
if (!enabled)
GameList::ReloadMemcardTimestampCache();
m_model->refreshIcons();
}
void GameListWidget::setShowCoverTitles(bool enabled)
{
if (m_model->getShowCoverTitles() == enabled)

@ -249,8 +249,8 @@ class GameListWidget final : public QWidget
public:
explicit GameListWidget(QWidget* parent, QAction* action_view_list, QAction* action_view_grid,
QAction* action_merge_disc_sets, QAction* action_show_list_icons,
QAction* action_animate_list_icons, QAction* action_show_grid_titles,
QAction* action_show_localized_titles);
QAction* action_animate_list_icons, QAction* action_prefer_achievement_game_icons,
QAction* action_show_grid_titles, QAction* action_show_localized_titles);
~GameListWidget();
ALWAYS_INLINE GameListModel* getModel() const { return m_model; }
@ -276,6 +276,7 @@ public:
void setShowLocalizedTitles(bool enabled);
void setShowGameIcons(bool enabled);
void setAnimateGameIcons(bool enabled);
void setPreferAchievementGameIcons(bool enabled);
void setShowCoverTitles(bool enabled);
void refreshGridCovers();
void focusSearchWidget();

@ -1837,8 +1837,8 @@ void MainWindow::setupAdditionalUi()
m_game_list_widget =
new GameListWidget(m_ui.mainContainer, m_ui.actionViewGameList, m_ui.actionViewGameGrid, m_ui.actionMergeDiscSets,
m_ui.actionShowGameIcons, m_ui.actionAnimateGameIcons, m_ui.actionGridViewShowTitles,
m_ui.actionShowLocalizedTitles);
m_ui.actionShowGameIcons, m_ui.actionAnimateGameIcons, m_ui.actionPreferAchievementGameIcons,
m_ui.actionGridViewShowTitles, m_ui.actionShowLocalizedTitles);
m_ui.mainContainer->addWidget(m_game_list_widget);
m_status_progress_widget = new QProgressBar(m_ui.statusBar);
@ -2521,6 +2521,9 @@ void MainWindow::connectSignals()
&GameListWidget::setShowLocalizedTitles);
connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons);
connect(m_ui.actionAnimateGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setAnimateGameIcons);
connect(m_ui.actionPreferAchievementGameIcons, &QAction::triggered, m_game_list_widget,
&GameListWidget::setPreferAchievementGameIcons);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
connect(m_ui.actionViewZoomIn, &QAction::triggered, this, &MainWindow::onViewZoomInActionTriggered);
connect(m_ui.actionViewZoomOut, &QAction::triggered, this, &MainWindow::onViewZoomOutActionTriggered);

@ -217,19 +217,21 @@
<addaction name="menuWindowSize"/>
<addaction name="separator"/>
<addaction name="menuSortBy"/>
<addaction name="actionViewZoomIn"/>
<addaction name="actionViewZoomOut"/>
<addaction name="separator"/>
<addaction name="actionMergeDiscSets"/>
<addaction name="actionShowLocalizedTitles"/>
<addaction name="actionShowGameIcons"/>
<addaction name="actionAnimateGameIcons"/>
<addaction name="actionPreferAchievementGameIcons"/>
<addaction name="separator"/>
<addaction name="actionGridViewShowTitles"/>
<addaction name="actionViewZoomIn"/>
<addaction name="actionViewZoomOut"/>
<addaction name="actionGridViewRefreshCovers"/>
<addaction name="actionViewRefreshAchievementProgress"/>
<addaction name="separator"/>
<addaction name="actionChangeGameListBackground"/>
<addaction name="actionClearGameListBackground"/>
<addaction name="actionViewRefreshAchievementProgress"/>
</widget>
<widget class="QMenu" name="menu_Tools">
<property name="title">
@ -1027,7 +1029,7 @@
<iconset theme="price-tag-3-line"/>
</property>
<property name="text">
<string>Show Titl&amp;es (Grid View)</string>
<string>Show G&amp;rid Captions</string>
</property>
<property name="iconText">
<string>Show Titles (Grid View)</string>
@ -1060,7 +1062,7 @@
</action>
<action name="actionGridViewRefreshCovers">
<property name="text">
<string>Refresh &amp;Covers (Grid View)</string>
<string>Refresh Grid &amp;Covers</string>
</property>
<property name="toolTip">
<string>Invalidates the cache of game covers, allowing new images to be discovered.</string>
@ -1134,7 +1136,7 @@
<iconset theme="image-fill"/>
</property>
<property name="text">
<string>Show Game Ico&amp;ns (List View)</string>
<string>Show Game Ico&amp;ns</string>
</property>
<property name="toolTip">
<string>Shows icons from the memory card of each game in list view.</string>
@ -1310,12 +1312,26 @@
<iconset theme="add-animation"/>
</property>
<property name="text">
<string>Animate Game Icons (List View)</string>
<string>Animate Game Icons</string>
</property>
<property name="toolTip">
<string>Animates icons in the list view when selected.</string>
</property>
</action>
<action name="actionPreferAchievementGameIcons">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="trophy-line"/>
</property>
<property name="text">
<string>Prefer Achievement Icons</string>
</property>
<property name="toolTip">
<string>Prioritizes the games badges used for RetroAchievements over memory card icons.</string>
</property>
</action>
</widget>
<resources>
<include location="resources/duckstation-qt.qrc"/>

Loading…
Cancel
Save