flycast/core/achievements/achievements.cpp

1010 lines
28 KiB
C++
Raw Normal View History

/*
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 <https://www.gnu.org/licenses/>.
*/
// Derived from duckstation: https://github.com/stenzek/duckstation/blob/master/src/core/achievements.cpp
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// 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"
2024-04-29 14:17:50 +00:00
#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 <cassert>
#include <rc_client.h>
#include <rc_hash.h>
#include <future>
#include <unordered_map>
#include <sstream>
#include <atomic>
#include <tuple>
#include <xxhash.h>
namespace achievements
{
class Achievements
{
public:
Achievements();
~Achievements();
bool init();
void term();
std::future<void> login(const char *username, const char *password);
void logout();
bool isLoggedOn() const { return loggedOn; }
bool isActive() const { return active; }
Game getCurrentGame();
std::vector<Achievement> 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<std::string, bool> 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<void> asyncServerCall;
cResetEvent resetEvent;
std::string cachePath;
std::unordered_map<u64, std::string> cacheMap;
std::mutex cacheMutex;
std::future<void> asyncImageDownload;
};
bool init()
{
return Achievements::Instance().init();
}
void term()
{
Achievements::Instance().term();
}
std::future<void> 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<Achievement> 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<std::mutex> _(cacheMutex);
cacheMap[v] = name;
}
flycast::closedir(dir);
}
}
static u64 hashUrl(const char *url) {
return XXH64(url, strlen(url), 13);
}
std::tuple<std::string, bool> Achievements::getCachedImage(const char *url)
{
u64 hash = hashUrl(url);
std::lock_guard<std::mutex> _(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<std::mutex> _(cacheMutex);
auto it = cacheMap.find(hash);
if (it != cacheMap.end())
return cachePath + it->second;
}
std::vector<u8> 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<std::mutex> _(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<void> Achievements::login(const char* username, const char* password)
{
init();
std::promise<void> *promise = new std::promise<void>();
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<void> *promise = (std::promise<void> *)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<u8> 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 *>(&track);
return nullptr;
}
if (track <= hashDisk->tracks.size())
return const_cast<Track *>(&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<size_t>(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 *>(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<Achievement> Achievements::getAchievementList()
{
std::vector<Achievement> 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<std::string> 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