/* This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ // Derived from duckstation: https://github.com/stenzek/duckstation/blob/master/src/core/achievements.cpp // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "achievements.h" #include "serialize.h" #ifdef USE_RACHIEVEMENTS #include "oslib/directory.h" #include "oslib/http_client.h" #include "hw/sh4/sh4_mem.h" #include "rend/gui_achievements.h" #include "imgread/common.h" #include "cfg/option.h" #include "oslib/oslib.h" #include "emulator.h" #include "stdclass.h" #include #include #include #include #include #include #include #include #include namespace achievements { class Achievements { public: Achievements(); ~Achievements(); bool init(); void term(); std::future login(const char *username, const char *password); void logout(); bool isLoggedOn() const { return loggedOn; } bool isActive() const { return active; } Game getCurrentGame(); std::vector getAchievementList(); void serialize(Serializer& ser); void deserialize(Deserializer& deser); static Achievements& Instance(); private: bool createClient(); std::string getGameHash(); void loadGame(); void gameLoaded(int result, const char *errorMessage); void unloadGame(); void pauseGame(); void resumeGame(); void loadCache(); std::string getOrDownloadImage(const char *url); std::tuple getCachedImage(const char *url); void diskChange(); static void clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata); static void clientLoginWithPasswordCallback(int result, const char *error_message, rc_client_t *client, void *userdata); void authenticationSuccess(const rc_client_user_t *user); static void clientMessageCallback(const char *message, const rc_client_t *client); static u32 clientReadMemory(u32 address, u8 *buffer, u32 num_bytes, rc_client_t *client); static void clientServerCall(const rc_api_request_t *request, rc_client_server_callback_t callback, void *callback_data, rc_client_t *client); static void clientEventHandler(const rc_client_event_t *event, rc_client_t *client); void handleResetEvent(const rc_client_event_t *event); void handleUnlockEvent(const rc_client_event_t *event); void handleAchievementChallengeIndicatorShowEvent(const rc_client_event_t *event); void handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event); void handleGameCompleted(const rc_client_event_t *event); void handleShowAchievementProgress(const rc_client_event_t *event); void handleHideAchievementProgress(const rc_client_event_t *event); void handleUpdateAchievementProgress(const rc_client_event_t *event); static void emuEventCallback(Event event, void *arg); rc_client_t *rc_client = nullptr; bool loggedOn = false; std::atomic_bool loadingGame {}; bool active = false; bool paused = false; std::string lastError; std::future asyncServerCall; cResetEvent resetEvent; std::string cachePath; std::unordered_map cacheMap; std::mutex cacheMutex; std::future asyncImageDownload; }; bool init() { return Achievements::Instance().init(); } void term() { Achievements::Instance().term(); } std::future login(const char *username, const char *password) { return Achievements::Instance().login(username, password); } void logout() { Achievements::Instance().logout(); } bool isLoggedOn() { return Achievements::Instance().isLoggedOn(); } bool isActive() { return Achievements::Instance().isActive(); } Game getCurrentGame() { return Achievements::Instance().getCurrentGame(); } std::vector getAchievementList() { return Achievements::Instance().getAchievementList(); } void serialize(Serializer& ser) { Achievements::Instance().serialize(ser); } void deserialize(Deserializer& deser) { Achievements::Instance().deserialize(deser); } Achievements& Achievements::Instance() { static Achievements instance; return instance; } // create the instance at start up OnLoad _([]() { Achievements::Instance(); }); Achievements::Achievements() { EventManager::listen(Event::Start, emuEventCallback, this); EventManager::listen(Event::Terminate, emuEventCallback, this); EventManager::listen(Event::Pause, emuEventCallback, this); EventManager::listen(Event::Resume, emuEventCallback, this); EventManager::listen(Event::DiskChange, emuEventCallback, this); } Achievements::~Achievements() { EventManager::unlisten(Event::Start, emuEventCallback, this); EventManager::unlisten(Event::Terminate, emuEventCallback, this); EventManager::unlisten(Event::Pause, emuEventCallback, this); EventManager::unlisten(Event::Resume, emuEventCallback, this); EventManager::unlisten(Event::DiskChange, emuEventCallback, this); term(); } bool Achievements::init() { if (rc_client != nullptr) return true; if (!createClient()) return false; rc_client_set_event_handler(rc_client, clientEventHandler); //TODO rc_client_set_hardcore_enabled(rc_client, 0); //rc_client_set_encore_mode_enabled(rc_client, 0); //rc_client_set_unofficial_enabled(rc_client, 0); //rc_client_set_spectator_mode_enabled(rc_client, 0); loadCache(); if (!config::AchievementsUserName.get().empty() && !config::AchievementsToken.get().empty()) { INFO_LOG(COMMON, "RA: Attempting login with user '%s'...", config::AchievementsUserName.get().c_str()); rc_client_begin_login_with_token(rc_client, config::AchievementsUserName.get().c_str(), config::AchievementsToken.get().c_str(), clientLoginWithTokenCallback, nullptr); } return true; } bool Achievements::createClient() { http::init(); rc_client = rc_client_create(clientReadMemory, clientServerCall); if (rc_client == nullptr) { WARN_LOG(COMMON, "Can't create RetroAchievements client"); return false; } #if !defined(NDEBUG) || defined(DEBUGFAST) rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, clientMessageCallback); #else rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_WARN, clientMessageCallback); #endif rc_client_set_userdata(rc_client, this); return true; } void Achievements::loadCache() { cachePath = get_writable_data_path("achievements/"); flycast::mkdir(cachePath.c_str(), 0755); DIR *dir = flycast::opendir(cachePath.c_str()); if (dir != nullptr) { while (true) { dirent *direntry = flycast::readdir(dir); if (direntry == nullptr) break; std::string name = direntry->d_name; if (name == "." || name == "..") continue; std::string s = get_file_basename(name); u64 v = strtoull(s.c_str(), nullptr, 16); std::lock_guard _(cacheMutex); cacheMap[v] = name; } flycast::closedir(dir); } } static u64 hashUrl(const char *url) { return XXH64(url, strlen(url), 13); } std::tuple Achievements::getCachedImage(const char *url) { u64 hash = hashUrl(url); std::lock_guard _(cacheMutex); auto it = cacheMap.find(hash); if (it != cacheMap.end()) { return make_tuple(cachePath + it->second, true); } else { std::stringstream stream; stream << std::hex << hash << ".png"; return make_tuple(cachePath + stream.str(), false); } } std::string Achievements::getOrDownloadImage(const char *url) { u64 hash = hashUrl(url); { std::lock_guard _(cacheMutex); auto it = cacheMap.find(hash); if (it != cacheMap.end()) return cachePath + it->second; } std::vector content; std::string content_type; int rc = http::get(url, content, content_type); if (!http::success(rc)) return {}; std::stringstream stream; stream << std::hex << hash << ".png"; std::string localPath = cachePath + stream.str(); FILE *f = nowide::fopen(localPath.c_str(), "wb"); if (f == nullptr) { WARN_LOG(COMMON, "Can't save image to %s", localPath.c_str()); return {}; } fwrite(content.data(), 1, content.size(), f); fclose(f); { std::lock_guard _(cacheMutex); cacheMap[hash] = stream.str(); } DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, localPath.c_str()); return localPath; } void Achievements::term() { if (rc_client == nullptr) return; unloadGame(); if (asyncImageDownload.valid()) asyncImageDownload.get(); rc_client_destroy(rc_client); rc_client = nullptr; } void Achievements::authenticationSuccess(const rc_client_user_t *user) { NOTICE_LOG(COMMON, "RA Login successful: token %s", config::AchievementsToken.get().c_str()); char url[512]; int rc = rc_client_user_get_image_url(user, url, sizeof(url)); if (rc == RC_OK) { std::string image = getOrDownloadImage(url); std::string text = "User " + config::AchievementsUserName.get() + " authenticated"; notifier.notify(Notification::Login, image, text); } loggedOn = true; if (!settings.content.fileName.empty()) // TODO better test? loadGame(); } void Achievements::clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata) { Achievements *achievements = (Achievements *)rc_client_get_userdata(client); if (result != RC_OK) { WARN_LOG(COMMON, "RA Login failed: %s", error_message); notifier.notify(Notification::Login, "", "RetroAchievements authentication failed", error_message); return; } achievements->authenticationSuccess(rc_client_get_user_info(client)); } std::future Achievements::login(const char* username, const char* password) { init(); std::promise *promise = new std::promise(); rc_client_begin_login_with_password(rc_client, username, password, clientLoginWithPasswordCallback, promise); return promise->get_future(); } void Achievements::clientLoginWithPasswordCallback(int result, const char *error_message, rc_client_t *client, void *userdata) { Achievements *achievements = (Achievements *)rc_client_get_userdata(client); std::promise *promise = (std::promise *)userdata; if (result != RC_OK) { std::string errorMsg = rc_error_str(result); if (error_message != nullptr) errorMsg += ": " + std::string(error_message); promise->set_exception(std::make_exception_ptr(FlycastException(errorMsg))); delete promise; WARN_LOG(COMMON, "RA Login failed: %s", errorMsg.c_str()); return; } const rc_client_user_t* user = rc_client_get_user_info(client); if (!user || !user->token) { WARN_LOG(COMMON, "RA: rc_client_get_user_info() returned NULL"); promise->set_exception(std::make_exception_ptr(FlycastException("No user token returned"))); delete promise; return; } // Store token in config config::AchievementsToken = user->token; SaveSettings(); achievements->authenticationSuccess(user); promise->set_value(); delete promise; } void Achievements::logout() { unloadGame(); rc_client_logout(rc_client); // Reset token in config config::AchievementsToken = ""; SaveSettings(); loggedOn = false; } void Achievements::clientMessageCallback(const char* message, const rc_client_t* client) { #if !defined(NDEBUG) || defined(DEBUGFAST) DEBUG_LOG(COMMON, "RA: %s", message); #else WARN_LOG(COMMON, "RA error: %s", message); #endif } u32 Achievements::clientReadMemory(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client) { if (address + num_bytes > RAM_SIZE) return 0; address += 0x0C000000; switch (num_bytes) { case 1: *buffer = ReadMem8_nommu(address); break; case 2: *(u16 *)buffer = ReadMem16_nommu(address); break; case 4: *(u32 *)buffer = ReadMem32_nommu(address); break; default: return 0; } return num_bytes; } void Achievements::clientServerCall(const rc_api_request_t *request, rc_client_server_callback_t callback, void *callback_data, rc_client_t *client) { Achievements *achievements = (Achievements *)rc_client_get_userdata(client); std::string url {request->url}; std::string payload; if (request->post_data != nullptr) payload = request->post_data; std::string contentType; if (request->content_type != nullptr) contentType = request->content_type; const auto& callServer = [url, contentType, payload, callback, callback_data]() { ThreadName _("Flycast-RA"); int rc; std::vector reply; if (!payload.empty()) rc = http::post(url, payload.c_str(), contentType.empty() ? nullptr : contentType.c_str(), reply); else rc = http::get(url, reply); rc_api_server_response_t rr; rr.http_status_code = rc; // TODO RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR if connection fails? rr.body_length = reply.size(); rr.body = (const char *)reply.data(); callback(&rr, callback_data); }; if (achievements->asyncServerCall.valid()) { if (achievements->asyncServerCall.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) { INFO_LOG(COMMON, "Async server call already in progress"); // process synchronously callServer(); return; } achievements->asyncServerCall.get(); } achievements->asyncServerCall = std::async(std::launch::async, callServer); } static void handleServerError(const rc_client_server_error_t* error) { WARN_LOG(COMMON, "RA server error: %s - %s", error->api, error->error_message); notifier.notify(Notification::Error, "", error->api, error->error_message); } void Achievements::clientEventHandler(const rc_client_event_t* event, rc_client_t* client) { Achievements *achievements = (Achievements *)rc_client_get_userdata(client); switch (event->type) { case RC_CLIENT_EVENT_RESET: achievements->handleResetEvent(event); break; case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: achievements->handleUnlockEvent(event); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: achievements->handleAchievementChallengeIndicatorShowEvent(event); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: achievements->handleAchievementChallengeIndicatorHideEvent(event); break; case RC_CLIENT_EVENT_GAME_COMPLETED: achievements->handleGameCompleted(event); break; case RC_CLIENT_EVENT_SERVER_ERROR: handleServerError(event->server_error); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: achievements->handleShowAchievementProgress(event); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: achievements->handleHideAchievementProgress(event); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: achievements->handleUpdateAchievementProgress(event); break; /* TODO case RC_CLIENT_EVENT_LEADERBOARD_STARTED: case RC_CLIENT_EVENT_LEADERBOARD_FAILED: case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: case RC_CLIENT_EVENT_DISCONNECTED: case RC_CLIENT_EVENT_RECONNECTED: */ default: WARN_LOG(COMMON, "RA: Unhandled event: %u", event->type); break; } } void Achievements::handleResetEvent(const rc_client_event_t *event) { INFO_LOG(COMMON, "RA: Resetting runtime due to reset event"); rc_client_reset(rc_client); } void Achievements::handleUnlockEvent(const rc_client_event_t *event) { const rc_client_achievement_t* cheevo = event->achievement; assert(cheevo != nullptr); INFO_LOG(COMMON, "RA: Achievement %s (%u) for game %s unlocked", cheevo->title, cheevo->id, settings.content.title.c_str()); char url[512]; int rc = rc_client_achievement_get_image_url(cheevo, cheevo->state, url, sizeof(url)); if (rc == RC_OK) { std::string image = getOrDownloadImage(url); std::string text = "Achievement " + std::string(cheevo->title) + " unlocked!"; notifier.notify(Notification::Login, image, text, cheevo->description); } } void Achievements::handleAchievementChallengeIndicatorShowEvent(const rc_client_event_t *event) { INFO_LOG(COMMON, "RA: Challenge: %s", event->achievement->title); char url[128]; int rc = rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)); if (rc == RC_OK) { std::string image = getOrDownloadImage(url); notifier.showChallenge(image); } } void Achievements::handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event) { INFO_LOG(COMMON, "RA: Challenge hidden: %s", event->achievement->title); char url[128]; int rc = rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)); if (rc == RC_OK) { std::string image = getOrDownloadImage(url); notifier.hideChallenge(image); } } void Achievements::handleGameCompleted(const rc_client_event_t *event) { const rc_client_game_t* game = rc_client_get_game_info(rc_client); std::string image; char url[128]; if (rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK) image = getOrDownloadImage(url); std::string text1 = (rc_client_get_hardcore_enabled(rc_client) ? "Mastered " : "Completed ") + std::string(game->title); rc_client_user_game_summary_t summary; rc_client_get_user_game_summary(rc_client, &summary); std::stringstream ss; ss << summary.num_unlocked_achievements << " achievements, " << summary.points_unlocked << " points"; std::string text3 = rc_client_get_user_info(rc_client)->display_name; notifier.notify(Notification::Mastery, image, text1, ss.str(), text3); } void Achievements::handleShowAchievementProgress(const rc_client_event_t *event) { handleUpdateAchievementProgress(event); } void Achievements::handleHideAchievementProgress(const rc_client_event_t *event) { notifier.notify(Notification::Progress, "", ""); } void Achievements::handleUpdateAchievementProgress(const rc_client_event_t *event) { char url[128]; std::string image; if (rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) == RC_OK) image = getOrDownloadImage(url); notifier.notify(Notification::Progress, image, event->achievement->measured_progress); } static Disc *hashDisk; static bool add150; static void *cdreader_open_track(const char* path, u32 track) { DEBUG_LOG(COMMON, "RA: cdreader_open_track %s track %d", path, track); if (track == RC_HASH_CDTRACK_FIRST_DATA) { for (const Track& track : hashDisk->tracks) if (track.isDataTrack()) return const_cast(&track); return nullptr; } if (track <= hashDisk->tracks.size()) return const_cast(&hashDisk->tracks[track - 1]); else return nullptr; } static size_t cdreader_read_sector(void* track_handle, u32 sector, void* buffer, size_t requested_bytes) { if (requested_bytes == 2048) // add 150 sectors to FAD corresponding to files // FIXME get rid of this add150 = true; //DEBUG_LOG(COMMON, "RA: cdreader_read_sector track %p sec %d+%d num %zd", track_handle, sector, add150 ? 150 : 0, requested_bytes); if (add150) sector += 150; u8 locbuf[2048]; hashDisk->ReadSectors(sector, 1, locbuf, 2048); requested_bytes = std::min(requested_bytes, 2048); memcpy(buffer, locbuf, requested_bytes); return requested_bytes; } static void cdreader_close_track(void* track_handle) { } static u32 cdreader_first_track_sector(void* track_handle) { Track& track = *static_cast(track_handle); DEBUG_LOG(COMMON, "RA: cdreader_first_track_sector track %p -> %d", track_handle, track.StartFAD); return track.StartFAD; } std::string Achievements::getGameHash() { if (settings.platform.isConsole()) { const u32 diskType = libGDR_GetDiscType(); if (diskType == NoDisk || diskType == Open) return {}; // Reopen the disk locally to avoid threading issues (CHD) try { hashDisk = OpenDisc(settings.content.path); } catch (const FlycastException& e) { return {}; } add150 = false; rc_hash_cdreader hooks = { cdreader_open_track, cdreader_read_sector, cdreader_close_track, cdreader_first_track_sector }; rc_hash_init_custom_cdreader(&hooks); rc_hash_init_error_message_callback([](const char *msg) { WARN_LOG(COMMON, "cdreader: %s", msg); }); #if !defined(NDEBUG) || defined(DEBUGFAST) rc_hash_init_verbose_message_callback([](const char *msg) { DEBUG_LOG(COMMON, "cdreader: %s", msg); }); #endif } char hash[33] {}; rc_hash_generate_from_file(hash, settings.platform.isConsole() ? RC_CONSOLE_DREAMCAST : RC_CONSOLE_ARCADE, settings.content.fileName.c_str()); // fileName is only used for arcade delete hashDisk; hashDisk = nullptr; return hash; } void Achievements::pauseGame() { paused = true; if (active) { resetEvent.Reset(); if (asyncServerCall.valid()) asyncServerCall.get(); asyncServerCall = std::async(std::launch::async, [this]() { while (paused) { resetEvent.Wait(1000); if (paused) rc_client_idle(rc_client); } }); } } void Achievements::resumeGame() { paused = false; resetEvent.Set(); if (asyncServerCall.valid()) asyncServerCall.get(); if (config::EnableAchievements) loadGame(); else term(); } void Achievements::emuEventCallback(Event event, void *arg) { Achievements *instance = ((Achievements *)arg); switch (event) { case Event::Start: case Event::Resume: instance->resumeGame(); break; case Event::Terminate: instance->unloadGame(); break; case Event::VBlank: rc_client_do_frame(instance->rc_client); break; case Event::Pause: instance->pauseGame(); break; case Event::DiskChange: instance->diskChange(); break; default: break; } } void Achievements::loadGame() { if (loadingGame.exchange(true)) // already loading return; if (active) { // already loaded loadingGame = false; return; } if (!init() || !isLoggedOn()) { if (!isLoggedOn()) INFO_LOG(COMMON, "Not logged on. Not loading game yet"); loadingGame = false; return; } std::string gameHash = getGameHash(); if (!gameHash.empty()) { rc_client_begin_load_game(rc_client, gameHash.c_str(), [](int result, const char *error_message, rc_client_t *client, void *userdata) { ((Achievements *)userdata)->gameLoaded(result, error_message); }, this); } else { INFO_LOG(COMMON, "RA: empty hash. Aborting load"); loadingGame = false; } } void Achievements::gameLoaded(int result, const char *errorMessage) { if (result != RC_OK) { if (result == RC_NO_GAME_LOADED) // Unknown game. INFO_LOG(COMMON, "RA: Unknown game, disabling achievements."); else if (result == RC_LOGIN_REQUIRED) { // We would've asked to re-authenticate, so leave HC on for now. // Once we've done so, we'll reload the game. } else WARN_LOG(COMMON, "RA Loading game failed: %s", errorMessage); loadingGame = false; return; } const rc_client_game_t* info = rc_client_get_game_info(rc_client); if (info == nullptr) { WARN_LOG(COMMON, "RA: rc_client_get_game_info() returned NULL"); loadingGame = false; return; } active = true; loadingGame = false; EventManager::listen(Event::VBlank, emuEventCallback, this); NOTICE_LOG(COMMON, "RA: game %d loaded: %s, achievements %d leaderboards %d rich presence %d", info->id, info->title, rc_client_has_achievements(rc_client), rc_client_has_leaderboards(rc_client), rc_client_has_rich_presence(rc_client)); std::string image; char url[512]; if (rc_client_game_get_image_url(info, url, sizeof(url)) == RC_OK) image = getOrDownloadImage(url); rc_client_user_game_summary_t summary; rc_client_get_user_game_summary(rc_client, &summary); std::string text = "You have " + std::to_string(summary.num_unlocked_achievements) + " of " + std::to_string(summary.num_core_achievements) + " achievements unlocked"; notifier.notify(Notification::Login, image, info->title, text); } void Achievements::unloadGame() { if (!active) return; active = false; paused = false; resetEvent.Set(); if (asyncServerCall.valid()) asyncServerCall.get(); EventManager::unlisten(Event::VBlank, emuEventCallback, this); rc_client_unload_game(rc_client); } void Achievements::diskChange() { if (!active) return; std::string hash = getGameHash(); if (hash == "") { unloadGame(); return; } rc_client_begin_change_media_from_hash(rc_client, hash.c_str(), [](int result, const char *errorMessage, rc_client_t *client, void *userdata) { if (result == RC_HARDCORE_DISABLED) { notifier.notify(Notification::Login, "", "Hardcore disabled", "Unrecognized media inserted"); } else if (result != RC_OK) { if (errorMessage == nullptr) errorMessage = rc_error_str(result); notifier.notify(Notification::Login, "", "Media change failed", errorMessage); } }, this); } Game Achievements::getCurrentGame() { if (!active) return Game{}; const rc_client_game_t *info = rc_client_get_game_info(rc_client); if (info == nullptr) return Game{}; char url[128]; std::string image; if (rc_client_game_get_image_url(info, url, sizeof(url)) == RC_OK) { bool cached; std::tie(image, cached) = getCachedImage(url); if (!cached) { if (asyncImageDownload.valid()) { if (asyncImageDownload.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) INFO_LOG(COMMON, "Async image download already in progress"); else asyncImageDownload.get(); } if (!asyncImageDownload.valid()) { std::string surl = url; asyncImageDownload = std::async(std::launch::async, [this, surl]() { getOrDownloadImage(surl.c_str()); }); } } } rc_client_user_game_summary_t summary; rc_client_get_user_game_summary(rc_client, &summary); return Game{ image, info->title, summary.num_unlocked_achievements, summary.num_core_achievements, summary.points_unlocked, summary.points_core }; } std::vector Achievements::getAchievementList() { std::vector achievements; rc_client_achievement_list_t *list = rc_client_create_achievement_list(rc_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); std::vector uncachedImages; for (u32 i = 0; i < list->num_buckets; i++) { const char *label = list->buckets[i].label; for (u32 j = 0; j < list->buckets[i].num_achievements; j++) { const rc_client_achievement_t *achievement = list->buckets[i].achievements[j]; char url[128]; std::string image; if (rc_client_achievement_get_image_url(achievement, achievement->state, url, sizeof(url)) == RC_OK) { bool cached; std::tie(image, cached) = getCachedImage(url); if (!cached) uncachedImages.push_back(url); } std::string status; if (achievement->measured_percent) status = achievement->measured_progress; achievements.emplace_back(image, achievement->title, achievement->description, label, status); } } rc_client_destroy_achievement_list(list); if (!uncachedImages.empty()) { if (asyncImageDownload.valid()) { if (asyncImageDownload.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) INFO_LOG(COMMON, "Async image download already in progress"); else asyncImageDownload.get(); } if (!asyncImageDownload.valid()) { asyncImageDownload = std::async(std::launch::async, [this, uncachedImages]() { for (const std::string& url : uncachedImages) getOrDownloadImage(url.c_str()); }); } } return achievements; } void Achievements::serialize(Serializer& ser) { u32 size = (u32)rc_client_progress_size(rc_client); if (size > 0) { u8 *buffer = new u8[size]; if (rc_client_serialize_progress(rc_client, buffer) != RC_OK) size = 0; ser << size; ser.serialize(buffer, size); delete[] buffer; } else { ser << size; } } void Achievements::deserialize(Deserializer& deser) { if (deser.version() < Deserializer::V50) { rc_client_deserialize_progress(rc_client, nullptr); } else { u32 size; deser >> size; if (size > 0) { u8 *buffer = new u8[size]; deser.deserialize(buffer, size); rc_client_deserialize_progress(rc_client, buffer); delete[] buffer; } else { rc_client_deserialize_progress(rc_client, nullptr); } } } } // namespace achievements #else // !USE_RACHIEVEMENTS namespace achievements { // Maintain savestate compatiblity with RA-enabled builds void serialize(Serializer& ser) { u32 size = 0; ser << size; } void deserialize(Deserializer& deser) { if (deser.version() >= Deserializer::V50) { u32 size; deser >> size; deser.skip(size); } } } #endif