mirror of
https://github.com/stenzek/duckstation.git
synced 2026-02-13 09:54:32 +00:00
GameList: Add option to download all game icons (#3655)
* GameList: Add option to download all game icons * Fix translation support in status text calls * Parallelize downloading game icons
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/binary_reader_writer.h"
|
||||
#include "common/error.h"
|
||||
#include "common/progress_callback.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/heap_array.h"
|
||||
#include "common/log.h"
|
||||
@@ -46,6 +47,7 @@
|
||||
#include "fmt/format.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "rc_api_info.h"
|
||||
#include "rc_api_runtime.h"
|
||||
#include "rc_client.h"
|
||||
#include "rc_consoles.h"
|
||||
@@ -2059,6 +2061,195 @@ std::string Achievements::GetGameBadgePath(std::string_view badge_name)
|
||||
return GetLocalImagePath(badge_name, RC_IMAGE_TYPE_GAME);
|
||||
}
|
||||
|
||||
bool Achievements::DownloadGameIcons(ProgressCallback* progress, Error* error)
|
||||
{
|
||||
progress->SetStatusText(TRANSLATE_SV("Achievements", "Collecting games..."));
|
||||
|
||||
// Collect all unique game IDs that don't have icons yet
|
||||
std::vector<u32> game_ids;
|
||||
{
|
||||
const auto lock = GameList::GetLock();
|
||||
for (const GameList::Entry& entry : GameList::GetEntries())
|
||||
{
|
||||
if (entry.achievements_game_id != 0)
|
||||
{
|
||||
// Check if we already have this badge
|
||||
const std::string existing_badge = GameList::GetAchievementGameBadgePath(entry.achievements_game_id);
|
||||
if (existing_badge.empty() &&
|
||||
std::find(game_ids.begin(), game_ids.end(), entry.achievements_game_id) == game_ids.end())
|
||||
{
|
||||
game_ids.push_back(entry.achievements_game_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (game_ids.empty())
|
||||
{
|
||||
progress->SetStatusText(TRANSLATE_SV("Achievements", "No games need icon downloads."));
|
||||
return true;
|
||||
}
|
||||
|
||||
INFO_LOG("Downloading icons for {} games from RetroAchievements", game_ids.size());
|
||||
progress->FormatStatusText(TRANSLATE_FS("Achievements", "Fetching icon info for {} games..."), game_ids.size());
|
||||
|
||||
// Create HTTP downloader
|
||||
std::unique_ptr<HTTPDownloader> http = HTTPDownloader::Create(Host::GetHTTPUserAgent());
|
||||
if (!http)
|
||||
{
|
||||
Error::SetStringView(error, "Failed to create HTTP downloader.");
|
||||
return false;
|
||||
}
|
||||
http->SetTimeout(30.0f);
|
||||
|
||||
// Fetch game titles (includes badge names) from RetroAchievements
|
||||
rc_api_fetch_game_titles_request_t titles_request;
|
||||
titles_request.game_ids = game_ids.data();
|
||||
titles_request.num_game_ids = static_cast<u32>(game_ids.size());
|
||||
|
||||
rc_api_request_t request;
|
||||
if (rc_api_init_fetch_game_titles_request(&request, &titles_request) != RC_OK)
|
||||
{
|
||||
Error::SetStringView(error, "Failed to create API request.");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<u8> response_data;
|
||||
bool request_success = false;
|
||||
|
||||
HTTPDownloader::Request::Callback callback = [&response_data, &request_success](
|
||||
s32 status_code, const Error&, const std::string&,
|
||||
HTTPDownloader::Request::Data data) {
|
||||
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
|
||||
{
|
||||
response_data = std::move(data);
|
||||
request_success = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (request.post_data)
|
||||
http->CreatePostRequest(request.url, request.post_data, std::move(callback));
|
||||
else
|
||||
http->CreateRequest(request.url, std::move(callback));
|
||||
|
||||
rc_api_destroy_request(&request);
|
||||
http->WaitForAllRequests();
|
||||
|
||||
if (!request_success || response_data.empty())
|
||||
{
|
||||
Error::SetStringView(error, "Failed to fetch game info from RetroAchievements.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse response
|
||||
rc_api_fetch_game_titles_response_t titles_response;
|
||||
rc_api_server_response_t server_response;
|
||||
server_response.body = reinterpret_cast<const char*>(response_data.data());
|
||||
server_response.body_length = response_data.size();
|
||||
server_response.http_status_code = 200;
|
||||
|
||||
const int parse_result = rc_api_process_fetch_game_titles_server_response(&titles_response, &server_response);
|
||||
if (parse_result != RC_OK)
|
||||
{
|
||||
const std::string_view response_preview(server_response.body,
|
||||
std::min<size_t>(server_response.body_length, 500));
|
||||
ERROR_LOG("Failed to parse game titles response ({}): {}", parse_result, response_preview);
|
||||
if (titles_response.response.error_message)
|
||||
Error::SetStringFmt(error, "RetroAchievements error: {}", titles_response.response.error_message);
|
||||
else
|
||||
Error::SetStringFmt(error, "Failed to parse API response (code {})", parse_result);
|
||||
rc_api_destroy_fetch_game_titles_response(&titles_response);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGuard response_guard([&titles_response]() { rc_api_destroy_fetch_game_titles_response(&titles_response); });
|
||||
|
||||
if (titles_response.num_entries == 0)
|
||||
{
|
||||
progress->SetStatusText(TRANSLATE_SV("Achievements", "No icon information found."));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Collect icons to download
|
||||
struct PendingDownload
|
||||
{
|
||||
u32 game_id;
|
||||
std::string image_name;
|
||||
std::string local_path;
|
||||
std::string url;
|
||||
std::vector<u8> data;
|
||||
bool success = false;
|
||||
};
|
||||
|
||||
std::vector<PendingDownload> downloads;
|
||||
downloads.reserve(titles_response.num_entries);
|
||||
|
||||
for (u32 i = 0; i < titles_response.num_entries; i++)
|
||||
{
|
||||
const rc_api_game_title_entry_t& entry = titles_response.entries[i];
|
||||
|
||||
if (!entry.image_name || entry.image_name[0] == '\0')
|
||||
continue;
|
||||
|
||||
std::string local_path = GetLocalImagePath(entry.image_name, RC_IMAGE_TYPE_GAME);
|
||||
if (FileSystem::FileExists(local_path.c_str()))
|
||||
{
|
||||
// Already have this icon, just update the cache
|
||||
GameList::UpdateAchievementBadgeName(entry.id, entry.image_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string url = GetImageURL(entry.image_name, RC_IMAGE_TYPE_GAME);
|
||||
if (url.empty())
|
||||
continue;
|
||||
|
||||
downloads.push_back({entry.id, entry.image_name, std::move(local_path), std::move(url), {}, false});
|
||||
}
|
||||
|
||||
if (downloads.empty())
|
||||
{
|
||||
progress->SetStatusText(TRANSLATE_SV("Achievements", "All icons already downloaded."));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create all download requests in parallel
|
||||
progress->SetProgressRange(static_cast<u32>(downloads.size()));
|
||||
progress->FormatStatusText(TRANSLATE_FS("Achievements", "Downloading {} game icons..."), downloads.size());
|
||||
|
||||
std::atomic<u32> completed_count{0};
|
||||
for (PendingDownload& dl : downloads)
|
||||
{
|
||||
http->CreateRequest(dl.url, [&dl, &completed_count, progress](s32 status_code, const Error&, const std::string&,
|
||||
HTTPDownloader::Request::Data data) {
|
||||
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
|
||||
{
|
||||
dl.data = std::move(data);
|
||||
dl.success = true;
|
||||
}
|
||||
progress->SetProgressValue(completed_count.fetch_add(1, std::memory_order_relaxed) + 1);
|
||||
});
|
||||
}
|
||||
|
||||
http->WaitForAllRequests();
|
||||
|
||||
// Process completed downloads
|
||||
u32 downloaded = 0;
|
||||
for (const PendingDownload& dl : downloads)
|
||||
{
|
||||
if (dl.success && !dl.data.empty())
|
||||
{
|
||||
if (FileSystem::WriteBinaryFile(dl.local_path.c_str(), dl.data))
|
||||
{
|
||||
GameList::UpdateAchievementBadgeName(dl.game_id, dl.image_name);
|
||||
downloaded++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
INFO_LOG("Downloaded {} game icons", downloaded);
|
||||
return true;
|
||||
}
|
||||
|
||||
u32 Achievements::GetPauseThrottleFrames()
|
||||
{
|
||||
if (!IsActive() || !IsHardcoreModeActive())
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <vector>
|
||||
|
||||
class Error;
|
||||
class ProgressCallback;
|
||||
class StateWrapper;
|
||||
class CDImage;
|
||||
|
||||
@@ -184,6 +185,10 @@ SmallString GetLoggedInUserPointsSummary();
|
||||
/// Returns the path to the local cache for the specified badge name.
|
||||
std::string GetGameBadgePath(std::string_view 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.
|
||||
bool DownloadGameIcons(ProgressCallback* progress, Error* error);
|
||||
|
||||
/// Returns 0 if pausing is allowed, otherwise the number of frames until pausing is allowed.
|
||||
u32 GetPauseThrottleFrames();
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
#include "gamelistrefreshthread.h"
|
||||
#include "mainwindow.h"
|
||||
#include "qthost.h"
|
||||
#include "qtprogresscallback.h"
|
||||
#include "qtutils.h"
|
||||
#include "settingswindow.h"
|
||||
|
||||
#include "core/achievements.h"
|
||||
#include "core/fullscreenui.h"
|
||||
#include "core/game_list.h"
|
||||
#include "core/host.h"
|
||||
@@ -2161,6 +2163,19 @@ void GameListWidget::setPreferAchievementGameIcons(bool enabled)
|
||||
m_model->refreshIcons();
|
||||
}
|
||||
|
||||
void GameListWidget::downloadAllGameIcons()
|
||||
{
|
||||
QtAsyncTaskWithProgressDialog::create(
|
||||
this, tr("Loading Game Icons").toStdString(), tr("Downloading game icons...").toStdString(), true, 0, 0, 0.0f,
|
||||
[](ProgressCallback* progress) -> std::function<void()> {
|
||||
Error error;
|
||||
if (!Achievements::DownloadGameIcons(progress, &error))
|
||||
WARNING_LOG("Failed to download game icons: {}", error.GetDescription());
|
||||
|
||||
return []() { g_main_window->refreshGameListModel(); };
|
||||
});
|
||||
}
|
||||
|
||||
void GameListWidget::setShowCoverTitles(bool enabled)
|
||||
{
|
||||
if (m_model->getShowCoverTitles() == enabled)
|
||||
|
||||
@@ -283,6 +283,7 @@ public:
|
||||
void setShowGameIcons(bool enabled);
|
||||
void setAnimateGameIcons(bool enabled);
|
||||
void setPreferAchievementGameIcons(bool enabled);
|
||||
void downloadAllGameIcons();
|
||||
void setShowCoverTitles(bool enabled);
|
||||
void refreshGridCovers();
|
||||
void focusSearchWidget();
|
||||
|
||||
@@ -530,6 +530,7 @@ void MainWindow::updateGameListRelatedActions()
|
||||
m_ui.actionViewZoomOut->setDisabled(disable);
|
||||
m_ui.actionGridViewRefreshCovers->setDisabled(disable || !game_grid);
|
||||
m_ui.actionPreferAchievementGameIcons->setDisabled(disable || !game_list);
|
||||
m_ui.actionDownloadAllGameIcons->setDisabled(disable);
|
||||
m_ui.actionChangeGameListBackground->setDisabled(disable);
|
||||
m_ui.actionClearGameListBackground->setDisabled(disable || !has_background);
|
||||
}
|
||||
@@ -2491,6 +2492,7 @@ void MainWindow::connectSignals()
|
||||
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.actionDownloadAllGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::downloadAllGameIcons);
|
||||
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);
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
<addaction name="actionShowGameIcons"/>
|
||||
<addaction name="actionAnimateGameIcons"/>
|
||||
<addaction name="actionPreferAchievementGameIcons"/>
|
||||
<addaction name="actionDownloadAllGameIcons"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionGridViewShowTitles"/>
|
||||
<addaction name="actionGridViewRefreshCovers"/>
|
||||
@@ -1387,6 +1388,17 @@
|
||||
<string>Prioritizes the games badges used for RetroAchievements over memory card icons.</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDownloadAllGameIcons">
|
||||
<property name="icon">
|
||||
<iconset theme="download-2-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Download All Game Icons</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Downloads icons for all games from RetroAchievements.</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="resources/duckstation-qt.qrc"/>
|
||||
|
||||
Reference in New Issue
Block a user