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.
This commit is contained in:
LillyJadeKatrin 2024-03-19 22:34:11 -04:00
parent 355b892621
commit 486a9d2318
5 changed files with 52 additions and 460 deletions

View File

@ -92,18 +92,21 @@ bool AchievementManager::HasAPIToken() const
return !Config::Get(Config::RA_API_TOKEN).empty();
}
void AchievementManager::HashGame(const std::string& file_path, const ResponseCallback& callback)
void AchievementManager::LoadGame(const std::string& file_path, const DiscIO::Volume* volume)
{
if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken())
{
callback(ResponseType::NOT_ENABLED);
return;
}
if (!m_is_runtime_initialized)
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 Manager initialized.");
callback(ResponseType::MANAGER_NOT_INITIALIZED);
"Attempted to load game achievements without achievement client initialized.");
return;
}
if (m_disabled)
@ -113,295 +116,32 @@ void AchievementManager::HashGame(const std::string& file_path, const ResponseCa
OSD::Duration::VERY_LONG, OSD::Color::RED);
return;
}
m_system = &Core::System::GetInstance();
m_queue.EmplaceItem([this, callback, file_path] {
Hash new_hash;
{
std::lock_guard lg{m_filereader_lock};
rc_hash_filereader volume_reader{
.open = &AchievementManager::FilereaderOpenByFilepath,
.seek = &AchievementManager::FilereaderSeek,
.tell = &AchievementManager::FilereaderTell,
.read = &AchievementManager::FilereaderRead,
.close = &AchievementManager::FilereaderClose,
};
rc_hash_init_custom_filereader(&volume_reader);
if (!rc_hash_generate_from_file(new_hash.data(), RC_CONSOLE_GAMECUBE, file_path.c_str()))
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Unable to generate achievement hash from game file {}.",
file_path);
callback(ResponseType::MALFORMED_OBJECT);
}
}
{
std::lock_guard lg{m_lock};
if (m_disabled)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Achievements disabled while hash was resolving.");
callback(ResponseType::EXPIRED_CONTEXT);
return;
}
m_game_hash = std::move(new_hash);
}
LoadGameSync(callback);
});
}
void AchievementManager::HashGame(const DiscIO::Volume* volume, const ResponseCallback& callback)
{
if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken())
{
callback(ResponseType::NOT_ENABLED);
return;
}
if (!m_is_runtime_initialized)
{
ERROR_LOG_FMT(ACHIEVEMENTS,
"Attempted to load game achievements without Achievement Manager initialized.");
callback(ResponseType::MANAGER_NOT_INITIALIZED);
return;
}
if (volume == nullptr)
{
INFO_LOG_FMT(ACHIEVEMENTS, "New volume is empty.");
return;
}
if (m_disabled)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager is disabled until core is rebooted.");
OSD::AddMessage("Achievements are disabled until core is rebooted.", OSD::Duration::VERY_LONG,
OSD::Color::RED);
return;
}
// Need to SetDisabled outside a lock because it uses m_lock internally.
bool disable = true;
if (volume)
{
std::lock_guard lg{m_lock};
if (!m_loading_volume)
{
m_loading_volume = DiscIO::CreateVolume(volume->GetBlobReader().CopyReader());
disable = false;
}
}
if (disable)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Disabling Achievement Manager due to hash spam.");
SetDisabled(true);
callback(ResponseType::EXPIRED_CONTEXT);
return;
}
m_system = &Core::System::GetInstance();
m_queue.EmplaceItem([this, callback] {
Hash new_hash;
{
std::lock_guard lg{m_filereader_lock};
rc_hash_filereader volume_reader{
.open = &AchievementManager::FilereaderOpenByVolume,
.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);
if (!rc_hash_generate_from_file(new_hash.data(), RC_CONSOLE_GAMECUBE, ""))
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Unable to generate achievement hash from volume.");
callback(ResponseType::MALFORMED_OBJECT);
return;
}
}
{
std::lock_guard lg{m_lock};
if (m_disabled)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Achievements disabled while hash was resolving.");
callback(ResponseType::EXPIRED_CONTEXT);
return;
}
m_game_hash = std::move(new_hash);
m_loading_volume.reset();
}
LoadGameSync(callback);
});
}
void AchievementManager::LoadGameSync(const ResponseCallback& callback)
{
if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken())
{
callback(ResponseType::NOT_ENABLED);
return;
}
u32 new_game_id = 0;
Hash current_hash;
{
std::lock_guard lg{m_lock};
current_hash = m_game_hash;
}
const auto resolve_hash_response = ResolveHash(current_hash, &new_game_id);
if (resolve_hash_response != ResponseType::SUCCESS || new_game_id == 0)
{
INFO_LOG_FMT(ACHIEVEMENTS, "No RetroAchievements data found for this game.");
OSD::AddMessage("No RetroAchievements data found for this game.", OSD::Duration::VERY_LONG,
OSD::Color::RED);
SetDisabled(true);
callback(resolve_hash_response);
return;
}
u32 old_game_id;
{
std::lock_guard lg{m_lock};
old_game_id = m_game_id;
}
if (new_game_id == old_game_id)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Alternate hash resolved for current game {}.", old_game_id);
callback(ResponseType::SUCCESS);
return;
}
else if (old_game_id != 0)
{
INFO_LOG_FMT(ACHIEVEMENTS, "Swapping game {} for game {}; achievements disabled.", old_game_id,
new_game_id);
OSD::AddMessage("Achievements are now disabled. Please close emulation to re-enable.",
OSD::Duration::VERY_LONG, OSD::Color::RED);
SetDisabled(true);
callback(ResponseType::EXPIRED_CONTEXT);
return;
}
{
std::lock_guard lg{m_lock};
m_game_id = new_game_id;
}
const auto start_session_response = StartRASession();
if (start_session_response != ResponseType::SUCCESS)
{
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to connect to RetroAchievements server.");
OSD::AddMessage("Failed to connect to RetroAchievements server.", OSD::Duration::VERY_LONG,
OSD::Color::RED);
callback(start_session_response);
return;
}
const auto fetch_game_data_response = FetchGameData();
if (fetch_game_data_response != ResponseType::SUCCESS)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Unable to retrieve data from RetroAchievements server.");
OSD::AddMessage("Unable to retrieve data from RetroAchievements server.",
OSD::Duration::VERY_LONG, OSD::Color::RED);
return;
}
INFO_LOG_FMT(ACHIEVEMENTS, "Loading achievements for {}.", m_game_data.title);
// Claim the lock, then queue the fetch unlock data calls, then initialize the unlock map in
// ActivateDeactiveAchievements. This allows the calls to process while initializing the
// unlock map but then forces them to wait until it's initialized before making modifications to
// it.
{
std::lock_guard lg{m_lock};
m_is_game_loaded = true;
m_framecount = 0;
LoadUnlockData([](ResponseType r_type) {});
ActivateDeactivateAchievements();
ActivateDeactivateLeaderboards();
ActivateDeactivateRichPresence();
}
FetchBadges();
// Reset this to zero so that RP immediately triggers on the first frame
m_last_ping_time = 0;
INFO_LOG_FMT(ACHIEVEMENTS, "RetroAchievements successfully loaded for {}.", m_game_data.title);
m_update_callback();
callback(fetch_game_data_response);
rc_client_begin_identify_and_load_game(m_client, RC_CONSOLE_GAMECUBE, file_path.c_str(), NULL, 0,
LoadGameCallback, NULL);
}
bool AchievementManager::IsGameLoaded() const
{
return m_is_game_loaded;
}
void AchievementManager::LoadUnlockData(const ResponseCallback& callback)
{
if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken())
{
callback(ResponseType::NOT_ENABLED);
return;
}
m_queue.EmplaceItem([this, callback] {
const auto hardcore_unlock_response = FetchUnlockData(true);
if (hardcore_unlock_response != ResponseType::SUCCESS)
{
ERROR_LOG_FMT(ACHIEVEMENTS,
"Failed to fetch hardcore unlock data; skipping softcore unlock.");
callback(hardcore_unlock_response);
return;
}
callback(FetchUnlockData(false));
m_update_callback();
});
}
void AchievementManager::ActivateDeactivateAchievements()
{
std::lock_guard lg{m_lock};
if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken())
return;
bool enabled = Config::Get(Config::RA_ACHIEVEMENTS_ENABLED);
bool unofficial = Config::Get(Config::RA_UNOFFICIAL_ENABLED);
bool encore = Config::Get(Config::RA_ENCORE_ENABLED);
for (u32 ix = 0; ix < m_game_data.num_achievements; ix++)
{
auto iter =
m_unlock_map.insert({m_game_data.achievements[ix].id,
UnlockStatus{.game_data_index = ix,
.points = m_game_data.achievements[ix].points,
.category = m_game_data.achievements[ix].category}});
ActivateDeactivateAchievement(iter.first->first, enabled, unofficial, encore);
}
INFO_LOG_FMT(ACHIEVEMENTS, "Achievements (de)activated.");
}
void AchievementManager::ActivateDeactivateLeaderboards()
{
std::lock_guard lg{m_lock};
if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken())
return;
bool leaderboards_enabled =
Config::Get(Config::RA_LEADERBOARDS_ENABLED) && Config::Get(Config::RA_HARDCORE_ENABLED);
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)
{
rc_runtime_activate_lboard(&m_runtime, leaderboard_id, leaderboard.definition, nullptr, 0);
m_queue.EmplaceItem([this, leaderboard_id] {
FetchBoardInfo(leaderboard_id);
m_update_callback();
});
}
else
{
rc_runtime_deactivate_lboard(&m_runtime, m_game_data.leaderboards[ix].id);
}
}
INFO_LOG_FMT(ACHIEVEMENTS, "Leaderboards (de)activated.");
}
void AchievementManager::ActivateDeactivateRichPresence()
{
std::lock_guard lg{m_lock};
if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken())
return;
rc_runtime_activate_richpresence(
&m_runtime,
(m_is_game_loaded && Config::Get(Config::RA_RICH_PRESENCE_ENABLED)) ?
m_game_data.rich_presence_script :
"",
nullptr, 0);
INFO_LOG_FMT(ACHIEVEMENTS, "Rich presence (de)activated.");
auto* game_info = rc_client_get_game_info(m_client);
return game_info && game_info->id != 0;
}
void AchievementManager::FetchBadges()
@ -910,14 +650,9 @@ void AchievementManager::CloseGame()
{
{
std::lock_guard lg{m_lock};
if (m_is_game_loaded)
if (rc_client_get_game_info(m_client))
{
m_is_game_loaded = false;
m_active_challenges.clear();
ActivateDeactivateAchievements();
ActivateDeactivateLeaderboards();
ActivateDeactivateRichPresence();
m_game_id = 0;
m_game_badge.name.clear();
m_unlock_map.clear();
m_leaderboard_map.clear();
@ -925,6 +660,7 @@ void AchievementManager::CloseGame()
m_game_data = {};
m_queue.Cancel();
m_image_queue.Cancel();
rc_client_unload_game(m_client);
m_system = nullptr;
}
}
@ -1070,157 +806,6 @@ void AchievementManager::LoginCallback(int result, const char* error_message, rc
Config::SetBaseOrCurrent(Config::RA_API_TOKEN, user->token);
}
AchievementManager::ResponseType AchievementManager::ResolveHash(const Hash& game_hash,
u32* game_id)
{
rc_api_resolve_hash_response_t hash_data{};
std::string username, api_token;
{
std::lock_guard lg{m_lock};
username = Config::Get(Config::RA_USERNAME);
api_token = Config::Get(Config::RA_API_TOKEN);
}
rc_api_resolve_hash_request_t resolve_hash_request = {
.username = username.c_str(), .api_token = api_token.c_str(), .game_hash = game_hash.data()};
ResponseType r_type = Request<rc_api_resolve_hash_request_t, rc_api_resolve_hash_response_t>(
resolve_hash_request, &hash_data, rc_api_init_resolve_hash_request,
rc_api_process_resolve_hash_response);
if (r_type == ResponseType::SUCCESS)
{
*game_id = hash_data.game_id;
INFO_LOG_FMT(ACHIEVEMENTS, "Hashed game ID {} for RetroAchievements.", *game_id);
}
else
{
INFO_LOG_FMT(ACHIEVEMENTS, "Hash {} not recognized by RetroAchievements.", game_hash.data());
}
rc_api_destroy_resolve_hash_response(&hash_data);
return r_type;
}
AchievementManager::ResponseType AchievementManager::StartRASession()
{
rc_api_start_session_request_t start_session_request;
rc_api_start_session_response_t session_data{};
std::string username, api_token;
{
std::lock_guard lg{m_lock};
username = Config::Get(Config::RA_USERNAME);
api_token = Config::Get(Config::RA_API_TOKEN);
start_session_request = {
.username = username.c_str(), .api_token = api_token.c_str(), .game_id = m_game_id};
}
ResponseType r_type = Request<rc_api_start_session_request_t, rc_api_start_session_response_t>(
start_session_request, &session_data, rc_api_init_start_session_request,
rc_api_process_start_session_response);
rc_api_destroy_start_session_response(&session_data);
return r_type;
}
AchievementManager::ResponseType AchievementManager::FetchGameData()
{
rc_api_fetch_game_data_request_t fetch_data_request;
rc_api_request_t api_request;
Common::HttpRequest http_request;
std::string username, api_token;
u32 game_id;
{
std::lock_guard lg{m_lock};
username = Config::Get(Config::RA_USERNAME);
api_token = Config::Get(Config::RA_API_TOKEN);
game_id = m_game_id;
}
fetch_data_request = {
.username = username.c_str(), .api_token = api_token.c_str(), .game_id = game_id};
if (rc_api_init_fetch_game_data_request(&api_request, &fetch_data_request) != RC_OK ||
!api_request.post_data)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid API request for game data.");
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)
{
WARN_LOG_FMT(ACHIEVEMENTS,
"RetroAchievements connection failed while fetching game data for ID {}. \nURL: "
"{} \npost_data: {}",
game_id, api_request.url,
api_request.post_data == nullptr ? "NULL" : api_request.post_data);
return ResponseType::CONNECTION_FAILED;
}
std::lock_guard lg{m_lock};
const std::string response_str(http_response->begin(), http_response->end());
if (rc_api_process_fetch_game_data_response(&m_game_data, response_str.c_str()) != RC_OK)
{
ERROR_LOG_FMT(ACHIEVEMENTS,
"Failed to process HTTP response fetching game data for ID {}. \nURL: {} "
"\npost_data: {} \nresponse: {}",
game_id, api_request.url,
api_request.post_data == nullptr ? "NULL" : api_request.post_data, response_str);
rc_api_destroy_fetch_game_data_response(&m_game_data);
m_game_data = {};
return ResponseType::MALFORMED_OBJECT;
}
if (!m_game_data.response.succeeded)
{
WARN_LOG_FMT(
ACHIEVEMENTS,
"Invalid RetroAchievements credentials fetching game data for ID {}; logging out user {}",
game_id, username);
// Logout technically does this via a CloseGame call, but doing this now prevents the activate
// methods from thinking they have something to do.
rc_api_destroy_fetch_game_data_response(&m_game_data);
m_game_data = {};
Logout();
return ResponseType::INVALID_CREDENTIALS;
}
if (game_id != m_game_id)
{
INFO_LOG_FMT(ACHIEVEMENTS,
"Attempted to retrieve game data for ID {}; running game is now ID {}", game_id,
m_game_id);
rc_api_destroy_fetch_game_data_response(&m_game_data);
m_game_data = {};
return ResponseType::EXPIRED_CONTEXT;
}
INFO_LOG_FMT(ACHIEVEMENTS, "Retrieved game data for ID {}.", game_id);
return ResponseType::SUCCESS;
}
AchievementManager::ResponseType AchievementManager::FetchUnlockData(bool hardcore)
{
rc_api_fetch_user_unlocks_response_t unlock_data{};
std::string username = Config::Get(Config::RA_USERNAME);
std::string api_token = Config::Get(Config::RA_API_TOKEN);
rc_api_fetch_user_unlocks_request_t fetch_unlocks_request = {.username = username.c_str(),
.api_token = api_token.c_str(),
.game_id = m_game_id,
.hardcore = hardcore};
ResponseType r_type =
Request<rc_api_fetch_user_unlocks_request_t, rc_api_fetch_user_unlocks_response_t>(
fetch_unlocks_request, &unlock_data, rc_api_init_fetch_user_unlocks_request,
rc_api_process_fetch_user_unlocks_response);
if (r_type == ResponseType::SUCCESS)
{
std::lock_guard lg{m_lock};
bool enabled = Config::Get(Config::RA_ACHIEVEMENTS_ENABLED);
bool unofficial = Config::Get(Config::RA_UNOFFICIAL_ENABLED);
bool encore = Config::Get(Config::RA_ENCORE_ENABLED);
for (AchievementId ix = 0; ix < unlock_data.num_achievement_ids; ix++)
{
auto it = m_unlock_map.find(unlock_data.achievement_ids[ix]);
if (it == m_unlock_map.end())
continue;
it->second.remote_unlock_status =
hardcore ? UnlockStatus::UnlockType::HARDCORE : UnlockStatus::UnlockType::SOFTCORE;
ActivateDeactivateAchievement(unlock_data.achievement_ids[ix], enabled, unofficial, encore);
}
}
rc_api_destroy_fetch_user_unlocks_response(&unlock_data);
return r_type;
}
AchievementManager::ResponseType AchievementManager::FetchBoardInfo(AchievementId leaderboard_id)
{
std::string username = Config::Get(Config::RA_USERNAME);
@ -1440,6 +1025,27 @@ AchievementManager::PingRichPresence(const RichPresence& rich_presence)
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};

