summaryrefslogtreecommitdiff
path: root/Source
diff options
context:
space:
mode:
authorAdmiral H. Curtiss <pikachu025@gmail.com>2023-10-15 21:40:58 +0200
committerGitHub <noreply@github.com>2023-10-15 21:40:58 +0200
commitbbf3fed93c90d3edddfaab1e970a2326fe06ac51 (patch)
tree9223a25ece5e0945fe3b0e99a701cd5d7f2e577e /Source
parentdaf9ff012a76000f33a4a3767ca6608bd55de11a (diff)
parentb824d55093a961efbdcdfd4f330d96306e835070 (diff)
Merge pull request #12027 from LillyJadeKatrin/retroachievements-leaderboards-tab
RetroAchievements - Leaderboards Tab
Diffstat (limited to 'Source')
-rw-r--r--Source/Core/Core/AchievementManager.cpp107
-rw-r--r--Source/Core/Core/AchievementManager.h19
-rw-r--r--Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.cpp127
-rw-r--r--Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.h24
-rw-r--r--Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp7
-rw-r--r--Source/Core/DolphinQt/Achievements/AchievementsWindow.h2
-rw-r--r--Source/Core/DolphinQt/CMakeLists.txt2
-rw-r--r--Source/Core/DolphinQt/DolphinQt.vcxproj2
8 files changed, 288 insertions, 2 deletions
diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp
index 1b6ff11d8e..d5acdc84e9 100644
--- a/Source/Core/Core/AchievementManager.cpp
+++ b/Source/Core/Core/AchievementManager.cpp
@@ -7,6 +7,7 @@
#include <fmt/format.h>
+#include <rcheevos/include/rc_api_info.h>
#include <rcheevos/include/rc_hash.h>
#include "Common/HttpRequest.h"
@@ -268,9 +269,15 @@ void AchievementManager::ActivateDeactivateLeaderboards()
for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++)
{
auto leaderboard = m_game_data.leaderboards[ix];
+ u32 leaderboard_id = leaderboard.id;
if (m_is_game_loaded && leaderboards_enabled && hardcore_mode_enabled)
{
- rc_runtime_activate_lboard(&m_runtime, leaderboard.id, leaderboard.definition, nullptr, 0);
+ rc_runtime_activate_lboard(&m_runtime, leaderboard_id, leaderboard.definition, nullptr, 0);
+ m_queue.EmplaceItem([this, leaderboard_id] {
+ FetchBoardInfo(leaderboard_id);
+ if (m_update_callback)
+ m_update_callback();
+ });
}
else
{
@@ -712,6 +719,12 @@ AchievementManager::GetAchievementProgress(AchievementId achievement_id, u32* va
return ResponseType::SUCCESS;
}
+const std::unordered_map<AchievementManager::AchievementId, AchievementManager::LeaderboardStatus>&
+AchievementManager::GetLeaderboardsInfo() const
+{
+ return m_leaderboard_map;
+}
+
AchievementManager::RichPresence AchievementManager::GetRichPresence()
{
std::lock_guard lg{m_lock};
@@ -732,6 +745,7 @@ void AchievementManager::CloseGame()
m_game_id = 0;
m_game_badge.name = "";
m_unlock_map.clear();
+ m_leaderboard_map.clear();
rc_api_destroy_fetch_game_data_response(&m_game_data);
std::memset(&m_game_data, 0, sizeof(m_game_data));
m_queue.Cancel();
@@ -955,6 +969,90 @@ AchievementManager::ResponseType AchievementManager::FetchUnlockData(bool hardco
return r_type;
}
+AchievementManager::ResponseType AchievementManager::FetchBoardInfo(AchievementId leaderboard_id)
+{
+ std::string username = Config::Get(Config::RA_USERNAME);
+ LeaderboardStatus lboard{};
+
+ {
+ rc_api_fetch_leaderboard_info_response_t board_info{};
+ const rc_api_fetch_leaderboard_info_request_t fetch_board_request = {
+ .leaderboard_id = leaderboard_id, .count = 4, .first_entry = 1, .username = nullptr};
+ const ResponseType r_type =
+ Request<rc_api_fetch_leaderboard_info_request_t, rc_api_fetch_leaderboard_info_response_t>(
+ fetch_board_request, &board_info, rc_api_init_fetch_leaderboard_info_request,
+ rc_api_process_fetch_leaderboard_info_response);
+ if (r_type != ResponseType::SUCCESS)
+ {
+ ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to fetch info for leaderboard ID {}.", leaderboard_id);
+ rc_api_destroy_fetch_leaderboard_info_response(&board_info);
+ return r_type;
+ }
+ lboard.name = board_info.title;
+ lboard.description = board_info.description;
+ lboard.entries.clear();
+ for (u32 i = 0; i < board_info.num_entries; ++i)
+ {
+ const auto& org_entry = board_info.entries[i];
+ LeaderboardEntry dest_entry =
+ LeaderboardEntry{.username = org_entry.username, .rank = org_entry.rank};
+ if (rc_runtime_format_lboard_value(dest_entry.score.data(), FORMAT_SIZE, org_entry.score,
+ board_info.format) == 0)
+ {
+ ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to format leaderboard score {}.", org_entry.score);
+ strncpy(dest_entry.score.data(), fmt::format("{}", org_entry.score).c_str(), FORMAT_SIZE);
+ }
+ lboard.entries[org_entry.index] = dest_entry;
+ }
+ rc_api_destroy_fetch_leaderboard_info_response(&board_info);
+ }
+
+ {
+ // Retrieve, if exists, the player's entry, the two entries above the player, and the two
+ // entries below the player, for a total of five entries. Technically I only need one entry
+ // below, but the API is ambiguous what happens if an even number and a username are provided.
+ rc_api_fetch_leaderboard_info_response_t board_info{};
+ const rc_api_fetch_leaderboard_info_request_t fetch_board_request = {
+ .leaderboard_id = leaderboard_id,
+ .count = 5,
+ .first_entry = 0,
+ .username = username.c_str()};
+ const ResponseType r_type =
+ Request<rc_api_fetch_leaderboard_info_request_t, rc_api_fetch_leaderboard_info_response_t>(
+ fetch_board_request, &board_info, rc_api_init_fetch_leaderboard_info_request,
+ rc_api_process_fetch_leaderboard_info_response);
+ if (r_type != ResponseType::SUCCESS)
+ {
+ ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to fetch info for leaderboard ID {}.", leaderboard_id);
+ rc_api_destroy_fetch_leaderboard_info_response(&board_info);
+ return r_type;
+ }
+ for (u32 i = 0; i < board_info.num_entries; ++i)
+ {
+ const auto& org_entry = board_info.entries[i];
+ LeaderboardEntry dest_entry =
+ LeaderboardEntry{.username = org_entry.username, .rank = org_entry.rank};
+ if (rc_runtime_format_lboard_value(dest_entry.score.data(), FORMAT_SIZE, org_entry.score,
+ board_info.format) == 0)
+ {
+ ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to format leaderboard score {}.", org_entry.score);
+ strncpy(dest_entry.score.data(), fmt::format("{}", org_entry.score).c_str(), FORMAT_SIZE);
+ }
+ lboard.entries[org_entry.index] = dest_entry;
+ if (org_entry.username == username)
+ lboard.player_index = org_entry.index;
+ }
+ rc_api_destroy_fetch_leaderboard_info_response(&board_info);
+ }
+
+ {
+ std::lock_guard lg{m_lock};
+ m_leaderboard_map[leaderboard_id] = lboard;
+ }
+
+ return ResponseType::SUCCESS;
+}
+
void AchievementManager::ActivateDeactivateAchievement(AchievementId id, bool enabled,
bool unofficial, bool encore)
{
@@ -1198,7 +1296,12 @@ void AchievementManager::HandleLeaderboardTriggeredEvent(const rc_runtime_event_
m_game_data.leaderboards[ix].title),
OSD::Duration::VERY_LONG, OSD::Color::YELLOW);
}
- return;
+ m_queue.EmplaceItem([this, event_id] {
+ FetchBoardInfo(event_id);
+ if (m_update_callback)
+ m_update_callback();
+ });
+ break;
}
}
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid leaderboard triggered event with id {}.", event_id);
diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h
index 2e2e3e938d..b5c1eee200 100644
--- a/Source/Core/Core/AchievementManager.h
+++ b/Source/Core/Core/AchievementManager.h
@@ -54,6 +54,7 @@ public:
using AchievementId = u32;
static constexpr size_t FORMAT_SIZE = 24;
using FormattedValue = std::array<char, FORMAT_SIZE>;
+ using LeaderboardRank = u32;
static constexpr size_t RP_SIZE = 256;
using RichPresence = std::array<char, RP_SIZE>;
using Badge = std::vector<u8>;
@@ -83,6 +84,21 @@ public:
static constexpr std::string_view GOLD = "#FFD700";
static constexpr std::string_view BLUE = "#0B71C1";
+ struct LeaderboardEntry
+ {
+ std::string username;
+ FormattedValue score;
+ LeaderboardRank rank;
+ };
+
+ struct LeaderboardStatus
+ {
+ std::string name;
+ std::string description;
+ u32 player_index = 0;
+ std::unordered_map<u32, LeaderboardEntry> entries;
+ };
+
static AchievementManager* GetInstance();
void Init();
void SetUpdateCallback(UpdateCallback callback);
@@ -113,6 +129,7 @@ public:
const UnlockStatus& GetUnlockStatus(AchievementId achievement_id) const;
AchievementManager::ResponseType GetAchievementProgress(AchievementId achievement_id, u32* value,
u32* target);
+ const std::unordered_map<AchievementId, LeaderboardStatus>& GetLeaderboardsInfo() const;
RichPresence GetRichPresence();
void CloseGame();
@@ -129,6 +146,7 @@ private:
ResponseType StartRASession();
ResponseType FetchGameData();
ResponseType FetchUnlockData(bool hardcore);
+ ResponseType FetchBoardInfo(AchievementId leaderboard_id);
void ActivateDeactivateAchievement(AchievementId id, bool enabled, bool unofficial, bool encore);
void GenerateRichPresence();
@@ -165,6 +183,7 @@ private:
time_t m_last_ping_time = 0;
std::unordered_map<AchievementId, UnlockStatus> m_unlock_map;
+ std::unordered_map<AchievementId, LeaderboardStatus> m_leaderboard_map;
Common::WorkQueueThread<std::function<void()>> m_queue;
Common::WorkQueueThread<std::function<void()>> m_image_queue;
diff --git a/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.cpp
new file mode 100644
index 0000000000..243bc4453c
--- /dev/null
+++ b/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.cpp
@@ -0,0 +1,127 @@
+// Copyright 2023 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifdef USE_RETRO_ACHIEVEMENTS
+#include "DolphinQt/Achievements/AchievementLeaderboardWidget.h"
+
+#include <QCheckBox>
+#include <QGroupBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QString>
+#include <QVBoxLayout>
+
+#include <fmt/format.h>
+
+#include <rcheevos/include/rc_api_runtime.h>
+#include <rcheevos/include/rc_api_user.h>
+#include <rcheevos/include/rc_runtime.h>
+
+#include "Common/CommonTypes.h"
+#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/ClearLayoutRecursively.h"
+#include "DolphinQt/QtUtils/ModalMessageBox.h"
+#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
+#include "DolphinQt/QtUtils/SignalBlocking.h"
+#include "DolphinQt/Settings.h"
+
+AchievementLeaderboardWidget::AchievementLeaderboardWidget(QWidget* parent) : QWidget(parent)
+{
+ m_common_box = new QGroupBox();
+ m_common_layout = new QGridLayout();
+
+ {
+ std::lock_guard lg{*AchievementManager::GetInstance()->GetLock()};
+ 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);
+}
+
+void AchievementLeaderboardWidget::UpdateData()
+{
+ ClearLayoutRecursively(m_common_layout);
+
+ if (!AchievementManager::GetInstance()->IsGameLoaded())
+ return;
+ const auto& leaderboards = AchievementManager::GetInstance()->GetLeaderboardsInfo();
+ int row = 0;
+ for (const auto& board_row : leaderboards)
+ {
+ const AchievementManager::LeaderboardStatus& board = board_row.second;
+ QLabel* a_title = new QLabel(QString::fromStdString(board.name));
+ QLabel* a_description = new QLabel(QString::fromStdString(board.description));
+ QVBoxLayout* a_col_left = new QVBoxLayout();
+ a_col_left->addWidget(a_title);
+ a_col_left->addWidget(a_description);
+ if (row > 0)
+ {
+ QFrame* a_divider = new QFrame();
+ a_divider->setFrameShape(QFrame::HLine);
+ m_common_layout->addWidget(a_divider, row - 1, 0);
+ }
+ m_common_layout->addLayout(a_col_left, row, 0);
+ // Each leaderboard entry is displayed with four values. These are *generally* intended to be,
+ // in order, the first place entry, the entry one above the player, the player's entry, and
+ // the entry one below the player.
+ // Edge cases:
+ // * If there are fewer than four entries in the leaderboard, all entries will be shown in
+ // order and the remainder of the list will be padded with empty values.
+ // * If the player does not currently have a score in the leaderboard, or is in the top 3,
+ // the four slots will be the top four players in order.
+ // * If the player is last place, the player will be in the fourth slot, and the second and
+ // third slots will be the two players above them. The first slot will always be first place.
+ std::array<u32, 4> to_display{1, 2, 3, 4};
+ if (board.player_index > to_display.size() - 1)
+ {
+ // If the rank one below than the player is found, offset = 1.
+ u32 offset = static_cast<u32>(board.entries.count(board.player_index + 1));
+ // Example: player is 10th place but not last
+ // to_display = {1, 10-3+1+1, 10-3+1+2, 10-3+1+3} = {1, 9, 10, 11}
+ // Example: player is 15th place and is last
+ // to_display = {1, 15-3+0+1, 15-3+0+2, 15-3+0+3} = {1, 13, 14, 15}
+ for (size_t i = 1; i < to_display.size(); ++i)
+ to_display[i] = board.player_index - 3 + offset + static_cast<u32>(i);
+ }
+ for (size_t i = 0; i < to_display.size(); ++i)
+ {
+ u32 index = to_display[i];
+ QLabel* a_rank = new QLabel(QStringLiteral("---"));
+ QLabel* a_username = new QLabel(QStringLiteral("---"));
+ QLabel* a_score = new QLabel(QStringLiteral("---"));
+ const auto it = board.entries.find(index);
+ if (it != board.entries.end())
+ {
+ a_rank->setText(tr("Rank %1").arg(it->second.rank));
+ a_username->setText(QString::fromStdString(it->second.username));
+ a_score->setText(QString::fromUtf8(it->second.score.data()));
+ }
+ QVBoxLayout* a_col = new QVBoxLayout();
+ a_col->addWidget(a_rank);
+ a_col->addWidget(a_username);
+ a_col->addWidget(a_score);
+ if (row > 0)
+ {
+ QFrame* a_divider = new QFrame();
+ a_divider->setFrameShape(QFrame::HLine);
+ m_common_layout->addWidget(a_divider, row - 1, static_cast<int>(i) + 1);
+ }
+ m_common_layout->addLayout(a_col, row, static_cast<int>(i) + 1);
+ }
+ row += 2;
+ }
+}
+
+#endif // USE_RETRO_ACHIEVEMENTS
diff --git a/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.h b/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.h
new file mode 100644
index 0000000000..055ea6ab3f
--- /dev/null
+++ b/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.h
@@ -0,0 +1,24 @@
+// Copyright 2023 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#ifdef USE_RETRO_ACHIEVEMENTS
+#include <QWidget>
+
+class QGroupBox;
+class QGridLayout;
+
+class AchievementLeaderboardWidget final : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit AchievementLeaderboardWidget(QWidget* parent);
+ void UpdateData();
+
+private:
+ QGroupBox* m_common_box;
+ QGridLayout* m_common_layout;
+};
+
+#endif // USE_RETRO_ACHIEVEMENTS
diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp
index d376da6109..8d01663c87 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp
+++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp
@@ -11,6 +11,7 @@
#include <QVBoxLayout>
#include "DolphinQt/Achievements/AchievementHeaderWidget.h"
+#include "DolphinQt/Achievements/AchievementLeaderboardWidget.h"
#include "DolphinQt/Achievements/AchievementProgressWidget.h"
#include "DolphinQt/Achievements/AchievementSettingsWidget.h"
#include "DolphinQt/QtUtils/QueueOnObject.h"
@@ -42,10 +43,14 @@ void AchievementsWindow::CreateMainLayout()
m_header_widget = new AchievementHeaderWidget(this);
m_tab_widget = new QTabWidget();
m_progress_widget = new AchievementProgressWidget(m_tab_widget);
+ m_leaderboard_widget = new AchievementLeaderboardWidget(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_tab_widget->addTab(GetWrappedWidget(m_leaderboard_widget, this, 125, 100), tr("Leaderboards"));
+ m_tab_widget->setTabVisible(2, AchievementManager::GetInstance()->IsGameLoaded());
m_button_box = new QDialogButtonBox(QDialogButtonBox::Close);
@@ -70,6 +75,8 @@ void AchievementsWindow::UpdateData()
// Settings tab handles its own updates ... indeed, that calls this
m_progress_widget->UpdateData();
m_tab_widget->setTabVisible(1, AchievementManager::GetInstance()->IsGameLoaded());
+ m_leaderboard_widget->UpdateData();
+ m_tab_widget->setTabVisible(2, AchievementManager::GetInstance()->IsGameLoaded());
}
update();
}
diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h
index d8407a3a17..6fd7165e1f 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h
+++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h
@@ -10,6 +10,7 @@
#include "DolphinQt/QtUtils/QueueOnObject.h"
class AchievementHeaderWidget;
+class AchievementLeaderboardWidget;
class AchievementProgressWidget;
class QDialogButtonBox;
class QTabWidget;
@@ -30,6 +31,7 @@ private:
AchievementHeaderWidget* m_header_widget;
QTabWidget* m_tab_widget;
AchievementProgressWidget* m_progress_widget;
+ AchievementLeaderboardWidget* m_leaderboard_widget;
QDialogButtonBox* m_button_box;
};
diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt
index 87fc14c766..b271fab7fb 100644
--- a/Source/Core/DolphinQt/CMakeLists.txt
+++ b/Source/Core/DolphinQt/CMakeLists.txt
@@ -30,6 +30,8 @@ add_executable(dolphin-emu
CheatsManager.h
Achievements/AchievementHeaderWidget.cpp
Achievements/AchievementHeaderWidget.h
+ Achievements/AchievementLeaderboardWidget.cpp
+ Achievements/AchievementLeaderboardWidget.h
Achievements/AchievementProgressWidget.cpp
Achievements/AchievementProgressWidget.h
Achievements/AchievementSettingsWidget.cpp
diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj
index f73ea5c1cc..35f1110e4c 100644
--- a/Source/Core/DolphinQt/DolphinQt.vcxproj
+++ b/Source/Core/DolphinQt/DolphinQt.vcxproj
@@ -51,6 +51,7 @@
<ClCompile Include="CheatSearchWidget.cpp" />
<ClCompile Include="CheatsManager.cpp" />
<ClCompile Include="Achievements\AchievementHeaderWidget.cpp" />
+ <ClCompile Include="Achievements\AchievementLeaderboardWidget.cpp" />
<ClCompile Include="Achievements\AchievementProgressWidget.cpp" />
<ClCompile Include="Achievements\AchievementSettingsWidget.cpp" />
<ClCompile Include="Achievements\AchievementsWindow.cpp" />
@@ -261,6 +262,7 @@
<QtMoc Include="CheatSearchWidget.h" />
<QtMoc Include="CheatsManager.h" />
<QtMoc Include="Achievements\AchievementHeaderWidget.h" />
+ <QtMoc Include="Achievements\AchievementLeaderboardWidget.h" />
<QtMoc Include="Achievements\AchievementProgressWidget.h" />
<QtMoc Include="Achievements\AchievementSettingsWidget.h" />
<QtMoc Include="Achievements\AchievementsWindow.h" />