From f700faf6ab9da09cdb011961906dc0e66df5767d Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Fri, 2 Jun 2023 21:17:43 -0400 Subject: [PATCH 1/5] Expose Achievements Data for Display Added some small methods to AchievementManager to expose useful data for displaying in an achievement UI. Also moved a couple things from private to public for the same purpose. --- Source/Core/Core/AchievementManager.cpp | 88 ++++++++++++++++++------- Source/Core/Core/AchievementManager.h | 37 +++++++---- 2 files changed, 87 insertions(+), 38 deletions(-) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 1a05af90bc..27e82a8c55 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -183,6 +183,11 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, }); } +bool AchievementManager::IsGameLoaded() const +{ + return m_is_game_loaded; +} + void AchievementManager::LoadUnlockData(const ResponseCallback& callback) { m_queue.EmplaceItem([this, callback] { @@ -313,6 +318,64 @@ void AchievementManager::AchievementEventHandler(const rc_runtime_event_t* runti } } +std::string AchievementManager::GetPlayerDisplayName() const +{ + return IsLoggedIn() ? m_display_name : ""; +} + +u32 AchievementManager::GetPlayerScore() const +{ + return IsLoggedIn() ? m_player_score : 0; +} + +std::string AchievementManager::GetGameDisplayName() const +{ + return IsGameLoaded() ? m_game_data.title : ""; +} + +AchievementManager::PointSpread AchievementManager::TallyScore() const +{ + PointSpread spread{}; + if (!IsGameLoaded()) + return spread; + for (const auto& entry : m_unlock_map) + { + u32 points = entry.second.points; + spread.total_count++; + spread.total_points += points; + if (entry.second.remote_unlock_status == UnlockStatus::UnlockType::HARDCORE || + (hardcore_mode_enabled && entry.second.session_unlock_count > 0)) + { + spread.hard_unlocks++; + spread.hard_points += points; + } + else if (entry.second.remote_unlock_status == UnlockStatus::UnlockType::SOFTCORE || + entry.second.session_unlock_count > 0) + { + spread.soft_unlocks++; + spread.soft_points += points; + } + } + return spread; +} + +rc_api_fetch_game_data_response_t* AchievementManager::GetGameData() +{ + return &m_game_data; +} + +AchievementManager::UnlockStatus +AchievementManager::GetUnlockStatus(AchievementId achievement_id) const +{ + return m_unlock_map.at(achievement_id); +} + +void AchievementManager::GetAchievementProgress(AchievementId achievement_id, u32* value, + u32* target) +{ + rc_runtime_get_achievement_measured(&m_runtime, achievement_id, value, target); +} + void AchievementManager::CloseGame() { m_is_game_loaded = false; @@ -353,6 +416,7 @@ AchievementManager::ResponseType AchievementManager::VerifyCredentials(const std { Config::SetBaseOrCurrent(Config::RA_API_TOKEN, login_data.api_token); m_display_name = login_data.display_name; + m_player_score = login_data.score; } rc_api_destroy_login_response(&login_data); return r_type; @@ -618,30 +682,6 @@ void AchievementManager::HandleLeaderboardTriggeredEvent(const rc_runtime_event_ } } -AchievementManager::PointSpread AchievementManager::TallyScore() const -{ - PointSpread spread{}; - for (const auto& entry : m_unlock_map) - { - u32 points = entry.second.points; - spread.total_count++; - spread.total_points += points; - if (entry.second.remote_unlock_status == UnlockStatus::UnlockType::HARDCORE || - (hardcore_mode_enabled && entry.second.session_unlock_count > 0)) - { - spread.hard_unlocks++; - spread.hard_points += points; - } - else if (entry.second.remote_unlock_status == UnlockStatus::UnlockType::SOFTCORE || - entry.second.session_unlock_count > 0) - { - spread.soft_unlocks++; - spread.soft_points += points; - } - } - return spread; -} - // Every RetroAchievements API call, with only a partial exception for fetch_image, follows // the same design pattern (here, X is the name of the call): // Create a specific rc_api_X_request_t struct and populate with the necessary values diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index f35cd5f659..00c2055920 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -52,12 +52,26 @@ public: u32 soft_points; }; + struct UnlockStatus + { + AchievementId game_data_index = 0; + enum class UnlockType + { + LOCKED, + SOFTCORE, + HARDCORE + } remote_unlock_status = UnlockType::LOCKED; + u32 session_unlock_count = 0; + u32 points = 0; + }; + static AchievementManager* GetInstance(); void Init(); ResponseType Login(const std::string& password); void LoginAsync(const std::string& password, const ResponseCallback& callback); bool IsLoggedIn() const; void LoadGameByFilenameAsync(const std::string& iso_path, const ResponseCallback& callback); + bool IsGameLoaded() const; void LoadUnlockData(const ResponseCallback& callback); void ActivateDeactivateAchievements(); @@ -68,6 +82,14 @@ public: u32 MemoryPeeker(u32 address, u32 num_bytes, void* ud); void AchievementEventHandler(const rc_runtime_event_t* runtime_event); + std::string GetPlayerDisplayName() const; + u32 GetPlayerScore() const; + std::string GetGameDisplayName() const; + PointSpread TallyScore() const; + rc_api_fetch_game_data_response_t* GetGameData(); + UnlockStatus GetUnlockStatus(AchievementId achievement_id) const; + void GetAchievementProgress(AchievementId achievement_id, u32* value, u32* target); + void CloseGame(); void Logout(); void Shutdown(); @@ -95,8 +117,6 @@ private: void HandleLeaderboardCanceledEvent(const rc_runtime_event_t* runtime_event); void HandleLeaderboardTriggeredEvent(const rc_runtime_event_t* runtime_event); - PointSpread TallyScore() const; - template ResponseType Request(RcRequest rc_request, RcResponse* rc_response, const std::function& init_request, @@ -106,24 +126,13 @@ private: Core::System* m_system{}; bool m_is_runtime_initialized = false; std::string m_display_name; + u32 m_player_score = 0; std::array m_game_hash{}; u32 m_game_id = 0; rc_api_fetch_game_data_response_t m_game_data{}; bool m_is_game_loaded = false; time_t m_last_ping_time = 0; - struct UnlockStatus - { - AchievementId game_data_index = 0; - enum class UnlockType - { - LOCKED, - SOFTCORE, - HARDCORE - } remote_unlock_status = UnlockType::LOCKED; - u32 session_unlock_count = 0; - u32 points = 0; - }; std::unordered_map m_unlock_map; Common::WorkQueueThread> m_queue; From ebe77f149f4db747347811f769cbb1866639bf51 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Fri, 2 Jun 2023 21:20:42 -0400 Subject: [PATCH 2/5] Added AchievementHeaderWidget to AchievementsWindow This widget displays a header on the AchievementsWindow dialog above the tabs that shows the currently logged in user (if there is one) and the game they are playing (if there is one). --- .../Achievements/AchievementHeaderWidget.cpp | 140 ++++++++++++++++++ .../Achievements/AchievementHeaderWidget.h | 42 ++++++ .../Achievements/AchievementsWindow.cpp | 5 + .../Achievements/AchievementsWindow.h | 2 + Source/Core/DolphinQt/CMakeLists.txt | 2 + Source/Core/DolphinQt/DolphinQt.vcxproj | 2 + 6 files changed, 193 insertions(+) create mode 100644 Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.cpp create mode 100644 Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.h diff --git a/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.cpp new file mode 100644 index 0000000000..4dc3e66538 --- /dev/null +++ b/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.cpp @@ -0,0 +1,140 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef USE_RETRO_ACHIEVEMENTS +#include "DolphinQt/Achievements/AchievementHeaderWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "Core/AchievementManager.h" +#include "Core/Core.h" + +#include "DolphinQt/QtUtils/ModalMessageBox.h" +#include "DolphinQt/QtUtils/NonDefaultQPushButton.h" +#include "DolphinQt/QtUtils/SignalBlocking.h" +#include "DolphinQt/Settings.h" + +AchievementHeaderWidget::AchievementHeaderWidget(QWidget* parent) : QWidget(parent) +{ + m_user_name = new QLabel(); + m_user_points = new QLabel(); + m_game_name = new QLabel(); + m_game_points = new QLabel(); + m_game_progress_hard = new QProgressBar(); + m_game_progress_soft = new QProgressBar(); + m_rich_presence = new QLabel(); + + QVBoxLayout* m_user_right_col = new QVBoxLayout(); + m_user_right_col->addWidget(m_user_name); + m_user_right_col->addWidget(m_user_points); + QHBoxLayout* m_user_layout = new QHBoxLayout(); + // TODO: player badge goes here + m_user_layout->addLayout(m_user_right_col); + m_user_box = new QGroupBox(); + m_user_box->setLayout(m_user_layout); + + QVBoxLayout* m_game_right_col = new QVBoxLayout(); + m_game_right_col->addWidget(m_game_name); + m_game_right_col->addWidget(m_game_points); + m_game_right_col->addWidget(m_game_progress_hard); + m_game_right_col->addWidget(m_game_progress_soft); + QHBoxLayout* m_game_upper_row = new QHBoxLayout(); + // TODO: player badge and game badge go here + m_game_upper_row->addLayout(m_game_right_col); + QVBoxLayout* m_game_layout = new QVBoxLayout(); + m_game_layout->addLayout(m_game_upper_row); + m_game_layout->addWidget(m_rich_presence); + m_game_box = new QGroupBox(); + m_game_box->setLayout(m_game_layout); + + QVBoxLayout* m_total = new QVBoxLayout(); + m_total->addWidget(m_user_box); + m_total->addWidget(m_game_box); + + UpdateData(); + + m_total->setContentsMargins(0, 0, 0, 0); + m_total->setAlignment(Qt::AlignTop); + setLayout(m_total); +} + +void AchievementHeaderWidget::UpdateData() +{ + if (!AchievementManager::GetInstance()->IsLoggedIn()) + { + m_user_box->setVisible(false); + m_game_box->setVisible(false); + return; + } + + QString user_name = + QString::fromStdString(AchievementManager::GetInstance()->GetPlayerDisplayName()); + m_user_name->setText(user_name); + m_user_points->setText(tr("%1 points").arg(AchievementManager::GetInstance()->GetPlayerScore())); + + if (!AchievementManager::GetInstance()->IsGameLoaded()) + { + m_user_box->setVisible(true); + m_game_box->setVisible(false); + return; + } + + AchievementManager::PointSpread point_spread = AchievementManager::GetInstance()->TallyScore(); + m_game_name->setText( + QString::fromStdString(AchievementManager::GetInstance()->GetGameDisplayName())); + m_game_points->setText(GetPointsString(user_name, point_spread)); + m_game_progress_hard = new QProgressBar(); + m_game_progress_hard->setRange(0, point_spread.total_count); + m_game_progress_soft->setValue(point_spread.hard_unlocks); + m_game_progress_soft->setRange(0, point_spread.total_count); + m_game_progress_soft->setValue(point_spread.hard_unlocks + point_spread.soft_unlocks); + // TODO: RP needs a minor refactor to work here, will be a future PR + // m_rich_presence->setText(QString::fromStdString(AchievementManager::GetInstance()->GenerateRichPresence())); + // m_rich_presence->setVisible(Config::Get(Config::RA_RICH_PRESENCE_ENABLED)); + m_rich_presence->setText(QString{}); + m_rich_presence->setVisible(false); + + m_user_box->setVisible(false); + m_game_box->setVisible(true); +} + +QString +AchievementHeaderWidget::GetPointsString(const QString& user_name, + const AchievementManager::PointSpread& point_spread) const +{ + if (point_spread.soft_points > 0) + { + return tr("%1 has unlocked %2/%3 achievements (%4 hardcore) worth %5/%6 points (%7 hardcore)") + .arg(user_name) + .arg(point_spread.hard_unlocks + point_spread.soft_unlocks) + .arg(point_spread.total_count) + .arg(point_spread.hard_unlocks) + .arg(point_spread.hard_points + point_spread.soft_points) + .arg(point_spread.total_points) + .arg(point_spread.hard_points); + } + else + { + return tr("%1 has unlocked %2/%3 achievements worth %4/%5 points") + .arg(user_name) + .arg(point_spread.hard_unlocks) + .arg(point_spread.total_count) + .arg(point_spread.hard_points) + .arg(point_spread.total_points); + } +} + +#endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.h b/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.h new file mode 100644 index 0000000000..b99457f2f1 --- /dev/null +++ b/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.h @@ -0,0 +1,42 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#ifdef USE_RETRO_ACHIEVEMENTS +#include + +#include "Core/AchievementManager.h" + +class QGroupBox; +class QLabel; +class QProgressBar; +class QVBoxLayout; + +class AchievementHeaderWidget final : public QWidget +{ + Q_OBJECT +public: + explicit AchievementHeaderWidget(QWidget* parent); + void UpdateData(); + +private: + QString GetPointsString(const QString& user_name, + const AchievementManager::PointSpread& point_spread) const; + + QGroupBox* m_common_box; + QVBoxLayout* m_common_layout; + + QLabel* m_user_name; + QLabel* m_user_points; + QLabel* m_game_name; + QLabel* m_game_points; + QProgressBar* m_game_progress_hard; + QProgressBar* m_game_progress_soft; + QLabel* m_rich_presence; + + QGroupBox* m_user_box; + QGroupBox* m_game_box; +}; + +#endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp index b23843f75b..1d08ede223 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp @@ -8,6 +8,7 @@ #include #include +#include "DolphinQt/Achievements/AchievementHeaderWidget.h" #include "DolphinQt/Achievements/AchievementSettingsWidget.h" #include "DolphinQt/QtUtils/WrapInScrollArea.h" @@ -30,6 +31,7 @@ void AchievementsWindow::CreateMainLayout() { auto* layout = new QVBoxLayout(); + m_header_widget = new AchievementHeaderWidget(this); m_tab_widget = new QTabWidget(); m_tab_widget->addTab( GetWrappedWidget(new AchievementSettingsWidget(m_tab_widget, this), this, 125, 100), @@ -37,6 +39,7 @@ void AchievementsWindow::CreateMainLayout() m_button_box = new QDialogButtonBox(QDialogButtonBox::Close); + layout->addWidget(m_header_widget); layout->addWidget(m_tab_widget); layout->addWidget(m_button_box); @@ -50,6 +53,8 @@ void AchievementsWindow::ConnectWidgets() void AchievementsWindow::UpdateData() { + m_header_widget->UpdateData(); + m_header_widget->setVisible(AchievementManager::GetInstance()->IsLoggedIn()); update(); } diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h index 9570e8127f..0e9a8bfdbe 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h @@ -6,6 +6,7 @@ #ifdef USE_RETRO_ACHIEVEMENTS #include +class AchievementHeaderWidget; class QTabWidget; class QDialogButtonBox; @@ -21,6 +22,7 @@ private: void showEvent(QShowEvent* event); void ConnectWidgets(); + AchievementHeaderWidget* m_header_widget; QTabWidget* m_tab_widget; QDialogButtonBox* m_button_box; }; diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index e302287677..6ce4591cc6 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -27,6 +27,8 @@ add_executable(dolphin-emu CheatSearchWidget.h CheatsManager.cpp CheatsManager.h + Achievements/AchievementHeaderWidget.cpp + Achievements/AchievementHeaderWidget.h Achievements/AchievementSettingsWidget.cpp Achievements/AchievementSettingsWidget.h Achievements/AchievementsWindow.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 79b05b372b..1d6512692e 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -50,6 +50,7 @@ + @@ -256,6 +257,7 @@ + From 582042de1f80e68c72952e124fe18698d2cb494f Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Fri, 2 Jun 2023 21:25:01 -0400 Subject: [PATCH 3/5] Added AchievementProgressWidget to AchievementsWindow This widget is a tab in the AchievementsWindow that displays the player's current achievement progress: which achievements are locked or unlocked, and the progress of achievements that have progress metrics. --- .../AchievementProgressWidget.cpp | 124 ++++++++++++++++++ .../Achievements/AchievementProgressWidget.h | 34 +++++ .../Achievements/AchievementsWindow.cpp | 7 + .../Achievements/AchievementsWindow.h | 4 + Source/Core/DolphinQt/CMakeLists.txt | 2 + Source/Core/DolphinQt/DolphinQt.vcxproj | 2 + 6 files changed, 173 insertions(+) create mode 100644 Source/Core/DolphinQt/Achievements/AchievementProgressWidget.cpp create mode 100644 Source/Core/DolphinQt/Achievements/AchievementProgressWidget.h diff --git a/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.cpp new file mode 100644 index 0000000000..9f8c38bfe5 --- /dev/null +++ b/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.cpp @@ -0,0 +1,124 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef USE_RETRO_ACHIEVEMENTS +#include "DolphinQt/Achievements/AchievementProgressWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "Core/AchievementManager.h" +#include "Core/Config/AchievementSettings.h" +#include "Core/Config/MainSettings.h" +#include "Core/Core.h" + +#include "DolphinQt/Config/ControllerInterface/ControllerInterfaceWindow.h" +#include "DolphinQt/QtUtils/ModalMessageBox.h" +#include "DolphinQt/QtUtils/NonDefaultQPushButton.h" +#include "DolphinQt/QtUtils/SignalBlocking.h" +#include "DolphinQt/Settings.h" + +AchievementProgressWidget::AchievementProgressWidget(QWidget* parent) : QWidget(parent) +{ + m_common_box = new QGroupBox(); + m_common_layout = new QVBoxLayout(); + + UpdateData(); + + m_common_box->setLayout(m_common_layout); + + auto* layout = new QVBoxLayout; + layout->setContentsMargins(0, 0, 0, 0); + layout->setAlignment(Qt::AlignTop); + layout->addWidget(m_common_box); + setLayout(layout); +} + +QGroupBox* +AchievementProgressWidget::CreateAchievementBox(const rc_api_achievement_definition_t* achievement) +{ + QLabel* a_title = new QLabel(QString::fromUtf8(achievement->title, strlen(achievement->title))); + QLabel* a_description = + new QLabel(QString::fromUtf8(achievement->description, strlen(achievement->description))); + QLabel* a_points = new QLabel(tr("%1 points").arg(achievement->points)); + QLabel* a_status = new QLabel(GetStatusString(achievement->id)); + QProgressBar* a_progress_bar = new QProgressBar(); + unsigned int value = 0; + unsigned int target = 0; + AchievementManager::GetInstance()->GetAchievementProgress(achievement->id, &value, &target); + if (target > 0) + { + a_progress_bar->setRange(0, target); + a_progress_bar->setValue(value); + } + else + { + a_progress_bar->setVisible(false); + } + + QVBoxLayout* a_col_right = new QVBoxLayout(); + a_col_right->addWidget(a_title); + a_col_right->addWidget(a_description); + a_col_right->addWidget(a_points); + a_col_right->addWidget(a_status); + a_col_right->addWidget(a_progress_bar); + QHBoxLayout* a_total = new QHBoxLayout(); + // TODO: achievement badge goes here + a_total->addLayout(a_col_right); + QGroupBox* a_group_box = new QGroupBox(); + a_group_box->setLayout(a_total); + return a_group_box; +} + +void AchievementProgressWidget::UpdateData() +{ + QLayoutItem* item; + while ((item = m_common_layout->layout()->takeAt(0)) != nullptr) + { + delete item->widget(); + delete item; + } + + const auto* game_data = AchievementManager::GetInstance()->GetGameData(); + for (u32 ix = 0; ix < game_data->num_achievements; ix++) + { + m_common_layout->addWidget(CreateAchievementBox(game_data->achievements + ix)); + } +} + +QString AchievementProgressWidget::GetStatusString(u32 achievement_id) const +{ + const auto unlock_status = AchievementManager::GetInstance()->GetUnlockStatus(achievement_id); + if (unlock_status.session_unlock_count > 0) + { + if (Config::Get(Config::RA_ENCORE_ENABLED)) + { + return tr("Unlocked %1 times this session").arg(unlock_status.session_unlock_count); + } + return tr("Unlocked this session"); + } + switch (unlock_status.remote_unlock_status) + { + case AchievementManager::UnlockStatus::UnlockType::LOCKED: + return tr("Locked"); + case AchievementManager::UnlockStatus::UnlockType::SOFTCORE: + return tr("Unlocked (Casual)"); + case AchievementManager::UnlockStatus::UnlockType::HARDCORE: + return tr("Unlocked"); + } + return {}; +} + +#endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.h b/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.h new file mode 100644 index 0000000000..b1e09e40d8 --- /dev/null +++ b/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.h @@ -0,0 +1,34 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#ifdef USE_RETRO_ACHIEVEMENTS +#include + +#include "Common/CommonTypes.h" + +class QCheckBox; +class QGroupBox; +class QLineEdit; +class QPushButton; +class QVBoxLayout; + +struct rc_api_achievement_definition_t; + +class AchievementProgressWidget final : public QWidget +{ + Q_OBJECT +public: + explicit AchievementProgressWidget(QWidget* parent); + void UpdateData(); + +private: + QGroupBox* CreateAchievementBox(const rc_api_achievement_definition_t* achievement); + QString GetStatusString(u32 achievement_id) const; + + QGroupBox* m_common_box; + QVBoxLayout* m_common_layout; +}; + +#endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp index 1d08ede223..451408fde5 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp @@ -9,6 +9,7 @@ #include #include "DolphinQt/Achievements/AchievementHeaderWidget.h" +#include "DolphinQt/Achievements/AchievementProgressWidget.h" #include "DolphinQt/Achievements/AchievementSettingsWidget.h" #include "DolphinQt/QtUtils/WrapInScrollArea.h" @@ -33,9 +34,12 @@ void AchievementsWindow::CreateMainLayout() m_header_widget = new AchievementHeaderWidget(this); m_tab_widget = new QTabWidget(); + m_progress_widget = new AchievementProgressWidget(m_tab_widget); m_tab_widget->addTab( GetWrappedWidget(new AchievementSettingsWidget(m_tab_widget, this), this, 125, 100), tr("Settings")); + m_tab_widget->addTab(GetWrappedWidget(m_progress_widget, this, 125, 100), tr("Progress")); + m_tab_widget->setTabVisible(1, AchievementManager::GetInstance()->IsGameLoaded()); m_button_box = new QDialogButtonBox(QDialogButtonBox::Close); @@ -55,6 +59,9 @@ void AchievementsWindow::UpdateData() { m_header_widget->UpdateData(); m_header_widget->setVisible(AchievementManager::GetInstance()->IsLoggedIn()); + // Settings tab handles its own updates ... indeed, that calls this + m_progress_widget->UpdateData(); + m_tab_widget->setTabVisible(1, AchievementManager::GetInstance()->IsGameLoaded()); update(); } diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h index 0e9a8bfdbe..1d9f089836 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h @@ -6,7 +6,10 @@ #ifdef USE_RETRO_ACHIEVEMENTS #include +#include "Core/AchievementManager.h" + class AchievementHeaderWidget; +class AchievementProgressWidget; class QTabWidget; class QDialogButtonBox; @@ -24,6 +27,7 @@ private: AchievementHeaderWidget* m_header_widget; QTabWidget* m_tab_widget; + AchievementProgressWidget* m_progress_widget; QDialogButtonBox* m_button_box; }; diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 6ce4591cc6..7087f03429 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -29,6 +29,8 @@ add_executable(dolphin-emu CheatsManager.h Achievements/AchievementHeaderWidget.cpp Achievements/AchievementHeaderWidget.h + Achievements/AchievementProgressWidget.cpp + Achievements/AchievementProgressWidget.h Achievements/AchievementSettingsWidget.cpp Achievements/AchievementSettingsWidget.h Achievements/AchievementsWindow.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 1d6512692e..ac769a10b9 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -51,6 +51,7 @@ + @@ -258,6 +259,7 @@ + From fbaeaf305bb713ca0a507713b132ee42f10a6e51 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Fri, 2 Jun 2023 21:31:30 -0400 Subject: [PATCH 4/5] Add UpdateCallback to AchievementManager AchievementManager now has a SetUpdateCallback method for providing a single universal callback for anytime something important changes in the achievement state, such as logging in/out, game load/close, or events such as achievement unlocks. AchievementsWindow sets this callback in its own init to its UpdateData method so that the AchievementsWindow gets updated when one of these changes takes place. --- Source/Core/Core/AchievementManager.cpp | 27 +++++++++++++++++-- Source/Core/Core/AchievementManager.h | 3 +++ .../Achievements/AchievementsWindow.cpp | 3 +++ .../Achievements/AchievementsWindow.h | 4 ++- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 27e82a8c55..407e0bed58 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -38,11 +38,20 @@ void AchievementManager::Init() } } +void AchievementManager::SetUpdateCallback(UpdateCallback callback) +{ + m_update_callback = std::move(callback); + m_update_callback(); +} + AchievementManager::ResponseType AchievementManager::Login(const std::string& password) { if (!m_is_runtime_initialized) return AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED; - return VerifyCredentials(password); + AchievementManager::ResponseType r_type = VerifyCredentials(password); + if (m_update_callback) + m_update_callback(); + return r_type; } void AchievementManager::LoginAsync(const std::string& password, const ResponseCallback& callback) @@ -52,7 +61,11 @@ void AchievementManager::LoginAsync(const std::string& password, const ResponseC callback(AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED); return; } - m_queue.EmplaceItem([this, password, callback] { callback(VerifyCredentials(password)); }); + m_queue.EmplaceItem([this, password, callback] { + callback(VerifyCredentials(password)); + if (m_update_callback) + m_update_callback(); + }); } bool AchievementManager::IsLoggedIn() const @@ -179,6 +192,8 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, // Reset this to zero so that RP immediately triggers on the first frame m_last_ping_time = 0; + if (m_update_callback) + m_update_callback(); callback(fetch_game_data_response); }); } @@ -199,6 +214,8 @@ void AchievementManager::LoadUnlockData(const ResponseCallback& callback) } callback(FetchUnlockData(false)); + if (m_update_callback) + m_update_callback(); }); } @@ -316,6 +333,8 @@ void AchievementManager::AchievementEventHandler(const rc_runtime_event_t* runti HandleLeaderboardTriggeredEvent(runtime_event); break; } + if (m_update_callback) + m_update_callback(); } std::string AchievementManager::GetPlayerDisplayName() const @@ -386,12 +405,16 @@ void AchievementManager::CloseGame() ActivateDeactivateAchievements(); ActivateDeactivateLeaderboards(); ActivateDeactivateRichPresence(); + if (m_update_callback) + m_update_callback(); } void AchievementManager::Logout() { CloseGame(); Config::SetBaseOrCurrent(Config::RA_API_TOKEN, ""); + if (m_update_callback) + m_update_callback(); } void AchievementManager::Shutdown() diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 00c2055920..9a4289612d 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -41,6 +41,7 @@ public: UNKNOWN_FAILURE }; using ResponseCallback = std::function; + using UpdateCallback = std::function; struct PointSpread { @@ -67,6 +68,7 @@ public: static AchievementManager* GetInstance(); void Init(); + void SetUpdateCallback(UpdateCallback callback); ResponseType Login(const std::string& password); void LoginAsync(const std::string& password, const ResponseCallback& callback); bool IsLoggedIn() const; @@ -125,6 +127,7 @@ private: rc_runtime_t m_runtime{}; Core::System* m_system{}; bool m_is_runtime_initialized = false; + UpdateCallback m_update_callback; std::string m_display_name; u32 m_player_score = 0; std::array m_game_hash{}; diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp index 451408fde5..40de94ee92 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp @@ -11,6 +11,7 @@ #include "DolphinQt/Achievements/AchievementHeaderWidget.h" #include "DolphinQt/Achievements/AchievementProgressWidget.h" #include "DolphinQt/Achievements/AchievementSettingsWidget.h" +#include "DolphinQt/QtUtils/QueueOnObject.h" #include "DolphinQt/QtUtils/WrapInScrollArea.h" AchievementsWindow::AchievementsWindow(QWidget* parent) : QDialog(parent) @@ -20,6 +21,8 @@ AchievementsWindow::AchievementsWindow(QWidget* parent) : QDialog(parent) CreateMainLayout(); ConnectWidgets(); + AchievementManager::GetInstance()->SetUpdateCallback( + [this] { QueueOnObject(this, &AchievementsWindow::UpdateData); }); } void AchievementsWindow::showEvent(QShowEvent* event) diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h index 1d9f089836..d8407a3a17 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h @@ -7,11 +7,13 @@ #include #include "Core/AchievementManager.h" +#include "DolphinQt/QtUtils/QueueOnObject.h" class AchievementHeaderWidget; class AchievementProgressWidget; -class QTabWidget; class QDialogButtonBox; +class QTabWidget; +class UpdateCallback; class AchievementsWindow : public QDialog { From ccc9d0e5ea12e33c06be1a695873b0fce56e1bff Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Fri, 9 Jun 2023 17:05:52 -0400 Subject: [PATCH 5/5] Synchronized Achievement Window Expanded the use of the lock mutex already used for loading the player's existing unlock status to guard against races involving the Achievements dialog window reading from data AchievementManager might be in the process of updating. The lock has been exposed publicly and the AchievementsWindow uses it in its UpdateData method, and anywhere else that might modify data used to render that window has also been wrapped with it. --- Source/Core/Core/AchievementManager.cpp | 67 ++++++++++++------- Source/Core/Core/AchievementManager.h | 1 + .../Achievements/AchievementsWindow.cpp | 15 +++-- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 407e0bed58..ef23dcf163 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -48,7 +48,11 @@ AchievementManager::ResponseType AchievementManager::Login(const std::string& pa { if (!m_is_runtime_initialized) return AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED; - AchievementManager::ResponseType r_type = VerifyCredentials(password); + AchievementManager::ResponseType r_type = AchievementManager::ResponseType::UNKNOWN_FAILURE; + { + std::lock_guard lg{m_lock}; + r_type = VerifyCredentials(password); + } if (m_update_callback) m_update_callback(); return r_type; @@ -62,7 +66,10 @@ void AchievementManager::LoginAsync(const std::string& password, const ResponseC return; } m_queue.EmplaceItem([this, password, callback] { + { + std::lock_guard lg{m_lock}; callback(VerifyCredentials(password)); + } if (m_update_callback) m_update_callback(); }); @@ -154,11 +161,11 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, } const auto fetch_game_data_response = FetchGameData(); - m_is_game_loaded = fetch_game_data_response == ResponseType::SUCCESS; - if (!m_is_game_loaded) + if (fetch_game_data_response != ResponseType::SUCCESS) { OSD::AddMessage("Unable to retrieve data from RetroAchievements server.", OSD::Duration::VERY_LONG, OSD::Color::RED); + return; } // Claim the lock, then queue the fetch unlock data calls, then initialize the unlock map in @@ -167,6 +174,7 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, // it. { std::lock_guard lg{m_lock}; + m_is_game_loaded = true; LoadUnlockData([](ResponseType r_type) {}); ActivateDeactivateAchievements(); PointSpread spread = TallyScore(); @@ -318,25 +326,33 @@ u32 AchievementManager::MemoryPeeker(u32 address, u32 num_bytes, void* ud) void AchievementManager::AchievementEventHandler(const rc_runtime_event_t* runtime_event) { - switch (runtime_event->type) { - case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: - HandleAchievementTriggeredEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_LBOARD_STARTED: - HandleLeaderboardStartedEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_LBOARD_CANCELED: - HandleLeaderboardCanceledEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_LBOARD_TRIGGERED: - HandleLeaderboardTriggeredEvent(runtime_event); - break; + std::lock_guard lg{m_lock}; + switch (runtime_event->type) + { + case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: + HandleAchievementTriggeredEvent(runtime_event); + break; + case RC_RUNTIME_EVENT_LBOARD_STARTED: + HandleLeaderboardStartedEvent(runtime_event); + break; + case RC_RUNTIME_EVENT_LBOARD_CANCELED: + HandleLeaderboardCanceledEvent(runtime_event); + break; + case RC_RUNTIME_EVENT_LBOARD_TRIGGERED: + HandleLeaderboardTriggeredEvent(runtime_event); + break; + } } if (m_update_callback) m_update_callback(); } +std::recursive_mutex* AchievementManager::GetLock() +{ + return &m_lock; +} + std::string AchievementManager::GetPlayerDisplayName() const { return IsLoggedIn() ? m_display_name : ""; @@ -397,14 +413,17 @@ void AchievementManager::GetAchievementProgress(AchievementId achievement_id, u3 void AchievementManager::CloseGame() { - m_is_game_loaded = false; - m_game_id = 0; - m_queue.Cancel(); - m_unlock_map.clear(); - m_system = nullptr; - ActivateDeactivateAchievements(); - ActivateDeactivateLeaderboards(); - ActivateDeactivateRichPresence(); + { + std::lock_guard lg{m_lock}; + m_is_game_loaded = false; + m_game_id = 0; + m_queue.Cancel(); + m_unlock_map.clear(); + m_system = nullptr; + ActivateDeactivateAchievements(); + ActivateDeactivateLeaderboards(); + ActivateDeactivateRichPresence(); + } if (m_update_callback) m_update_callback(); } diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 9a4289612d..14b6b0ec77 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -84,6 +84,7 @@ public: u32 MemoryPeeker(u32 address, u32 num_bytes, void* ud); void AchievementEventHandler(const rc_runtime_event_t* runtime_event); + std::recursive_mutex* GetLock(); std::string GetPlayerDisplayName() const; u32 GetPlayerScore() const; std::string GetGameDisplayName() const; diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp index 40de94ee92..d791d1b107 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp @@ -4,6 +4,8 @@ #ifdef USE_RETRO_ACHIEVEMENTS #include "DolphinQt/Achievements/AchievementsWindow.h" +#include + #include #include #include @@ -60,11 +62,14 @@ void AchievementsWindow::ConnectWidgets() void AchievementsWindow::UpdateData() { - m_header_widget->UpdateData(); - m_header_widget->setVisible(AchievementManager::GetInstance()->IsLoggedIn()); - // Settings tab handles its own updates ... indeed, that calls this - m_progress_widget->UpdateData(); - m_tab_widget->setTabVisible(1, AchievementManager::GetInstance()->IsGameLoaded()); + { + std::lock_guard lg{*AchievementManager::GetInstance()->GetLock()}; + m_header_widget->UpdateData(); + m_header_widget->setVisible(AchievementManager::GetInstance()->IsLoggedIn()); + // Settings tab handles its own updates ... indeed, that calls this + m_progress_widget->UpdateData(); + m_tab_widget->setTabVisible(1, AchievementManager::GetInstance()->IsGameLoaded()); + } update(); }