View File

@ -119,14 +119,9 @@ public:
void SetUpdateCallback(UpdateCallback callback);
void Login(const std::string& password);
bool HasAPIToken() const;
void HashGame(const std::string& file_path, const ResponseCallback& callback);
void HashGame(const DiscIO::Volume* volume, const ResponseCallback& callback);
void LoadGame(const std::string& file_path, const DiscIO::Volume* volume);
bool IsGameLoaded() const;
void LoadUnlockData(const ResponseCallback& callback);
void ActivateDeactivateAchievements();
void ActivateDeactivateLeaderboards();
void ActivateDeactivateRichPresence();
void FetchBadges();
void DoFrame();
@ -173,11 +168,7 @@ private:
static void LoginCallback(int result, const char* error_message, rc_client_t* client,
void* userdata);
ResponseType ResolveHash(const Hash& game_hash, u32* game_id);
void LoadGameSync(const ResponseCallback& callback);
ResponseType StartRASession();
ResponseType FetchGameData();
ResponseType FetchUnlockData(bool hardcore);
ResponseType FetchBoardInfo(AchievementId leaderboard_id);
std::unique_ptr<DiscIO::Volume>& GetLoadingVolume() { return m_loading_volume; };
@ -189,6 +180,8 @@ private:
ResponseType SubmitLeaderboard(AchievementId leaderboard_id, int value);
ResponseType PingRichPresence(const RichPresence& rich_presence);
static void LoadGameCallback(int result, const char* error_message, rc_client_t* client,
void* userdata);
void DisplayWelcomeMessage();
void HandleAchievementTriggeredEvent(const rc_runtime_event_t* runtime_event);

