Merge pull request #12027 from LillyJadeKatrin/retroachievements-leaderboards-tab
RetroAchievements - Leaderboards Tab
This commit is contained in:
commit
bbf3fed93c
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue