diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index dd6fa8e4c..958e43a9c 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -140,6 +140,7 @@ static bool TryLoggingInWithToken(); static void EnableHardcodeMode(bool display_message, bool display_game_summary); static void OnHardcoreModeChanged(bool enabled, bool display_message, bool display_game_summary); static bool IsRAIntegrationInitializing(); +static bool IsLoggedIn(); static bool IsLoggedInOrLoggingIn(); static void FinishInitialize(); static void FinishLogin(const rc_client_t* client); @@ -220,6 +221,8 @@ static void FetchHashLibraryCallback(int result, const char* error_message, rc_c rc_client_t* client, void* callback_userdata); static void FetchAllProgressCallback(int result, const char* error_message, rc_client_all_user_progress_t* list, rc_client_t* client, void* callback_userdata); +static void RefreshAllProgressCallback(int result, const char* error_message, rc_client_all_user_progress_t* list, + rc_client_t* client, void* callback_userdata); static void BuildHashDatabase(const rc_client_hash_library_t* hashlib, const rc_client_all_user_progress_t* allprog); static bool SortAndSaveHashDatabase(Error* error); @@ -296,6 +299,7 @@ struct State rc_client_hash_library_t* fetch_hash_library_result = nullptr; rc_client_async_handle_t* fetch_all_progress_request = nullptr; rc_client_all_user_progress_t* fetch_all_progress_result = nullptr; + rc_client_async_handle_t* refresh_all_progress_request = nullptr; #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION rc_client_async_handle_t* load_raintegration_request = nullptr; @@ -696,6 +700,8 @@ void Achievements::FinishInitialize() if (IsLoggedInOrLoggingIn() && g_settings.achievements_hardcore_mode) DisplayHardcoreDeferredMessage(); } + + Host::OnAchievementsActiveChanged(true); } bool Achievements::CreateClient(rc_client_t** client, std::unique_ptr* http) @@ -865,6 +871,7 @@ void Achievements::Shutdown() #endif DestroyClient(&s_state.client, &s_state.http_downloader); + Host::OnAchievementsActiveChanged(false); } void Achievements::ClientMessageCallback(const char* message, const rc_client_t* client) @@ -2085,9 +2092,14 @@ std::string Achievements::GetLeaderboardUserBadgePath(const rc_client_leaderboar return path; } +bool Achievements::IsLoggedIn() +{ + return (rc_client_get_user_info(s_state.client) != nullptr); +} + bool Achievements::IsLoggedInOrLoggingIn() { - return (rc_client_get_user_info(s_state.client) != nullptr || s_state.login_request); + return (IsLoggedIn() || s_state.login_request); } bool Achievements::Login(const char* username, const char* password, Error* error) @@ -4055,6 +4067,62 @@ void Achievements::FinishRefreshHashDatabase() // update game list, we might have some new games that weren't in the seed database GameList::UpdateAllAchievementData(); + + Host::OnAchievementsAllProgressRefreshed(); +} + +bool Achievements::RefreshAllProgressDatabase(Error* error) +{ + if (!IsLoggedIn()) + { + Error::SetStringView(error, TRANSLATE_SV("Achievements", "User is not logged in.")); + return false; + } + + if (s_state.fetch_hash_library_request || s_state.fetch_all_progress_request || s_state.refresh_all_progress_request) + { + Error::SetStringView(error, TRANSLATE_SV("Achievements", "Progress is already being updated.")); + return false; + } + + // refresh in progress + s_state.refresh_all_progress_request = rc_client_begin_fetch_all_user_progress(s_state.client, RC_CONSOLE_PLAYSTATION, + RefreshAllProgressCallback, nullptr); + + return true; +} + +void Achievements::RefreshAllProgressCallback(int result, const char* error_message, + rc_client_all_user_progress_t* list, rc_client_t* client, + void* callback_userdata) +{ + s_state.refresh_all_progress_request = nullptr; + + if (result != RC_OK) + { + Host::ReportErrorAsync(TRANSLATE_SV("Achievements", "Error"), + fmt::format("{}: {}\n{}", TRANSLATE_SV("Achievements", "Refresh all progress failed"), + rc_error_str(result), error_message)); + return; + } + + BuildProgressDatabase(list); + rc_client_destroy_all_user_progress(list); + + GameList::UpdateAllAchievementData(); + + Host::OnAchievementsAllProgressRefreshed(); + + if (FullscreenUI::IsInitialized()) + { + GPUThread::RunOnThread([]() { + if (!FullscreenUI::IsInitialized()) + return; + + ImGuiFullscreen::ShowToast({}, TRANSLATE_STR("Achievements", "Updated achievement progress database."), + Host::OSD_INFO_DURATION); + }); + } } void Achievements::BuildHashDatabase(const rc_client_hash_library_t* hashlib, diff --git a/src/core/achievements.h b/src/core/achievements.h index 0437c46b5..5298ef38e 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -80,6 +80,9 @@ void UpdateSettings(const Settings& old_config); /// Shuts down the RetroAchievements client. void Shutdown(); +/// Call to refresh the all-progress database. +bool RefreshAllProgressDatabase(Error* error); + /// Called when the system is start. Engages hardcore mode if enabled. void OnSystemStarting(CDImage* image, bool disable_hardcore_mode); @@ -217,9 +220,15 @@ void OnAchievementsLoginSuccess(const char* display_name, u32 points, u32 sc_poi /// Implementers can assume the lock is held when this is called. void OnAchievementsRefreshed(); +/// Called when achievements login completes or they are disabled. +void OnAchievementsActiveChanged(bool active); + /// Called whenever hardcore mode is toggled. void OnAchievementsHardcoreModeChanged(bool enabled); +/// Called whenever all progress is manually refreshed and completed. +void OnAchievementsAllProgressRefreshed(); + #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION /// Called when the RAIntegration menu changes. diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 217be1cc4..7c3c20122 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -6350,10 +6350,19 @@ void FullscreenUI::DrawAchievementsSettingsPage() false); ImGui::PopStyleColor(); - if (MenuButton(FSUI_ICONVSTR(ICON_FA_KEY, "Logout"), FSUI_VSTR("Logs out of RetroAchievements."))) + if (MenuButton(FSUI_ICONVSTR(ICON_FA_LIST_OL, "Update Progress"), + FSUI_VSTR("Updates the progress database for achievements shown in the game list."))) { - Host::RunOnCPUThread(&Achievements::Logout); + Host::RunOnCPUThread([]() { + Error error; + if (!Achievements::RefreshAllProgressDatabase(&error)) + ImGuiFullscreen::ShowToast(FSUI_STR("Failed to update progress database"), error.TakeDescription(), + Host::OSD_ERROR_DURATION); + }); } + + if (MenuButton(FSUI_ICONVSTR(ICON_FA_KEY, "Logout"), FSUI_VSTR("Logs out of RetroAchievements."))) + Host::RunOnCPUThread(&Achievements::Logout); } else { @@ -9365,6 +9374,7 @@ TRANSLATE_NOOP("FullscreenUI", "Failed to delete {}."); TRANSLATE_NOOP("FullscreenUI", "Failed to load '{}'."); TRANSLATE_NOOP("FullscreenUI", "Failed to load shader {}. It may be invalid.\nError was:"); TRANSLATE_NOOP("FullscreenUI", "Failed to save controller preset '{}'."); +TRANSLATE_NOOP("FullscreenUI", "Failed to update progress database"); TRANSLATE_NOOP("FullscreenUI", "Fast Boot"); TRANSLATE_NOOP("FullscreenUI", "Fast Forward Boot"); TRANSLATE_NOOP("FullscreenUI", "Fast Forward Memory Card Access"); @@ -9769,6 +9779,8 @@ TRANSLATE_NOOP("FullscreenUI", "Ungrouped"); TRANSLATE_NOOP("FullscreenUI", "Unknown"); TRANSLATE_NOOP("FullscreenUI", "Unknown File Size"); TRANSLATE_NOOP("FullscreenUI", "Unlimited"); +TRANSLATE_NOOP("FullscreenUI", "Update Progress"); +TRANSLATE_NOOP("FullscreenUI", "Updates the progress database for achievements shown in the game list."); TRANSLATE_NOOP("FullscreenUI", "Upscales the game's rendering by the specified multiplier."); TRANSLATE_NOOP("FullscreenUI", "Use Blit Swap Chain"); TRANSLATE_NOOP("FullscreenUI", "Use Debug GPU Device"); diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 539552e59..0e60711ff 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -745,6 +745,8 @@ void GameList::PopulateEntryAchievements(Entry* entry, const Achievements::Progr entry->achievements_game_id = hentry->game_id; entry->num_achievements = Truncate16(hentry->num_achievements); + entry->unlocked_achievements = 0; + entry->unlocked_achievements_hc = 0; if (entry->num_achievements > 0) { const Achievements::ProgressDatabase::Entry* apd_entry = achievements_progress.LookupGame(hentry->game_id); diff --git a/src/duckstation-mini/mini_host.cpp b/src/duckstation-mini/mini_host.cpp index 7fed49d08..f6af1d94a 100644 --- a/src/duckstation-mini/mini_host.cpp +++ b/src/duckstation-mini/mini_host.cpp @@ -1113,11 +1113,21 @@ void Host::OnAchievementsRefreshed() // noop } +void Host::OnAchievementsActiveChanged(bool active) +{ + // noop +} + void Host::OnAchievementsHardcoreModeChanged(bool enabled) { // noop } +void Host::OnAchievementsAllProgressRefreshed() +{ + // noop +} + #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION void Host::OnRAIntegrationMenuChanged() diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 17fd8d250..043c6d0ce 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -2100,6 +2100,8 @@ void MainWindow::connectSignals() connect(m_ui.actionGridViewZoomOut, &QAction::triggered, this, &MainWindow::onViewGameGridZoomOutActionTriggered); connect(m_ui.actionGridViewRefreshCovers, &QAction::triggered, m_game_list_widget, &GameListWidget::refreshGridCovers); + connect(m_ui.actionViewRefreshAchievementProgress, &QAction::triggered, g_emu_thread, + &EmuThread::refreshAchievementsAllProgress); connect(m_ui.actionChangeGameListBackground, &QAction::triggered, this, &MainWindow::onViewChangeGameListBackgroundTriggered); connect(m_ui.actionClearGameListBackground, &QAction::triggered, this, @@ -2128,8 +2130,11 @@ void MainWindow::connectSignals() connect(g_emu_thread, &EmuThread::fullscreenUIStartedOrStopped, this, &MainWindow::onFullscreenUIStartedOrStopped); connect(g_emu_thread, &EmuThread::achievementsLoginRequested, this, &MainWindow::onAchievementsLoginRequested); connect(g_emu_thread, &EmuThread::achievementsLoginSuccess, this, &MainWindow::onAchievementsLoginSuccess); + connect(g_emu_thread, &EmuThread::achievementsActiveChanged, this, &MainWindow::onAchievementsActiveChanged); connect(g_emu_thread, &EmuThread::achievementsHardcoreModeChanged, this, &MainWindow::onAchievementsHardcoreModeChanged); + connect(g_emu_thread, &EmuThread::achievementsAllProgressRefreshed, this, + &MainWindow::onAchievementsAllProgressRefreshed); connect(g_emu_thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered); connect(g_emu_thread, &EmuThread::onCreateAuxiliaryRenderWindow, this, &MainWindow::onCreateAuxiliaryRenderWindow, Qt::BlockingQueuedConnection); @@ -2758,6 +2763,11 @@ void MainWindow::onAchievementsLoginSuccess(const QString& username, quint32 poi } } +void MainWindow::onAchievementsActiveChanged(bool active) +{ + m_ui.actionViewRefreshAchievementProgress->setEnabled(active); +} + void MainWindow::onAchievementsHardcoreModeChanged(bool enabled) { if (enabled) @@ -2770,6 +2780,11 @@ void MainWindow::onAchievementsHardcoreModeChanged(bool enabled) updateEmulationActions(s_system_starting, s_system_valid, enabled); } +void MainWindow::onAchievementsAllProgressRefreshed() +{ + m_ui.statusBar->showMessage(tr("RA: Updated achievement progress database.")); +} + bool MainWindow::onCreateAuxiliaryRenderWindow(RenderAPI render_api, qint32 x, qint32 y, quint32 width, quint32 height, const QString& title, const QString& icon_name, Host::AuxiliaryRenderWindowUserData userdata, diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index c94db16c3..28598d38e 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -151,7 +151,9 @@ private Q_SLOTS: void onMediaCaptureStopped(); void onAchievementsLoginRequested(Achievements::LoginRequestReason reason); void onAchievementsLoginSuccess(const QString& username, quint32 points, quint32 sc_points, quint32 unread_messages); + void onAchievementsActiveChanged(bool active); void onAchievementsHardcoreModeChanged(bool enabled); + void onAchievementsAllProgressRefreshed(); bool onCreateAuxiliaryRenderWindow(RenderAPI render_api, qint32 x, qint32 y, quint32 width, quint32 height, const QString& title, const QString& icon_name, Host::AuxiliaryRenderWindowUserData userdata, diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 3dfa26b6f..98b12d2fe 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -213,6 +213,7 @@ + @@ -987,6 +988,14 @@ Clear List Background + + + false + + + Refresh Achievement &Progress + + diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 45c0a3052..b4b4c6034 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1631,6 +1631,22 @@ void EmuThread::saveScreenshot() System::SaveScreenshot(); } +void EmuThread::refreshAchievementsAllProgress() +{ + if (!isCurrentThread()) + { + QMetaObject::invokeMethod(this, &EmuThread::refreshAchievementsAllProgress, Qt::QueuedConnection); + return; + } + + Error error; + if (!Achievements::RefreshAllProgressDatabase(&error)) + { + emit errorReported(tr("Error"), QString::fromStdString(error.GetDescription())); + return; + } +} + void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) { emit g_emu_thread->achievementsLoginRequested(reason); @@ -1669,11 +1685,21 @@ void Host::OnAchievementsRefreshed() emit g_emu_thread->achievementsRefreshed(game_id, game_info); } +void Host::OnAchievementsActiveChanged(bool active) +{ + emit g_emu_thread->achievementsActiveChanged(active); +} + void Host::OnAchievementsHardcoreModeChanged(bool enabled) { emit g_emu_thread->achievementsHardcoreModeChanged(enabled); } +void Host::OnAchievementsAllProgressRefreshed() +{ + emit g_emu_thread->achievementsAllProgressRefreshed(); +} + void Host::OnCoverDownloaderOpenRequested() { emit g_emu_thread->onCoverDownloaderOpenRequested(); diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index 713a9905e..9726788d5 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -155,7 +155,9 @@ Q_SIGNALS: void achievementsLoginRequested(Achievements::LoginRequestReason reason); void achievementsLoginSuccess(const QString& username, quint32 points, quint32 sc_points, quint32 unread_messages); void achievementsRefreshed(quint32 id, const QString& game_info_string); + void achievementsActiveChanged(bool active); void achievementsHardcoreModeChanged(bool enabled); + void achievementsAllProgressRefreshed(); void cheatEnabled(quint32 index, bool enabled); void mediaCaptureStarted(); void mediaCaptureStopped(); @@ -183,6 +185,7 @@ public Q_SLOTS: void closeInputSources(); void startFullscreenUI(); void stopFullscreenUI(); + void refreshAchievementsAllProgress(); void bootSystem(std::shared_ptr params); void resumeSystemFromMostRecentState(); void shutdownSystem(bool save_state, bool check_memcard_busy); diff --git a/src/duckstation-regtest/regtest_host.cpp b/src/duckstation-regtest/regtest_host.cpp index 2dd5ec0b3..603a9e360 100644 --- a/src/duckstation-regtest/regtest_host.cpp +++ b/src/duckstation-regtest/regtest_host.cpp @@ -576,11 +576,21 @@ void Host::OnAchievementsRefreshed() // noop } +void Host::OnAchievementsActiveChanged(bool active) +{ + // noop +} + void Host::OnAchievementsHardcoreModeChanged(bool enabled) { // noop } +void Host::OnAchievementsAllProgressRefreshed() +{ + // noop +} + #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION void Host::OnRAIntegrationMenuChanged()