View File

@ -576,8 +576,7 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
}
#ifdef USE_RETRO_ACHIEVEMENTS
AchievementManager::GetInstance().HashGame(executable.path,
[](AchievementManager::ResponseType r_type) {});
AchievementManager::GetInstance().LoadGame(executable.path, nullptr);
#endif // USE_RETRO_ACHIEVEMENTS
if (!executable.reader->LoadIntoMemory(system))

View File

@ -399,8 +399,7 @@ void DVDInterface::SetDisc(std::unique_ptr<DiscIO::VolumeDisc> disc,
}
#ifdef USE_RETRO_ACHIEVEMENTS
AchievementManager::GetInstance().HashGame(disc.get(),
[](AchievementManager::ResponseType r_type) {});
AchievementManager::GetInstance().LoadGame("", disc.get());
#endif // USE_RETRO_ACHIEVEMENTS
// Assume that inserting a disc requires having an empty disc before

View File

@ -268,19 +268,16 @@ void AchievementSettingsWidget::Logout()
void AchievementSettingsWidget::ToggleAchievements()
{
SaveSettings();
AchievementManager::GetInstance().ActivateDeactivateAchievements();
}
void AchievementSettingsWidget::ToggleLeaderboards()
{
SaveSettings();
AchievementManager::GetInstance().ActivateDeactivateLeaderboards();
}
void AchievementSettingsWidget::ToggleRichPresence()
{
SaveSettings();
AchievementManager::GetInstance().ActivateDeactivateRichPresence();
}
void AchievementSettingsWidget::ToggleHardcore()
@ -311,13 +308,11 @@ void AchievementSettingsWidget::ToggleBadges()
void AchievementSettingsWidget::ToggleUnofficial()
{
SaveSettings();
AchievementManager::GetInstance().ActivateDeactivateAchievements();
}
void AchievementSettingsWidget::ToggleEncore()
{
SaveSettings();
AchievementManager::GetInstance().ActivateDeactivateAchievements();
}
#endif // USE_RETRO_ACHIEVEMENTS