GameList: Fix memcard icon extraction for non-database games

Also fix "Edit Memory Cards" menu option.
This commit is contained in:
Stenzek
2025-12-01 00:32:39 +10:00
parent 1bf7431b89
commit 7970287d69
7 changed files with 42 additions and 38 deletions

View File

@@ -1065,12 +1065,12 @@ GPUTexture* FullscreenUI::GetGameListCover(const GameList::Entry* entry, bool fa
}
// because memcard icons are crap res
if (fallback_to_icon && cover_it->second.empty())
if (fallback_to_icon && cover_it->second.empty() && !entry->serial.empty() && entry->IsDiscOrDiscSet())
{
cover_it = s_game_list_locals.icon_image_map.find(entry->serial);
if (cover_it == s_game_list_locals.icon_image_map.end())
{
std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path, entry->achievements_game_id);
std::string icon_path = GameList::GetGameIconPath(entry);
cover_it = s_game_list_locals.icon_image_map.emplace(entry->serial, std::move(icon_path)).first;
}
}

View File

@@ -2026,28 +2026,29 @@ void GameList::ReloadMemcardTimestampCache()
entry.serial[sizeof(entry.serial) - 1] = 0;
}
std::string GameList::GetGameIconPath(std::string_view serial, std::string_view path, u32 achievements_game_id)
std::string GameList::GetGameIconPath(const GameList::Entry* entry)
{
std::string ret;
std::string fallback_path;
if (achievements_game_id != 0)
if (entry->achievements_game_id != 0)
{
fallback_path = GetAchievementGameBadgePath(achievements_game_id);
fallback_path = GetAchievementGameBadgePath(entry->achievements_game_id);
if (!fallback_path.empty() && PreferAchievementGameBadgesForIcons())
return (ret = std::move(fallback_path));
}
if (serial.empty())
if (entry->serial.empty())
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));
ret = Path::Combine(EmuFolders::GameIcons, TinyString::from_format("{}.png", entry->serial));
if (FileSystem::FileExists(ret.c_str()))
return ret;
MemoryCardType type;
std::string memcard_path = System::GetGameMemoryCardPath(serial, path, 0, &type);
std::string memcard_path =
System::GetGameMemoryCardPath(entry->title, entry->has_custom_title, entry->serial, entry->path, 0, &type);
FILESYSTEM_STAT_DATA memcard_sd;
if (memcard_path.empty() || type == MemoryCardType::Shared ||
!FileSystem::StatFile(memcard_path.c_str(), &memcard_sd))
@@ -2057,33 +2058,33 @@ std::string GameList::GetGameIconPath(std::string_view serial, std::string_view
const s64 timestamp = memcard_sd.ModificationTime;
TinyString index_serial;
index_serial.assign(
serial.substr(0, std::min<size_t>(serial.length(), MemcardTimestampCacheEntry::MAX_SERIAL_LENGTH - 1)));
index_serial.assign(entry->serial.substr(
0, std::min<size_t>(entry->serial.length(), MemcardTimestampCacheEntry::MAX_SERIAL_LENGTH - 1)));
MemcardTimestampCacheEntry* serial_entry = nullptr;
for (MemcardTimestampCacheEntry& entry : s_state.memcard_timestamp_cache_entries)
MemcardTimestampCacheEntry* cache_entry = nullptr;
for (MemcardTimestampCacheEntry& it : s_state.memcard_timestamp_cache_entries)
{
if (StringUtil::EqualNoCase(index_serial, entry.serial))
if (StringUtil::EqualNoCase(index_serial, it.serial))
{
// 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)
if (it.memcard_timestamp == timestamp && !it.icon_was_extracted)
return (ret = std::move(fallback_path));
serial_entry = &entry;
cache_entry = &it;
break;
}
}
if (!serial_entry)
if (!cache_entry)
{
serial_entry = &s_state.memcard_timestamp_cache_entries.emplace_back();
std::memset(serial_entry, 0, sizeof(MemcardTimestampCacheEntry));
cache_entry = &s_state.memcard_timestamp_cache_entries.emplace_back();
std::memset(cache_entry, 0, sizeof(MemcardTimestampCacheEntry));
}
serial_entry->memcard_timestamp = timestamp;
serial_entry->icon_was_extracted = false;
StringUtil::Strlcpy(serial_entry->serial, index_serial.view(), sizeof(serial_entry->serial));
cache_entry->memcard_timestamp = timestamp;
cache_entry->icon_was_extracted = false;
StringUtil::Strlcpy(cache_entry->serial, index_serial.view(), sizeof(cache_entry->serial));
// Try extracting an icon.
Error error;
@@ -2105,8 +2106,8 @@ std::string GameList::GetGameIconPath(std::string_view serial, std::string_view
for (size_t i = 0; i < fi.icon_frames.size(); i++)
image.SetPixels(static_cast<u32>(i), fi.icon_frames[i].pixels, MemoryCardImage::ICON_WIDTH * sizeof(u32));
serial_entry->icon_was_extracted = image.SaveToFile(ret.c_str(), AnimatedImage::DEFAULT_SAVE_QUALITY, &error);
if (serial_entry->icon_was_extracted)
cache_entry->icon_was_extracted = image.SaveToFile(ret.c_str(), AnimatedImage::DEFAULT_SAVE_QUALITY, &error);
if (cache_entry->icon_was_extracted)
return ret;
else
ERROR_LOG("Failed to save memory card icon to {}: {}", Path::GetFileName(ret), error.GetDescription());
@@ -2118,7 +2119,7 @@ std::string GameList::GetGameIconPath(std::string_view serial, std::string_view
ERROR_LOG("Failed to load memory card '{}': {}", Path::GetFileName(memcard_path), error.GetDescription());
}
UpdateMemcardTimestampCache(*serial_entry);
UpdateMemcardTimestampCache(*cache_entry);
return (ret = std::move(fallback_path));
}

View File

@@ -76,6 +76,7 @@ struct Entry
ALWAYS_INLINE bool IsValid() const { return (type < EntryType::MaxCount); }
ALWAYS_INLINE bool IsDisc() const { return (type == EntryType::Disc); }
ALWAYS_INLINE bool IsDiscSet() const { return (type == EntryType::DiscSet); }
ALWAYS_INLINE bool IsDiscOrDiscSet() const { return (type == EntryType::Disc || type == EntryType::DiscSet); }
ALWAYS_INLINE bool HasCustomLanguage() const { return (custom_language != GameDatabase::Language::MaxCount); }
ALWAYS_INLINE EntryType GetSortType() const { return (type == EntryType::DiscSet) ? EntryType::Disc : type; }
ALWAYS_INLINE const GameDatabase::DiscSetEntry* GetDiscSetEntry() const
@@ -170,7 +171,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, u32 achievements_game_id);
std::string GetGameIconPath(const GameList::Entry* entry);
void ReloadMemcardTimestampCache();
/// Updates game list with new achievement unlocks.

