Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RetroAchievements - Leaderboards Tab #12027

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
107 changes: 105 additions & 2 deletions Source/Core/Core/AchievementManager.cpp
Expand Up @@ -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"
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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};
Expand All @@ -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();
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions Source/Core/Core/AchievementManager.h
Expand Up @@ -54,6 +54,7 @@ class AchievementManager
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>;
Expand Down Expand Up @@ -83,6 +84,21 @@ class AchievementManager
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);
Expand Down Expand Up @@ -113,6 +129,7 @@ class AchievementManager
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();
Expand All @@ -129,6 +146,7 @@ class AchievementManager
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();
Expand Down Expand Up @@ -165,6 +183,7 @@ class AchievementManager
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;
Expand Down
127 changes: 127 additions & 0 deletions 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
24 changes: 24 additions & 0 deletions 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
7 changes: 7 additions & 0 deletions Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp
Expand Up @@ -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"
Expand Down Expand Up @@ -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);

Expand All @@ -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();
}
Expand Down