dolphin/Source/Core/Core/AchievementManager.cpp
LillyJadeKatrin 486a9d2318 Refactored Achievement Mananger to load games through rc_client
HashGame has become LoadGame, similar structure with the file loaders but using the client instead. LoadGameCallback has been created to handle the results. The old LoadGameSync has been deleted as have
several hash and load methods that it called.
2024-05-01 06:43:19 -04:00

1397 lines
49 KiB
C++

// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef USE_RETRO_ACHIEVEMENTS
#include "Core/AchievementManager.h"
#include <cctype>
#include <memory>
#include <fmt/format.h>
#include <rcheevos/include/rc_api_info.h>
#include <rcheevos/include/rc_hash.h>
#include "Common/Image.h"
#include "Common/Logging/Log.h"
#include "Common/WorkQueueThread.h"
#include "Core/Config/AchievementSettings.h"
#include "Core/Core.h"
#include "Core/PowerPC/MMU.h"
#include "Core/System.h"
#include "DiscIO/Blob.h"
#include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VideoEvents.h"
static std::unique_ptr<OSD::Icon> DecodeBadgeToOSDIcon(const AchievementManager::Badge& badge);
AchievementManager& AchievementManager::GetInstance()
{
static AchievementManager s_instance;
return s_instance;
}
void AchievementManager::Init()
{
if (!m_client && Config::Get(Config::RA_ENABLED))
{
m_client = rc_client_create(MemoryPeekerV2, RequestV2);
std::string host_url = Config::Get(Config::RA_HOST_URL);
if (!host_url.empty())
rc_client_set_host(m_client, host_url.c_str());
rc_client_enable_logging(m_client, RC_CLIENT_LOG_LEVEL_VERBOSE,
[](const char* message, const rc_client_t* client) {
INFO_LOG_FMT(ACHIEVEMENTS, "{}", message);
});
rc_client_set_hardcore_enabled(m_client, 0);
rc_client_set_unofficial_enabled(m_client, 1);
m_queue.Reset("AchievementManagerQueue", [](const std::function<void()>& func) { func(); });
m_image_queue.Reset("AchievementManagerImageQueue",
[](const std::function<void()>& func) { func(); });
if (HasAPIToken())
Login("");
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager Initialized");
}
}
void AchievementManager::SetUpdateCallback(UpdateCallback callback)
{
m_update_callback = std::move(callback);
if (!m_update_callback)
m_update_callback = [] {};
m_update_callback();
}
void AchievementManager::Login(const std::string& password)
{
if (!m_client)
{
ERROR_LOG_FMT(
ACHIEVEMENTS,
"Attempted login to RetroAchievements server without achievement client initialized.");
return;
}
if (password.empty())
{
rc_client_begin_login_with_token(m_client, Config::Get(Config::RA_USERNAME).c_str(),
Config::Get(Config::RA_API_TOKEN).c_str(), LoginCallback,
nullptr);
}
else
{
rc_client_begin_login_with_password(m_client, Config::Get(Config::RA_USERNAME).c_str(),
password.c_str(), LoginCallback, nullptr);
}
}
bool AchievementManager::HasAPIToken() const
{
return !Config::Get(Config::RA_API_TOKEN).empty();
}
void AchievementManager::LoadGame(const std::string& file_path, const DiscIO::Volume* volume)
{
if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken())
{
return;
}
if (file_path.empty() && volume == nullptr)
{
WARN_LOG_FMT(ACHIEVEMENTS, "Called Load Game without a game.");
return;
}
if (!m_client)
{
ERROR_LOG_FMT(ACHIEVEMENTS,
"Attempted to load game achievements without achievement client initialized.");
return;
}
if (m_disabled)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager is disabled until core is rebooted.");
OSD::AddMessage("Achievements are disabled until you restart emulation.",
OSD::Duration::VERY_LONG, OSD::Color::RED);
return;
}
if (volume)
{
std::lock_guard lg{m_lock};
if (!m_loading_volume)
{
m_loading_volume = DiscIO::CreateVolume(volume->GetBlobReader().CopyReader());
}
}
std::lock_guard lg{m_filereader_lock};
rc_hash_filereader volume_reader{
.open = (volume) ? &AchievementManager::FilereaderOpenByVolume :
&AchievementManager::FilereaderOpenByFilepath,
.seek = &AchievementManager::FilereaderSeek,
.tell = &AchievementManager::FilereaderTell,
.read = &AchievementManager::FilereaderRead,
.close = &AchievementManager::FilereaderClose,
};
rc_hash_init_custom_filereader(&volume_reader);
rc_client_begin_identify_and_load_game(m_client, RC_CONSOLE_GAMECUBE, file_path.c_str(), NULL, 0,
LoadGameCallback, NULL);
}
bool AchievementManager::IsGameLoaded() const
{
auto* game_info = rc_client_get_game_info(m_client);
return game_info && game_info->id != 0;
}
void AchievementManager::FetchBadges()
{
if (!m_is_runtime_initialized || !HasAPIToken() || !Config::Get(Config::RA_BADGES_ENABLED))
{
m_update_callback();
return;
}
m_image_queue.Cancel();
auto* user = rc_client_get_user_info(m_client);
if (m_player_badge.name.compare(user->display_name) != 0)
{
m_image_queue.EmplaceItem([this, user] {
std::string name_to_fetch;
{
std::lock_guard lg{m_lock};
if (m_player_badge.name.compare(user->display_name) == 0)
return;
name_to_fetch.assign(user->display_name);
}
rc_api_fetch_image_request_t icon_request = {.image_name = name_to_fetch.c_str(),
.image_type = RC_IMAGE_TYPE_USER};
Badge fetched_badge;
if (RequestImage(icon_request, &fetched_badge) == ResponseType::SUCCESS)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded player badge id {}.", name_to_fetch);
std::lock_guard lg{m_lock};
if (name_to_fetch.compare(user->display_name) != 0)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Requested outdated badge id {} for player id {}.",
name_to_fetch, user->display_name);
return;
}
m_player_badge.badge = std::move(fetched_badge);
m_player_badge.name = std::move(name_to_fetch);
}
else
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to download player badge id {}.", name_to_fetch);
}
m_update_callback();
});
}
if (!IsGameLoaded())
{
m_update_callback();
return;
}
bool badgematch = false;
{
std::lock_guard lg{m_lock};
badgematch = m_game_badge.name == m_game_data.image_name;
}
if (!badgematch)
{
m_image_queue.EmplaceItem([this] {
std::string name_to_fetch;
{
std::lock_guard lg{m_lock};
if (m_game_badge.name == m_game_data.image_name)
return;
name_to_fetch = m_game_data.image_name;
}
rc_api_fetch_image_request_t icon_request = {.image_name = name_to_fetch.c_str(),
.image_type = RC_IMAGE_TYPE_GAME};
Badge fetched_badge;
if (RequestImage(icon_request, &fetched_badge) == ResponseType::SUCCESS)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded game badge id {}.", name_to_fetch);
std::lock_guard lg{m_lock};
if (name_to_fetch != m_game_data.image_name)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Requested outdated badge id {} for game id {}.",
name_to_fetch, m_game_data.image_name);
return;
}
m_game_badge.badge = std::move(fetched_badge);
m_game_badge.name = std::move(name_to_fetch);
}
else
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to download game badge id {}.", name_to_fetch);
}
m_update_callback();
});
}
unsigned num_achievements = m_game_data.num_achievements;
for (size_t index = 0; index < num_achievements; index++)
{
std::lock_guard lg{m_lock};
// In case the number of achievements changes since the loop started; I just don't want
// to lock for the ENTIRE loop so instead I reclaim the lock each cycle
if (num_achievements != m_game_data.num_achievements)
break;
const auto& initial_achievement = m_game_data.achievements[index];
const std::string badge_name_to_fetch(initial_achievement.badge_name);
const UnlockStatus& unlock_status = m_unlock_map[initial_achievement.id];
if (unlock_status.unlocked_badge.name != badge_name_to_fetch)
{
m_image_queue.EmplaceItem([this, index] {
std::string current_name, name_to_fetch;
{
std::lock_guard lock{m_lock};
if (m_game_data.num_achievements <= index)
{
INFO_LOG_FMT(
ACHIEVEMENTS,
"Attempted to fetch unlocked badge for index {} after achievement list cleared.",
index);
return;
}
const auto& achievement = m_game_data.achievements[index];
const auto unlock_itr = m_unlock_map.find(achievement.id);
if (unlock_itr == m_unlock_map.end())
{
ERROR_LOG_FMT(
ACHIEVEMENTS,
"Attempted to fetch unlocked badge for achievement id {} not in unlock map.",
index);
return;
}
name_to_fetch = achievement.badge_name;
current_name = unlock_itr->second.unlocked_badge.name;
}
if (current_name == name_to_fetch)
return;
rc_api_fetch_image_request_t icon_request = {.image_name = name_to_fetch.c_str(),
.image_type = RC_IMAGE_TYPE_ACHIEVEMENT};
Badge fetched_badge;
if (RequestImage(icon_request, &fetched_badge) == ResponseType::SUCCESS)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded unlocked achievement badge id {}.",
name_to_fetch);
std::lock_guard lock{m_lock};
if (m_game_data.num_achievements <= index)
{
INFO_LOG_FMT(ACHIEVEMENTS,
"Fetched unlocked badge for index {} after achievement list cleared.",
index);
return;
}
const auto& achievement = m_game_data.achievements[index];
const auto unlock_itr = m_unlock_map.find(achievement.id);
if (unlock_itr == m_unlock_map.end())
{
ERROR_LOG_FMT(ACHIEVEMENTS,
"Fetched unlocked badge for achievement id {} not in unlock map.", index);
return;
}
if (name_to_fetch != achievement.badge_name)
{
INFO_LOG_FMT(
ACHIEVEMENTS,
"Requested outdated unlocked achievement badge id {} for achievement id {}.",
name_to_fetch, current_name);
return;
}
unlock_itr->second.unlocked_badge.badge = std::move(fetched_badge);
unlock_itr->second.unlocked_badge.name = std::move(name_to_fetch);
}
else
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to download unlocked achievement badge id {}.",
name_to_fetch);
}
m_update_callback();
});
}
if (unlock_status.locked_badge.name != badge_name_to_fetch)
{
m_image_queue.EmplaceItem([this, index] {
std::string current_name, name_to_fetch;
{
std::lock_guard lock{m_lock};
if (m_game_data.num_achievements <= index)
{
INFO_LOG_FMT(
ACHIEVEMENTS,
"Attempted to fetch locked badge for index {} after achievement list cleared.",
index);
return;
}
const auto& achievement = m_game_data.achievements[index];
const auto unlock_itr = m_unlock_map.find(achievement.id);
if (unlock_itr == m_unlock_map.end())
{
ERROR_LOG_FMT(
ACHIEVEMENTS,
"Attempted to fetch locked badge for achievement id {} not in unlock map.", index);
return;
}
name_to_fetch = achievement.badge_name;
current_name = unlock_itr->second.locked_badge.name;
}
if (current_name == name_to_fetch)
return;
rc_api_fetch_image_request_t icon_request = {
.image_name = name_to_fetch.c_str(), .image_type = RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED};
Badge fetched_badge;
if (RequestImage(icon_request, &fetched_badge) == ResponseType::SUCCESS)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded locked achievement badge id {}.",
name_to_fetch);
std::lock_guard lock{m_lock};
if (m_game_data.num_achievements <= index)
{
INFO_LOG_FMT(ACHIEVEMENTS,
"Fetched locked badge for index {} after achievement list cleared.",
index);
return;
}
const auto& achievement = m_game_data.achievements[index];
const auto unlock_itr = m_unlock_map.find(achievement.id);
if (unlock_itr == m_unlock_map.end())
{
ERROR_LOG_FMT(ACHIEVEMENTS,
"Fetched locked badge for achievement id {} not in unlock map.", index);
return;
}
if (name_to_fetch != achievement.badge_name)
{
INFO_LOG_FMT(ACHIEVEMENTS,
"Requested outdated locked achievement badge id {} for achievement id {}.",
name_to_fetch, current_name);
return;
}
unlock_itr->second.locked_badge.badge = std::move(fetched_badge);
unlock_itr->second.locked_badge.name = std::move(name_to_fetch);
}
else
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to download locked achievement badge id {}.",
name_to_fetch);
}
m_update_callback();
});
}
}
m_update_callback();
}
void AchievementManager::DoFrame()
{
if (!m_is_game_loaded || !Core::IsCPUThread())
return;
if (m_framecount == 0x200)
{
DisplayWelcomeMessage();
}
if (m_framecount <= 0x200)
{
m_framecount++;
}
{
std::lock_guard lg{m_lock};
rc_runtime_do_frame(
&m_runtime,
[](const rc_runtime_event_t* runtime_event) {
GetInstance().AchievementEventHandler(runtime_event);
},
[](unsigned address, unsigned num_bytes, void* ud) {
return static_cast<AchievementManager*>(ud)->MemoryPeeker(address, num_bytes, ud);
},
this, nullptr);
}
if (!m_system)
return;
time_t current_time = std::time(nullptr);
if (difftime(current_time, m_last_ping_time) > 120)
{
GenerateRichPresence(Core::CPUThreadGuard{*m_system});
m_queue.EmplaceItem([this] { PingRichPresence(m_rich_presence); });
m_last_ping_time = current_time;
m_update_callback();
}
}
u32 AchievementManager::MemoryPeeker(u32 address, u32 num_bytes, void* ud)
{
if (!m_system)
return 0u;
Core::CPUThreadGuard threadguard(*m_system);
switch (num_bytes)
{
case 1:
return m_system->GetMMU()
.HostTryReadU8(threadguard, address, PowerPC::RequestedAddressSpace::Physical)
.value_or(PowerPC::ReadResult<u8>(false, 0u))
.value;
case 2:
return Common::swap16(
m_system->GetMMU()
.HostTryReadU16(threadguard, address, PowerPC::RequestedAddressSpace::Physical)
.value_or(PowerPC::ReadResult<u16>(false, 0u))
.value);
case 4:
return Common::swap32(
m_system->GetMMU()
.HostTryReadU32(threadguard, address, PowerPC::RequestedAddressSpace::Physical)
.value_or(PowerPC::ReadResult<u32>(false, 0u))
.value);
default:
ASSERT(false);
return 0u;
}
}
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_ACHIEVEMENT_PROGRESS_UPDATED:
HandleAchievementProgressUpdatedEvent(runtime_event);
break;
case RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED:
HandleAchievementPrimedEvent(runtime_event);
break;
case RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED:
HandleAchievementUnprimedEvent(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::recursive_mutex& AchievementManager::GetLock()
{
return m_lock;
}
bool AchievementManager::IsHardcoreModeActive() const
{
std::lock_guard lg{m_lock};
if (!Config::Get(Config::RA_HARDCORE_ENABLED))
return false;
if (!Core::IsRunning())
return true;
if (!IsGameLoaded())
return false;
return (m_runtime.trigger_count + m_runtime.lboard_count > 0);
}
std::string_view AchievementManager::GetPlayerDisplayName() const
{
if (!HasAPIToken())
return "";
auto* user = rc_client_get_user_info(m_client);
if (!user)
return "";
return std::string_view(user->display_name);
}
u32 AchievementManager::GetPlayerScore() const
{
if (!HasAPIToken())
return 0;
auto* user = rc_client_get_user_info(m_client);
if (!user)
return 0;
return user->score;
}
const AchievementManager::BadgeStatus& AchievementManager::GetPlayerBadge() const
{
return m_player_badge;
}
std::string AchievementManager::GetGameDisplayName() const
{
return IsGameLoaded() ? m_game_data.title : "";
}
AchievementManager::PointSpread AchievementManager::TallyScore() const
{
PointSpread spread{};
if (!IsGameLoaded())
return spread;
bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED);
for (const auto& entry : m_unlock_map)
{
if (entry.second.category != RC_ACHIEVEMENT_CATEGORY_CORE)
continue;
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;
}
const AchievementManager::BadgeStatus& AchievementManager::GetGameBadge() const
{
return m_game_badge;
}
const AchievementManager::UnlockStatus&
AchievementManager::GetUnlockStatus(AchievementId achievement_id) const
{
return m_unlock_map.at(achievement_id);
}
AchievementManager::ResponseType
AchievementManager::GetAchievementProgress(AchievementId achievement_id, u32* value, u32* target)
{
if (!IsGameLoaded())
{
ERROR_LOG_FMT(
ACHIEVEMENTS,
"Attempted to request measured data for achievement ID {} when no game is running.",
achievement_id);
return ResponseType::INVALID_REQUEST;
}
int result = rc_runtime_get_achievement_measured(&m_runtime, achievement_id, value, target);
if (result == 0)
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to get measured data for achievement ID {}.",
achievement_id);
return ResponseType::MALFORMED_OBJECT;
}
return ResponseType::SUCCESS;
}
const std::unordered_map<AchievementManager::AchievementId, AchievementManager::LeaderboardStatus>&
AchievementManager::GetLeaderboardsInfo() const
{
return m_leaderboard_map;
}
AchievementManager::RichPresence AchievementManager::GetRichPresence() const
{
std::lock_guard lg{m_lock};
return m_rich_presence;
}
void AchievementManager::SetDisabled(bool disable)
{
bool previously_disabled;
{
std::lock_guard lg{m_lock};
previously_disabled = m_disabled;
m_disabled = disable;
if (disable && m_is_game_loaded)
CloseGame();
}
if (!previously_disabled && disable && Config::Get(Config::RA_ENABLED))
{
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager has been disabled.");
OSD::AddMessage("Please close all games to re-enable achievements.", OSD::Duration::VERY_LONG,
OSD::Color::RED);
m_update_callback();
}
if (previously_disabled && !disable)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager has been re-enabled.");
m_update_callback();
}
};
const AchievementManager::NamedIconMap& AchievementManager::GetChallengeIcons() const
{
return m_active_challenges;
}
void AchievementManager::CloseGame()
{
{
std::lock_guard lg{m_lock};
if (rc_client_get_game_info(m_client))
{
m_active_challenges.clear();
m_game_badge.name.clear();
m_unlock_map.clear();
m_leaderboard_map.clear();
rc_api_destroy_fetch_game_data_response(&m_game_data);
m_game_data = {};
m_queue.Cancel();
m_image_queue.Cancel();
rc_client_unload_game(m_client);
m_system = nullptr;
}
}
m_update_callback();
INFO_LOG_FMT(ACHIEVEMENTS, "Game closed.");
}
void AchievementManager::Logout()
{
{
std::lock_guard lg{m_lock};
CloseGame();
SetDisabled(false);
m_player_badge.name.clear();
Config::SetBaseOrCurrent(Config::RA_API_TOKEN, "");
}
m_update_callback();
INFO_LOG_FMT(ACHIEVEMENTS, "Logged out from server.");
}
void AchievementManager::Shutdown()
{
if (m_client)
{
CloseGame();
SetDisabled(false);
m_queue.Shutdown();
// DON'T log out - keep those credentials for next run.
rc_client_destroy(m_client);
m_client = nullptr;
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager shut down.");
}
}
void* AchievementManager::FilereaderOpenByFilepath(const char* path_utf8)
{
auto state = std::make_unique<FilereaderState>();
state->volume = DiscIO::CreateVolume(path_utf8);
if (!state->volume)
return nullptr;
return state.release();
}
void* AchievementManager::FilereaderOpenByVolume(const char* path_utf8)
{
auto state = std::make_unique<FilereaderState>();
{
auto& instance = GetInstance();
std::lock_guard lg{instance.GetLock()};
state->volume = std::move(instance.GetLoadingVolume());
}
if (!state->volume)
return nullptr;
return state.release();
}
void AchievementManager::FilereaderSeek(void* file_handle, int64_t offset, int origin)
{
switch (origin)
{
case SEEK_SET:
static_cast<FilereaderState*>(file_handle)->position = offset;
break;
case SEEK_CUR:
static_cast<FilereaderState*>(file_handle)->position += offset;
break;
case SEEK_END:
// Unused
break;
}
}
int64_t AchievementManager::FilereaderTell(void* file_handle)
{
return static_cast<FilereaderState*>(file_handle)->position;
}
size_t AchievementManager::FilereaderRead(void* file_handle, void* buffer, size_t requested_bytes)
{
FilereaderState* filereader_state = static_cast<FilereaderState*>(file_handle);
bool success = (filereader_state->volume->Read(filereader_state->position, requested_bytes,
static_cast<u8*>(buffer), DiscIO::PARTITION_NONE));
if (success)
{
filereader_state->position += requested_bytes;
return requested_bytes;
}
else
{
return 0;
}
}
void AchievementManager::FilereaderClose(void* file_handle)
{
delete static_cast<FilereaderState*>(file_handle);
}
void AchievementManager::LoginCallback(int result, const char* error_message, rc_client_t* client,
void* userdata)
{
if (result != RC_OK)
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to login {} to RetroAchievements server.",
Config::Get(Config::RA_USERNAME));
return;
}
const rc_client_user_t* user;
{
std::lock_guard lg{AchievementManager::GetInstance().GetLock()};
user = rc_client_get_user_info(client);
}
if (!user)
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to retrieve user information from client.");
return;
}
std::string config_username = Config::Get(Config::RA_USERNAME);
if (config_username != user->username)
{
if (Common::CaseInsensitiveEquals(config_username, user->username))
{
INFO_LOG_FMT(ACHIEVEMENTS,
"Case mismatch between site {} and local {}; updating local config.",
user->username, Config::Get(Config::RA_USERNAME));
Config::SetBaseOrCurrent(Config::RA_USERNAME, user->username);
}
else
{
INFO_LOG_FMT(ACHIEVEMENTS, "Attempted to login prior user {}; current user is {}.",
user->username, Config::Get(Config::RA_USERNAME));
rc_client_logout(client);
return;
}
}
INFO_LOG_FMT(ACHIEVEMENTS, "Successfully logged in {} to RetroAchievements server.",
user->username);
std::lock_guard lg{AchievementManager::GetInstance().GetLock()};
Config::SetBaseOrCurrent(Config::RA_API_TOKEN, user->token);
}
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];
auto 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.insert_or_assign(org_entry.index, std::move(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];
auto 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.insert_or_assign(org_entry.index, std::move(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.insert_or_assign(leaderboard_id, std::move(lboard));
}
return ResponseType::SUCCESS;
}
void AchievementManager::ActivateDeactivateAchievement(AchievementId id, bool enabled,
bool unofficial, bool encore)
{
auto it = m_unlock_map.find(id);
if (it == m_unlock_map.end())
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Attempted to unlock unknown achievement id {}.", id);
return;
}
const UnlockStatus& status = it->second;
u32 index = status.game_data_index;
bool active = (rc_runtime_get_achievement(&m_runtime, id) != nullptr);
bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED);
// Deactivate achievements if game is not loaded
bool activate = m_is_game_loaded;
// Activate achievements only if achievements are enabled
if (activate && !enabled)
activate = false;
// Deactivate if achievement is unofficial, unless unofficial achievements are enabled
if (activate && !unofficial &&
m_game_data.achievements[index].category == RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL)
{
activate = false;
}
// If encore mode is on, activate/deactivate regardless of current unlock status
if (activate && !encore)
{
// Encore is off, achievement has been unlocked in this session, deactivate
activate = (status.session_unlock_count == 0);
// Encore is off, achievement has been hardcore unlocked on site, deactivate
if (activate && status.remote_unlock_status == UnlockStatus::UnlockType::HARDCORE)
activate = false;
// Encore is off, hardcore is off, achievement has been softcore unlocked on site, deactivate
if (activate && !hardcore_mode_enabled &&
status.remote_unlock_status == UnlockStatus::UnlockType::SOFTCORE)
{
activate = false;
}
}
if (!active && activate)
{
rc_runtime_activate_achievement(&m_runtime, id, m_game_data.achievements[index].definition,
nullptr, 0);
}
if (active && !activate)
rc_runtime_deactivate_achievement(&m_runtime, id);
}
void AchievementManager::GenerateRichPresence(const Core::CPUThreadGuard& guard)
{
std::lock_guard lg{m_lock};
rc_runtime_get_richpresence(
&m_runtime, m_rich_presence.data(), RP_SIZE,
[](unsigned address, unsigned num_bytes, void* ud) {
return static_cast<AchievementManager*>(ud)->MemoryPeeker(address, num_bytes, ud);
},
this, nullptr);
}
AchievementManager::ResponseType AchievementManager::AwardAchievement(AchievementId achievement_id)
{
std::string username = Config::Get(Config::RA_USERNAME);
std::string api_token = Config::Get(Config::RA_API_TOKEN);
bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED);
rc_api_award_achievement_request_t award_request = {.username = username.c_str(),
.api_token = api_token.c_str(),
.achievement_id = achievement_id,
.hardcore = hardcore_mode_enabled,
.game_hash = m_game_hash.data()};
rc_api_award_achievement_response_t award_response = {};
ResponseType r_type =
Request<rc_api_award_achievement_request_t, rc_api_award_achievement_response_t>(
award_request, &award_response, rc_api_init_award_achievement_request,
rc_api_process_award_achievement_response);
rc_api_destroy_award_achievement_response(&award_response);
if (r_type == ResponseType::SUCCESS)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Awarded achievement ID {}.", achievement_id);
}
else
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to award achievement ID {}.", achievement_id);
}
return r_type;
}
AchievementManager::ResponseType AchievementManager::SubmitLeaderboard(AchievementId leaderboard_id,
int value)
{
std::string username = Config::Get(Config::RA_USERNAME);
std::string api_token = Config::Get(Config::RA_API_TOKEN);
rc_api_submit_lboard_entry_request_t submit_request = {.username = username.c_str(),
.api_token = api_token.c_str(),
.leaderboard_id = leaderboard_id,
.score = value,
.game_hash = m_game_hash.data()};
rc_api_submit_lboard_entry_response_t submit_response = {};
ResponseType r_type =
Request<rc_api_submit_lboard_entry_request_t, rc_api_submit_lboard_entry_response_t>(
submit_request, &submit_response, rc_api_init_submit_lboard_entry_request,
rc_api_process_submit_lboard_entry_response);
rc_api_destroy_submit_lboard_entry_response(&submit_response);
if (r_type == ResponseType::SUCCESS)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Submitted leaderboard ID {}.", leaderboard_id);
}
else
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to submit leaderboard ID {}.", leaderboard_id);
}
return r_type;
}
AchievementManager::ResponseType
AchievementManager::PingRichPresence(const RichPresence& rich_presence)
{
std::string username = Config::Get(Config::RA_USERNAME);
std::string api_token = Config::Get(Config::RA_API_TOKEN);
rc_api_ping_request_t ping_request = {.username = username.c_str(),
.api_token = api_token.c_str(),
.game_id = m_game_id,
.rich_presence = rich_presence.data()};
rc_api_ping_response_t ping_response = {};
ResponseType r_type = Request<rc_api_ping_request_t, rc_api_ping_response_t>(
ping_request, &ping_response, rc_api_init_ping_request, rc_api_process_ping_response);
rc_api_destroy_ping_response(&ping_response);
return r_type;
}
void AchievementManager::LoadGameCallback(int result, const char* error_message,
rc_client_t* client, void* userdata)
{
if (result != RC_OK)
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to load data for current game.");
return;
}
auto* game = rc_client_get_game_info(client);
if (!game)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to retrieve game information from client.");
return;
}
INFO_LOG_FMT(ACHIEVEMENTS, "Loaded data for game ID {}.", game->id);
AchievementManager::GetInstance().FetchBadges();
AchievementManager::GetInstance().m_system = &Core::System::GetInstance();
}
void AchievementManager::DisplayWelcomeMessage()
{
std::lock_guard lg{m_lock};
PointSpread spread = TallyScore();
if (Config::Get(Config::RA_HARDCORE_ENABLED))
{
OSD::AddMessage(
fmt::format("You have {}/{} achievements worth {}/{} points", spread.hard_unlocks,
spread.total_count, spread.hard_points, spread.total_points),
OSD::Duration::VERY_LONG, OSD::Color::YELLOW,
(Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) :
nullptr);
OSD::AddMessage("Hardcore mode is ON", OSD::Duration::VERY_LONG, OSD::Color::YELLOW);
}
else
{
OSD::AddMessage(fmt::format("You have {}/{} achievements worth {}/{} points",
spread.hard_unlocks + spread.soft_unlocks, spread.total_count,
spread.hard_points + spread.soft_points, spread.total_points),
OSD::Duration::VERY_LONG, OSD::Color::CYAN,
(Config::Get(Config::RA_BADGES_ENABLED)) ?
DecodeBadgeToOSDIcon(m_game_badge.badge) :
nullptr);
OSD::AddMessage("Hardcore mode is OFF", OSD::Duration::VERY_LONG, OSD::Color::CYAN);
}
}
void AchievementManager::HandleAchievementTriggeredEvent(const rc_runtime_event_t* runtime_event)
{
bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED);
const auto event_id = runtime_event->id;
auto it = m_unlock_map.find(event_id);
if (it == m_unlock_map.end())
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid achievement triggered event with id {}.", event_id);
return;
}
it->second.session_unlock_count++;
AchievementId game_data_index = it->second.game_data_index;
OSD::AddMessage(fmt::format("Unlocked: {} ({})", m_game_data.achievements[game_data_index].title,
m_game_data.achievements[game_data_index].points),
OSD::Duration::VERY_LONG,
(hardcore_mode_enabled) ? OSD::Color::YELLOW : OSD::Color::CYAN,
(Config::Get(Config::RA_BADGES_ENABLED)) ?
DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge) :
nullptr);
if (m_game_data.achievements[game_data_index].category == RC_ACHIEVEMENT_CATEGORY_CORE)
{
auto* user = rc_client_get_user_info(m_client);
m_queue.EmplaceItem([this, event_id] { AwardAchievement(event_id); });
PointSpread spread = TallyScore();
if (spread.hard_points == spread.total_points &&
it->second.remote_unlock_status != UnlockStatus::UnlockType::HARDCORE)
{
OSD::AddMessage(
fmt::format("Congratulations! {} has mastered {}", user->display_name, m_game_data.title),
OSD::Duration::VERY_LONG, OSD::Color::YELLOW,
(Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) :
nullptr);
}
else if (spread.hard_points + spread.soft_points == spread.total_points &&
it->second.remote_unlock_status == UnlockStatus::UnlockType::LOCKED)
{
OSD::AddMessage(fmt::format("Congratulations! {} has completed {}", user->display_name,
m_game_data.title),
OSD::Duration::VERY_LONG, OSD::Color::CYAN,
(Config::Get(Config::RA_BADGES_ENABLED)) ?
DecodeBadgeToOSDIcon(m_game_badge.badge) :
nullptr);
}
}
ActivateDeactivateAchievement(event_id, Config::Get(Config::RA_ACHIEVEMENTS_ENABLED),
Config::Get(Config::RA_UNOFFICIAL_ENABLED),
Config::Get(Config::RA_ENCORE_ENABLED));
}
void AchievementManager::HandleAchievementProgressUpdatedEvent(
const rc_runtime_event_t* runtime_event)
{
if (!Config::Get(Config::RA_PROGRESS_ENABLED))
return;
auto it = m_unlock_map.find(runtime_event->id);
if (it == m_unlock_map.end())
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid achievement progress updated event with id {}.",
runtime_event->id);
return;
}
AchievementId game_data_index = it->second.game_data_index;
FormattedValue value{};
if (rc_runtime_format_achievement_measured(&m_runtime, runtime_event->id, value.data(),
FORMAT_SIZE) == 0)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to format measured data {}.", value.data());
return;
}
OSD::AddMessage(
fmt::format("{} {}", m_game_data.achievements[game_data_index].title, value.data()),
OSD::Duration::SHORT, OSD::Color::GREEN,
(Config::Get(Config::RA_BADGES_ENABLED)) ?
DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge) :
nullptr);
}
void AchievementManager::HandleAchievementPrimedEvent(const rc_runtime_event_t* runtime_event)
{
if (!Config::Get(Config::RA_BADGES_ENABLED))
return;
auto it = m_unlock_map.find(runtime_event->id);
if (it == m_unlock_map.end())
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid achievement primed event with id {}.", runtime_event->id);
return;
}
m_active_challenges[it->second.unlocked_badge.name] =
DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge);
}
void AchievementManager::HandleAchievementUnprimedEvent(const rc_runtime_event_t* runtime_event)
{
if (!Config::Get(Config::RA_BADGES_ENABLED))
return;
auto it = m_unlock_map.find(runtime_event->id);
if (it == m_unlock_map.end())
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid achievement unprimed event with id {}.",
runtime_event->id);
return;
}
m_active_challenges.erase(it->second.unlocked_badge.name);
}
void AchievementManager::HandleLeaderboardStartedEvent(const rc_runtime_event_t* runtime_event)
{
for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++)
{
if (m_game_data.leaderboards[ix].id == runtime_event->id)
{
OSD::AddMessage(fmt::format("Attempting leaderboard: {}", m_game_data.leaderboards[ix].title),
OSD::Duration::VERY_LONG, OSD::Color::GREEN);
return;
}
}
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid leaderboard started event with id {}.", runtime_event->id);
}
void AchievementManager::HandleLeaderboardCanceledEvent(const rc_runtime_event_t* runtime_event)
{
for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++)
{
if (m_game_data.leaderboards[ix].id == runtime_event->id)
{
OSD::AddMessage(fmt::format("Failed leaderboard: {}", m_game_data.leaderboards[ix].title),
OSD::Duration::VERY_LONG, OSD::Color::RED);
return;
}
}
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid leaderboard canceled event with id {}.", runtime_event->id);
}
void AchievementManager::HandleLeaderboardTriggeredEvent(const rc_runtime_event_t* runtime_event)
{
const auto event_id = runtime_event->id;
const auto event_value = runtime_event->value;
m_queue.EmplaceItem([this, event_id, event_value] { SubmitLeaderboard(event_id, event_value); });
for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++)
{
if (m_game_data.leaderboards[ix].id == event_id)
{
FormattedValue value{};
rc_runtime_format_lboard_value(value.data(), static_cast<int>(value.size()), event_value,
m_game_data.leaderboards[ix].format);
if (std::find(value.begin(), value.end(), '\0') == value.end())
{
OSD::AddMessage(fmt::format("Scored {} on leaderboard: {}",
std::string_view{value.data(), value.size()},
m_game_data.leaderboards[ix].title),
OSD::Duration::VERY_LONG, OSD::Color::YELLOW);
}
else
{
OSD::AddMessage(fmt::format("Scored {} on leaderboard: {}", value.data(),
m_game_data.leaderboards[ix].title),
OSD::Duration::VERY_LONG, OSD::Color::YELLOW);
}
m_queue.EmplaceItem([this, event_id] {
FetchBoardInfo(event_id);
m_update_callback();
});
break;
}
}
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid leaderboard triggered event with id {}.", event_id);
}
// 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
// Call rc_api_init_X_request to convert this into a generic rc_api_request_t struct
// Perform the HTTP request using the url and post_data in the rc_api_request_t struct
// Call rc_api_process_X_response to convert the raw string HTTP response into a
// rc_api_X_response_t struct
// Use the data in the rc_api_X_response_t struct as needed
// Call rc_api_destroy_X_response when finished with the response struct to free memory
template <typename RcRequest, typename RcResponse>
AchievementManager::ResponseType AchievementManager::Request(
RcRequest rc_request, RcResponse* rc_response,
const std::function<int(rc_api_request_t*, const RcRequest*)>& init_request,
const std::function<int(RcResponse*, const char*)>& process_response)
{
rc_api_request_t api_request;
Common::HttpRequest http_request;
if (init_request(&api_request, &rc_request) != RC_OK || !api_request.post_data)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid API request.");
return ResponseType::INVALID_REQUEST;
}
auto http_response = http_request.Post(api_request.url, api_request.post_data);
rc_api_destroy_request(&api_request);
if (http_response.has_value() && http_response->size() > 0)
{
const std::string response_str(http_response->begin(), http_response->end());
if (process_response(rc_response, response_str.c_str()) != RC_OK)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to process HTTP response. \nURL: {} \nresponse: {}",
api_request.url, response_str);
return ResponseType::MALFORMED_OBJECT;
}
if (rc_response->response.succeeded)
{
return ResponseType::SUCCESS;
}
else
{
Logout();
WARN_LOG_FMT(ACHIEVEMENTS, "Invalid RetroAchievements credentials; failed login.");
return ResponseType::INVALID_CREDENTIALS;
}
}
else
{
WARN_LOG_FMT(ACHIEVEMENTS, "RetroAchievements connection failed. \nURL: {}", api_request.url);
return ResponseType::CONNECTION_FAILED;
}
}
AchievementManager::ResponseType
AchievementManager::RequestImage(rc_api_fetch_image_request_t rc_request, Badge* rc_response)
{
rc_api_request_t api_request;
Common::HttpRequest http_request;
if (rc_api_init_fetch_image_request(&api_request, &rc_request) != RC_OK)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid request for image.");
return ResponseType::INVALID_REQUEST;
}
auto http_response = http_request.Get(api_request.url);
if (http_response.has_value() && http_response->size() > 0)
{
rc_api_destroy_request(&api_request);
*rc_response = std::move(*http_response);
return ResponseType::SUCCESS;
}
else
{
WARN_LOG_FMT(ACHIEVEMENTS, "RetroAchievements connection failed on image request.\n URL: {}",
api_request.url);
rc_api_destroy_request(&api_request);
return ResponseType::CONNECTION_FAILED;
}
}
static std::unique_ptr<OSD::Icon> DecodeBadgeToOSDIcon(const AchievementManager::Badge& badge)
{
if (badge.empty())
return nullptr;
auto icon = std::make_unique<OSD::Icon>();
if (!Common::LoadPNG(badge, &icon->rgba_data, &icon->width, &icon->height))
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Error decoding badge.");
return nullptr;
}
return icon;
}
void AchievementManager::RequestV2(const rc_api_request_t* request,
rc_client_server_callback_t callback, void* callback_data,
rc_client_t* client)
{
std::string url = request->url;
std::string post_data = request->post_data;
AchievementManager::GetInstance().m_queue.EmplaceItem([url = std::move(url),
post_data = std::move(post_data),
callback = std::move(callback),
callback_data = std::move(callback_data)] {
const Common::HttpRequest::Headers USER_AGENT_HEADER = {{"User-Agent", "Dolphin/Placeholder"}};
Common::HttpRequest http_request;
Common::HttpRequest::Response http_response;
if (!post_data.empty())
{
http_response = http_request.Post(url, post_data, USER_AGENT_HEADER,
Common::HttpRequest::AllowedReturnCodes::All);
}
else
{
http_response =
http_request.Get(url, USER_AGENT_HEADER, Common::HttpRequest::AllowedReturnCodes::All);
}
rc_api_server_response_t server_response;
if (http_response.has_value() && http_response->size() > 0)
{
server_response.body = reinterpret_cast<const char*>(http_response->data());
server_response.body_length = http_response->size();
server_response.http_status_code = http_request.GetLastResponseCode();
}
else
{
constexpr char error_message[] = "Failed HTTP request.";
server_response.body = error_message;
server_response.body_length = sizeof(error_message);
server_response.http_status_code = RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR;
}
callback(&server_response, callback_data);
});
}
u32 AchievementManager::MemoryPeekerV2(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client)
{
if (buffer == nullptr)
return 0u;
auto& system = Core::System::GetInstance();
Core::CPUThreadGuard threadguard(system);
for (u32 num_read = 0; num_read < num_bytes; num_read++)
{
auto value = system.GetMMU().HostTryReadU8(threadguard, address + num_read,
PowerPC::RequestedAddressSpace::Physical);
if (!value.has_value())
return num_read;
buffer[num_read] = value.value().value;
}
return num_bytes;
}
#endif // USE_RETRO_ACHIEVEMENTS