View File

@@ -5761,8 +5761,8 @@ void System::DeleteSaveStates(std::string_view serial, bool resume)
}
}
std::string System::GetGameMemoryCardPath(std::string_view serial, std::string_view path, u32 slot,
MemoryCardType* out_type)
std::string System::GetGameMemoryCardPath(std::string_view title, bool is_custom_title, std::string_view serial,
std::string_view path, u32 slot, MemoryCardType* out_type /* = nullptr */)
{
const char* section = "MemoryCards";
const TinyString type_key = TinyString::from_format("Card{}Type", slot + 1);
@@ -5821,10 +5821,8 @@ std::string System::GetGameMemoryCardPath(std::string_view serial, std::string_v
case MemoryCardType::PerGameTitle:
{
const std::string custom_title = GameList::GetCustomTitleForPath(path);
const GameDatabase::Entry* entry = GameDatabase::GetEntryForSerial(serial);
const std::string_view game_title =
(!custom_title.empty() || !entry) ? std::string_view(custom_title) : entry->GetSaveTitle();
const std::string_view game_title = (is_custom_title || !entry) ? title : entry->GetSaveTitle();
// Multi-disc game - use disc set name.
if (entry && entry->disc_set)

View File

@@ -404,8 +404,8 @@ std::optional<ExtendedSaveStateInfo> GetExtendedSaveStateInfo(const char* path);
void DeleteSaveStates(std::string_view serial, bool resume);
/// Returns the path to the memory card for the specified game, considering game settings.
std::string GetGameMemoryCardPath(std::string_view serial, std::string_view path, u32 slot,
MemoryCardType* out_type = nullptr);
std::string GetGameMemoryCardPath(std::string_view title, bool is_custom_title, std::string_view serial,
std::string_view path, u32 slot, MemoryCardType* out_type = nullptr);
/// Returns intended output volume considering fast forwarding.
u8 GetAudioOutputVolume();

View File

@@ -610,7 +610,7 @@ const QPixmap& GameListModel::getCoverForEntry(const GameList::Entry* ge) const
const QPixmap* GameListModel::lookupIconPixmapForEntry(const GameList::Entry* ge) const
{
// We only do this for discs/disc sets for now.
if (m_show_game_icons && (!ge->serial.empty() && (ge->IsDisc() || ge->IsDiscSet())))
if (m_show_game_icons && !ge->serial.empty() && ge->IsDiscOrDiscSet())
{
QPixmap* item = m_icon_pixmap_cache.Lookup(ge->serial);
if (item)
@@ -621,7 +621,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, ge->achievements_game_id);
const std::string path = GameList::GetGameIconPath(ge);
QPixmap pm;
if (!path.empty() && pm.load(QString::fromStdString(path)))
{
@@ -682,7 +682,7 @@ QIcon GameListModel::getIconForGame(const QString& path)
const auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(path.toStdString());
if (!entry || entry->serial.empty() || (!entry->IsDisc() && !entry->IsDiscSet()))
if (!entry || entry->serial.empty() || !entry->IsDiscOrDiscSet())
return ret;
// Only use the cache if we're not using larger icons. Otherwise they'll get double scaled.
@@ -697,7 +697,7 @@ QIcon GameListModel::getIconForGame(const QString& path)
}
}
const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path, entry->achievements_game_id);
const std::string icon_path = GameList::GetGameIconPath(entry);
if (!icon_path.empty())
ret = QIcon(QString::fromStdString(icon_path));
@@ -1535,7 +1535,7 @@ public:
{
DebugAssert(source_row >= 0);
const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path, entry->achievements_game_id);
const std::string icon_path = GameList::GetGameIconPath(entry);
if (icon_path.empty())
{
clearEntry();

View File

@@ -531,6 +531,7 @@ void MainWindow::updateGameListRelatedActions()
m_ui.actionViewZoomIn->setDisabled(disable);
m_ui.actionViewZoomOut->setDisabled(disable);
m_ui.actionGridViewRefreshCovers->setDisabled(disable || !game_grid);
m_ui.actionPreferAchievementGameIcons->setDisabled(disable || !game_list);
m_ui.actionChangeGameListBackground->setDisabled(disable);
m_ui.actionClearGameListBackground->setDisabled(disable || !has_background);
}
@@ -909,7 +910,10 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg
QString paths[2];
for (u32 i = 0; i < 2; i++)
paths[i] = QString::fromStdString(System::GetGameMemoryCardPath(entry->serial, entry->path, i));
{
paths[i] = QString::fromStdString(
System::GetGameMemoryCardPath(entry->title, entry->has_custom_title, entry->serial, entry->path, i));
}
g_main_window->openMemoryCardEditor(paths[0], paths[1]);
});