Files
RAIntegration/src/services/AchievementRuntime.cpp
T

2900 lines
115 KiB
C++

#include "AchievementRuntime.hh"
#include "AchievementRuntimeExports.hh"
#include "Exports.hh"
#include "RA_Defs.h"
#include "RA_Json.h"
#include "RA_Resource.h"
#include "util\EnumOps.hh"
#include "util\Log.hh"
#include "util\Strings.hh"
#include "RA_md5factory.h"
#include "data\context\ConsoleContext.hh"
#include "data\context\EmulatorContext.hh"
#include "data\context\GameContext.hh"
#include "data\context\SessionTracker.hh"
#include "data\context\UserContext.hh"
#include "services\FrameEventQueue.hh"
#include "services\GameIdentifier.hh"
#include "services\Http.hh"
#include "services\IAudioSystem.hh"
#include "services\IConfiguration.hh"
#include "services\IFileSystem.hh"
#include "services\IHttpRequester.hh"
#include "services\ILocalStorage.hh"
#include "services\IThreadPool.hh"
#include "services\ServiceLocator.hh"
#include "services\impl\JsonFileConfiguration.hh"
#include "ui\viewmodels\IntegrationMenuViewModel.hh"
#include "ui\viewmodels\LoginViewModel.hh"
#include "ui\viewmodels\MessageBoxViewModel.hh"
#include "ui\viewmodels\OverlayManager.hh"
#include "ui\viewmodels\PopupMessageViewModel.hh"
#include "ui\viewmodels\UnknownGameViewModel.hh"
#include "ui\viewmodels\WindowManager.hh"
#include <rcheevos\include\rc_api_runtime.h>
#include <rcheevos\include\rc_client_raintegration.h>
#include <rcheevos\src\rapi\rc_api_common.h> // for parsing cached patchdata response
#include <rcheevos\src\rcheevos\rc_internal.h>
#include <rcheevos\src\rc_client_internal.h>
#include <rcheevos\src\rhash\md5.h>
namespace ra {
namespace services {
static std::map<uint32_t, int> s_mAchievementPopups;
static int CanSubmit()
{
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
if (pGameContext.GetMode() == ra::data::context::GameContext::Mode::CompatibilityTest)
return 0;
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline))
return 0;
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
if (pEmulatorContext.WasMemoryModified())
return 0;
if (_RA_HardcoreModeIsActive() && pEmulatorContext.IsMemoryInsecure())
return 0;
return 1;
}
static int CanSubmitAchievementUnlock(uint32_t nAchievementId, rc_client_t*)
{
if (!CanSubmit())
return 0;
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
const auto* pAchievement = pGameContext.Assets().FindAchievement(nAchievementId);
if (pAchievement == nullptr ||
pAchievement->IsModified() ||
pAchievement->GetChanges() != ra::data::models::AssetChanges::None ||
pAchievement->GetCategory() != ra::data::models::AssetCategory::Core)
{
return 0;
}
return 1;
}
static int CanSubmitLeaderboardEntry(uint32_t nLeaderboardId, rc_client_t*)
{
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
return false;
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(nLeaderboardId);
if (pLeaderboard == nullptr ||
pLeaderboard->IsModified() ||
pLeaderboard->GetChanges() != ra::data::models::AssetChanges::None ||
pLeaderboard->GetCategory() != ra::data::models::AssetCategory::Core)
{
return 0;
}
return CanSubmit();
}
static int RichPresenceOverride(rc_client_t*, char buffer[], size_t buffer_size)
{
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
const auto& pWindowManager = ra::services::ServiceLocator::Get<ra::ui::viewmodels::WindowManager>();
const bool bIsInspectingMemory = pWindowManager.MemoryBookmarks.IsVisible() ||
pWindowManager.MemoryInspector.IsVisible() ||
pWindowManager.AssetEditor.IsVisible();
if (bIsInspectingMemory)
{
if (pGameContext.GetMode() == ra::data::context::GameContext::Mode::CompatibilityTest)
return snprintf(buffer, buffer_size, "Testing Compatibility");
if (_RA_HardcoreModeIsActive())
return snprintf(buffer, buffer_size, "Inspecting Memory in Hardcore mode");
const char* sMessage = "Inspecting Memory";
for (const auto& pAsset : pGameContext.Assets())
{
if (!pAsset.IsShownInList())
continue;
if (pAsset.GetCategory() == ra::data::models::AssetCategory::Local)
{
sMessage = "Developing Achievements";
break;
}
if (pAsset.GetChanges() != ra::data::models::AssetChanges::None)
sMessage = "Fixing Achievements";
}
return snprintf(buffer, buffer_size, "%s", sMessage);
}
// don't send text from modified rich presence to server
const auto* pRichPresence = pGameContext.Assets().FindRichPresence();
if (pRichPresence && pRichPresence->GetChanges() != ra::data::models::AssetChanges::None)
return snprintf(buffer, buffer_size, "Playing %s", ra::Narrow(pGameContext.GameTitle()).c_str());
// allow default behavior
return 0;
}
static uint32_t IdentifyUnknownHash(uint32_t console_id, const char* hash, rc_client_t*, void*)
{
RA_LOG_INFO("Could not identify game with hash %s", hash);
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
auto sEstimatedGameTitle = ra::Widen(pEmulatorContext.GetGameTitle());
if (console_id == RC_CONSOLE_UNKNOWN)
console_id = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>().Id();
ra::ui::viewmodels::UnknownGameViewModel vmUnknownGame;
vmUnknownGame.InitializeGameTitles(ra::itoe<ConsoleID>(console_id));
vmUnknownGame.SetSystemName(ra::Widen(rc_console_name(console_id)));
vmUnknownGame.SetChecksum(ra::Widen(hash));
vmUnknownGame.SetEstimatedGameName(sEstimatedGameTitle);
vmUnknownGame.SetNewGameName(sEstimatedGameTitle);
if (vmUnknownGame.ShowModal() == ra::ui::DialogResult::OK)
{
if (vmUnknownGame.GetTestMode())
{
ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>().SetMode(
ra::data::context::GameContext::Mode::CompatibilityTest);
}
return vmUnknownGame.GetSelectedGameId();
}
return 0;
}
AchievementRuntime::AchievementRuntime()
{
m_pClient.reset(rc_client_create(AchievementRuntime::ReadMemory, AchievementRuntime::ServerCallAsync));
m_pClient->callbacks.can_submit_achievement_unlock = CanSubmitAchievementUnlock;
m_pClient->callbacks.can_submit_leaderboard_entry = CanSubmitLeaderboardEntry;
m_pClient->callbacks.rich_presence_override = RichPresenceOverride;
m_pClient->callbacks.identify_unknown_hash = IdentifyUnknownHash;
#ifndef RA_UTEST
rc_client_enable_logging(m_pClient.get(), RC_CLIENT_LOG_LEVEL_VERBOSE, AchievementRuntime::LogMessage);
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.IsCustomHost())
rc_client_set_host(m_pClient.get(), pConfiguration.GetHostUrl().c_str());
const auto bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
rc_client_set_hardcore_enabled(m_pClient.get(), bHardcore ? 1 : 0);
if (pConfiguration.IsCustomHost())
rc_client_set_host(m_pClient.get(), pConfiguration.GetHostUrl().c_str());
#endif
rc_client_set_event_handler(m_pClient.get(), EventHandler);
m_pClient->state.allow_leaderboards_in_softcore = true;
rc_client_set_unofficial_enabled(m_pClient.get(), 1);
}
AchievementRuntime::~AchievementRuntime() { Shutdown(); }
static void DummyEventHandler(const rc_client_event_t*, rc_client_t*) noexcept
{
}
void AchievementRuntime::Shutdown() noexcept
{
if (m_pClient != nullptr)
{
// don't need to handle events while shutting down
rc_client_set_event_handler(m_pClient.get(), DummyEventHandler);
rc_client_destroy(m_pClient.release());
}
}
void AchievementRuntime::LogMessage(const char* sMessage, const rc_client_t*)
{
const auto& pLogger = ra::services::ServiceLocator::Get<ra::services::ILogger>();
if (pLogger.IsEnabled(ra::services::LogLevel::Info))
pLogger.LogMessage(ra::services::LogLevel::Info, sMessage);
}
uint32_t AchievementRuntime::ReadMemory(uint32_t nAddress, uint8_t* pBuffer, uint32_t nBytes, rc_client_t*)
{
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
return pEmulatorContext.ReadMemory(nAddress, pBuffer, nBytes);
}
static void ConvertHttpResponseToApiServerResponse(rc_api_server_response_t& pResponse,
const ra::services::Http::Response& httpResponse,
std::string& sErrorBuffer)
{
memset(&pResponse, 0, sizeof(pResponse));
pResponse.http_status_code = ra::etoi(httpResponse.StatusCode());
pResponse.body = httpResponse.Content().c_str();
pResponse.body_length = httpResponse.Content().length();
if (pResponse.http_status_code > 599 || pResponse.body_length == 0)
{
const auto& pHttpRequestService = ra::services::ServiceLocator::Get<ra::services::IHttpRequester>();
sErrorBuffer = pHttpRequestService.GetStatusCodeText(pResponse.http_status_code);
pResponse.body = sErrorBuffer.c_str();
pResponse.body_length = sErrorBuffer.length();
pResponse.http_status_code = pHttpRequestService.IsRetryable(pResponse.http_status_code) ?
RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR : RC_API_SERVER_RESPONSE_CLIENT_ERROR;
}
}
static std::string GetParam(const ra::services::Http::Request& httpRequest, const std::string& sParam)
{
std::string sScan = "&" + sParam + "=";
auto nIndex = httpRequest.GetPostData().find(sScan);
if (nIndex == std::string::npos)
{
const auto nLen = sScan.length() - 1;
if (httpRequest.GetPostData().length() < nLen)
return "";
if (httpRequest.GetPostData().compare(0, nLen, sScan, 1, nLen) != 0)
return "";
nIndex = nLen;
}
else
{
nIndex += sScan.length();
}
auto nIndex2 = httpRequest.GetPostData().find('&', nIndex);
if (nIndex2 == std::string::npos)
nIndex2 = httpRequest.GetPostData().length();
return std::string(httpRequest.GetPostData(), nIndex, nIndex2 - nIndex);
}
static ra::services::Http::Response AchievementDataNotFound(const std::string& sGameId)
{
return ra::services::Http::Response(ra::services::Http::StatusCode::NotFound,
ra::StringPrintf("{\"Success\":false,\"Error\":\"Achievement data for game %s not found in cache\"}", sGameId));
}
static ra::services::Http::Response HandleOfflineRequest(const ra::services::Http::Request& httpRequest, const std::string sApi)
{
if (sApi == "ping")
return ra::services::Http::Response(ra::services::Http::StatusCode::OK, "{\"Success\":true}");
if (sApi == "achievementsets")
{
const auto sGameHash = GetParam(httpRequest, "m");
const auto nGameId = ra::services::ServiceLocator::GetMutable<ra::services::GameIdentifier>().IdentifyHash(sGameHash);
// see if the data is available in the cache
auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
auto pData = pLocalStorage.ReadText(ra::services::StorageItemType::GameData, std::to_wstring(nGameId));
if (pData == nullptr)
return AchievementDataNotFound(std::to_string(nGameId));
const auto nSize = pData->GetSize();
std::string sContents;
sContents.resize(nSize + 1);
GSL_SUPPRESS_TYPE1 pData->GetBytes(reinterpret_cast<uint8_t*>(sContents.data()), nSize);
if (sContents.find(",\"Sets\":[") == std::string::npos) // ignore patch response - only return achievementsets response
return AchievementDataNotFound(std::to_string(nGameId));
return ra::services::Http::Response(ra::services::Http::StatusCode::OK, sContents);
}
if (sApi == "patch")
{
const auto sGameId = GetParam(httpRequest, "g");
// see if the data is available in the cache
auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
auto pData = pLocalStorage.ReadText(ra::services::StorageItemType::GameData, ra::Widen(sGameId));
if (pData == nullptr)
return AchievementDataNotFound(sGameId);
std::string sContents = "{\"Success\":true,\"PatchData\":";
std::string sLine;
while (pData->GetLine(sLine))
sContents.append(sLine);
sContents.push_back('}');
return ra::services::Http::Response(ra::services::Http::StatusCode::OK, sContents);
}
if (sApi == "startsession")
return ra::services::Http::Response(ra::services::Http::StatusCode::OK, "{\"Success\":true}");
return ra::services::Http::Response(ra::services::Http::StatusCode::NotImplemented,
ra::StringPrintf("{\"Success\":false,\"Error\":\"No offline implementation for %s\"}", sApi));
}
void AchievementRuntime::AsyncServerCall(const rc_api_request_t* pRequest,
AsyncServerCallCallback fCallback, void* pCallbackData) const
{
struct callback_pair_t
{
AsyncServerCallCallback fCallback;
void* pCallbackData;
};
auto* pCallbackPair = static_cast<callback_pair_t*>(malloc(sizeof(callback_pair_t)));
Expects(pCallbackPair != nullptr);
pCallbackPair->fCallback = fCallback;
pCallbackPair->pCallbackData = pCallbackData;
ServerCallAsync(pRequest,
[](const rc_api_server_response_t* server_response, void* callback_data)
{
Expects(server_response != nullptr);
Expects(callback_data != nullptr);
auto* pCallbackPair = static_cast<callback_pair_t*>(callback_data);
pCallbackPair->fCallback(*server_response, pCallbackPair->pCallbackData);
free(pCallbackPair);
},
pCallbackPair, nullptr);
}
void AchievementRuntime::ServerCallAsync(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback,
void* pCallbackData, rc_client_t*)
{
ra::services::Http::Request httpRequest(pRequest->url);
httpRequest.SetPostData(pRequest->post_data);
httpRequest.SetContentType(pRequest->content_type);
std::string sApi;
auto nIndex = httpRequest.GetPostData().find("r=", 0, 2);
if (nIndex != std::string::npos)
{
nIndex += 2;
for (; nIndex < httpRequest.GetPostData().length(); ++nIndex)
{
const char c = httpRequest.GetPostData().at(nIndex);
if (c == '&')
break;
sApi.push_back(c);
}
std::string sParams;
bool redacted = false;
for (const char c : httpRequest.GetPostData())
{
if (c == '=')
{
const auto param = sParams.back();
redacted = (param == 't') || (param == 'p' && ra::StringStartsWith(sApi, "login"));
if (redacted)
{
sParams.append("=[redacted]");
continue;
}
}
else if (c == '&')
{
redacted = false;
}
else if (redacted)
{
continue;
}
sParams.push_back(c);
}
RA_LOG_INFO(">> %s request: %s", sApi.c_str(), sParams.c_str());
}
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline))
{
ra::services::ServiceLocator::GetMutable<ra::services::IThreadPool>().RunAsync(
[httpRequest = std::move(httpRequest), fCallback, pCallbackData, sApi]()
{
ra::services::Http::Response httpResponse = HandleOfflineRequest(httpRequest, sApi);
RA_LOG_INFO("<< %s response (offline) (%d): %s", sApi.c_str(), ra::etoi(httpResponse.StatusCode()), httpResponse.Content().c_str());
rc_api_server_response_t pResponse;
std::string sErrorBuffer;
ConvertHttpResponseToApiServerResponse(pResponse, httpResponse, sErrorBuffer);
fCallback(&pResponse, pCallbackData);
});
}
else
{
httpRequest.CallAsync([fCallback, pCallbackData, sApi](const ra::services::Http::Response& httpResponse)
{
rc_api_server_response_t pResponse;
std::string sErrorBuffer;
ConvertHttpResponseToApiServerResponse(pResponse, httpResponse, sErrorBuffer);
RA_LOG_INFO("<< %s response (%d): %s", sApi.c_str(), ra::etoi(httpResponse.StatusCode()), httpResponse.Content().c_str());
fCallback(&pResponse, pCallbackData);
});
}
}
typedef struct QueueMemoryReadData
{
std::function<void()> fCallback;
} QueueMemoryReadData;
static void DispatchMemoryRead(struct rc_client_scheduled_callback_data_t* callback_data,
rc_client_t*, rc_clock_t)
{
QueueMemoryReadData* data = static_cast<QueueMemoryReadData*>(callback_data->data);
data->fCallback();
delete data;
free(callback_data);
}
void AchievementRuntime::QueueMemoryRead(std::function<void()>&& fCallback) const
{
if (GetClient()->state.allow_background_memory_reads)
{
fCallback();
return;
}
if (m_hDoFrameThread == 0 || IsOnDoFrameThread())
{
fCallback();
return;
}
QueueMemoryReadData* data = new QueueMemoryReadData();
Expects(data != nullptr);
data->fCallback = std::move(fCallback);
rc_client_scheduled_callback_data_t* scheduled_callback =
static_cast<rc_client_scheduled_callback_data_t*>(calloc(1, sizeof(rc_client_scheduled_callback_data_t)));
Expects(scheduled_callback != nullptr);
scheduled_callback->callback = DispatchMemoryRead;
scheduled_callback->data = data;
rc_client_schedule_callback(GetClient(), scheduled_callback);
// We have to assume the client will call rc_client_do_frame or rc_client_idle in a
// timely manner or the callback won't get called and the UI will appear unresponsive.
}
/* ---- ClientSynchronizer ----- */
class AchievementRuntime::ClientSynchronizer
{
public:
ClientSynchronizer() = default;
virtual ~ClientSynchronizer()
{
m_pPublishedSubset = nullptr;
for (auto* pMemory : m_vAllocatedMemory)
free(pMemory);
}
ClientSynchronizer(const ClientSynchronizer&) noexcept = delete;
ClientSynchronizer& operator=(const ClientSynchronizer&) noexcept = delete;
ClientSynchronizer(ClientSynchronizer&&) noexcept = delete;
ClientSynchronizer& operator=(ClientSynchronizer&&) noexcept = delete;
private:
rc_client_subset_info_t* m_pPublishedSubset = nullptr;
rc_client_t* m_pClient = nullptr;
typedef struct rc_client_subset_wrapper_t
{
std::vector<rc_client_subset_info_t> pCoreSubset;
std::vector<rc_client_subset_info_t> pLocalSubset;
rc_buffer_t pBuffer{};
std::vector<void*> vAllocatedMemory;
} SubsetWrapper;
std::unique_ptr<SubsetWrapper> m_pSubsetWrapper;
std::vector<void*> m_vAllocatedMemory;
static bool DetachMemory(std::vector<void*>& pAllocatedMemory, void* pMemory) noexcept
{
for (auto pIter = pAllocatedMemory.begin(); pIter != pAllocatedMemory.end(); ++pIter)
{
if (*pIter == pMemory)
{
pAllocatedMemory.erase(pIter);
return true;
}
}
return false;
}
static rc_client_leaderboard_info_t* FindLeaderboard(rc_client_subset_info_t* pSubset, uint32_t nId)
{
Expects(pSubset != nullptr);
auto* pLeaderboard = pSubset->leaderboards;
const auto* pLeaderboardStop = pLeaderboard + pSubset->public_.num_leaderboards;
for (; pLeaderboard < pLeaderboardStop; ++pLeaderboard)
{
if (pLeaderboard->public_.id == nId)
return pLeaderboard;
}
return nullptr;
}
static rc_client_achievement_info_t* FindAchievement(rc_client_subset_info_t* pSubset, uint32_t nId)
{
Expects(pSubset != nullptr);
auto* pAchievement = pSubset->achievements;
const auto* pAchievementStop = pAchievement + pSubset->public_.num_achievements;
for (; pAchievement < pAchievementStop; ++pAchievement)
{
if (pAchievement->public_.id == nId)
return pAchievement;
}
return nullptr;
}
void SyncSubset(rc_client_subset_info_t* pSubset, SubsetWrapper* pSubsetWrapper,
std::vector<ra::data::models::AchievementModel*>& vAchievements,
std::vector<ra::data::models::LeaderboardModel*>& vLeaderboards)
{
pSubset->active = true;
if (!vAchievements.empty())
{
pSubset->achievements = static_cast<rc_client_achievement_info_t*>(
rc_buffer_alloc(&pSubsetWrapper->pBuffer, vAchievements.size() * sizeof(*pSubset->achievements)));
Expects(pSubset->achievements != nullptr);
memset(pSubset->achievements, 0, vAchievements.size() * sizeof(*pSubset->achievements));
rc_client_achievement_info_t* pAchievement = pSubset->achievements;
Expects(pAchievement != nullptr);
rc_client_achievement_info_t* pSrcAchievement = nullptr;
for (auto* vmAchievement : vAchievements)
{
Expects(vmAchievement != nullptr);
const auto nAchievementId = vmAchievement->GetID();
if (vmAchievement->GetChanges() == ra::data::models::AssetChanges::None)
{
// reset to server state
rc_client_subset_info_t* pPublishedSubset = m_pPublishedSubset;
for (; pPublishedSubset; pPublishedSubset = pPublishedSubset->next)
{
pSrcAchievement = FindAchievement(pPublishedSubset, nAchievementId);
if (pSrcAchievement != nullptr)
{
memcpy(pAchievement, pSrcAchievement, sizeof(*pAchievement));
vmAchievement->ReplaceAttached(*pAchievement++);
break;
}
}
}
else
{
pSrcAchievement = nullptr;
if (m_pSubsetWrapper != nullptr)
{
if (vmAchievement->GetCategory() == ra::data::models::AssetCategory::Local)
{
for (auto& pLocalSubset : m_pSubsetWrapper->pLocalSubset)
{
pSrcAchievement = FindAchievement(&pLocalSubset, nAchievementId);
if (pSrcAchievement)
break;
}
}
else
{
for (auto& pCoreSubset : m_pSubsetWrapper->pCoreSubset)
{
pSrcAchievement = FindAchievement(&pCoreSubset, nAchievementId);
if (pSrcAchievement)
break;
}
}
}
if (pSrcAchievement != nullptr && pSrcAchievement == vmAchievement->GetAttached())
{
// transfer the trigger ownership to the new SubsetWrapper
if (pSrcAchievement->trigger && DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pSrcAchievement->trigger))
pSubsetWrapper->vAllocatedMemory.push_back(pSrcAchievement->trigger);
memcpy(pAchievement, pSrcAchievement, sizeof(*pAchievement));
vmAchievement->ReplaceAttached(*pAchievement);
}
else
{
if (pSrcAchievement != nullptr && pSrcAchievement->trigger)
{
const auto& sTrigger = vmAchievement->GetTrigger();
uint8_t md5[16];
rc_runtime_checksum(sTrigger.c_str(), md5);
if (memcmp(pSrcAchievement->md5, md5, sizeof(md5)) == 0)
{
memcpy(pAchievement->md5, pSrcAchievement->md5, sizeof(md5));
pAchievement->trigger = pSrcAchievement->trigger;
// transfer the trigger ownership to the new SubsetWrapper
if (DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pSrcAchievement->trigger))
pSubsetWrapper->vAllocatedMemory.push_back(pSrcAchievement->trigger);
}
}
// no rc_client_achievement_t found for this achievement, populate from the model
vmAchievement->AttachAndInitialize(*pAchievement);
}
// have to generate local urls for local images
if (pAchievement->public_.badge_name[0] == 'L')
{
const auto& pImageRepository = ra::services::ServiceLocator::Get<ra::ui::IImageRepository>();
auto sUrl = ra::StringPrintf("file://%s",
pImageRepository.GetFilename(ra::ui::ImageType::Badge, ra::Narrow(vmAchievement->GetBadge())));
std::replace(sUrl.begin(), sUrl.end(), '\\', '/');
pAchievement->public_.badge_url = pAchievement->public_.badge_locked_url =
rc_buffer_strncpy(&pSubsetWrapper->pBuffer, sUrl.c_str(), sUrl.length());
}
++pAchievement;
}
}
pSubset->public_.num_achievements = gsl::narrow_cast<uint32_t>(pAchievement - pSubset->achievements);
}
if (!vLeaderboards.empty())
{
pSubset->leaderboards = static_cast<rc_client_leaderboard_info_t*>(
rc_buffer_alloc(&pSubsetWrapper->pBuffer, vLeaderboards.size() * sizeof(*pSubset->leaderboards)));
memset(pSubset->leaderboards, 0, vLeaderboards.size() * sizeof(*pSubset->leaderboards));
rc_client_leaderboard_info_t* pLeaderboard = pSubset->leaderboards;
rc_client_leaderboard_info_t* pSrcLeaderboard = nullptr;
for (auto* vmLeaderboard : vLeaderboards)
{
Expects(vmLeaderboard != nullptr);
const auto nLeaderboardId = vmLeaderboard->GetID();
if (vmLeaderboard->GetChanges() == ra::data::models::AssetChanges::None)
{
// reset to server state
rc_client_subset_info_t* pPublishedSubset = m_pPublishedSubset;
for (; pPublishedSubset; pPublishedSubset = pPublishedSubset->next)
{
pSrcLeaderboard = FindLeaderboard(pPublishedSubset, nLeaderboardId);
if (pSrcLeaderboard != nullptr)
{
memcpy(pLeaderboard, pSrcLeaderboard, sizeof(*pLeaderboard));
// if the leaderboard is running, we have to also copy the current value and tracker reference
if (pSrcLeaderboard->lboard->state == RC_LBOARD_STATE_STARTED)
{
pPublishedSubset = m_pClient->game->subsets;
for (; pPublishedSubset; pPublishedSubset = pPublishedSubset->next)
{
pSrcLeaderboard = FindLeaderboard(pPublishedSubset, nLeaderboardId);
if (pSrcLeaderboard != nullptr)
{
if (pSrcLeaderboard->lboard == pLeaderboard->lboard)
{
pLeaderboard->tracker = pSrcLeaderboard->tracker;
pLeaderboard->value = pSrcLeaderboard->value;
}
else
{
rc_client_release_leaderboard_tracker(m_pClient->game, pSrcLeaderboard);
}
break;
}
}
}
vmLeaderboard->ReplaceAttached(*pLeaderboard++);
break;
}
}
}
else
{
pSrcLeaderboard = nullptr;
if (m_pSubsetWrapper != nullptr && vmLeaderboard->GetAttached() != nullptr)
{
if (vmLeaderboard->GetCategory() == ra::data::models::AssetCategory::Local)
{
for (auto& pLocalSubset : m_pSubsetWrapper->pLocalSubset)
{
pSrcLeaderboard = FindLeaderboard(&pLocalSubset, nLeaderboardId);
if (pSrcLeaderboard)
break;
}
}
else
{
for (auto& pCoreSubset : m_pSubsetWrapper->pCoreSubset)
{
pSrcLeaderboard = FindLeaderboard(&pCoreSubset, nLeaderboardId);
if (pSrcLeaderboard)
break;
}
}
}
if (pSrcLeaderboard != nullptr && pSrcLeaderboard == vmLeaderboard->GetAttached())
{
// transfer the lboard ownership to the new SubsetWrapper
if (pSrcLeaderboard->lboard && DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pSrcLeaderboard->lboard))
pSubsetWrapper->vAllocatedMemory.push_back(pSrcLeaderboard->lboard);
memcpy(pLeaderboard, pSrcLeaderboard, sizeof(*pLeaderboard));
vmLeaderboard->ReplaceAttached(*pLeaderboard++);
}
else
{
if (pSrcLeaderboard != nullptr && pSrcLeaderboard->lboard)
{
const auto& sTrigger = vmLeaderboard->GetDefinition();
uint8_t md5[16];
rc_runtime_checksum(sTrigger.c_str(), md5);
if (memcmp(pSrcLeaderboard->md5, md5, sizeof(md5)) == 0)
{
memcpy(pLeaderboard->md5, pSrcLeaderboard->md5, sizeof(md5));
pLeaderboard->lboard = pSrcLeaderboard->lboard;
// transfer the trigger ownership to the new SubsetWrapper
if (DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pSrcLeaderboard->lboard))
pSubsetWrapper->vAllocatedMemory.push_back(pSrcLeaderboard->lboard);
}
}
// no rc_client_leaderboard_t found for this leaderboard, populate from the model
vmLeaderboard->AttachAndInitialize(*pLeaderboard++);
}
}
}
pSubset->public_.num_leaderboards = gsl::narrow_cast<uint32_t>(pLeaderboard - pSubset->leaderboards);
}
}
void BuildSubsetWrapper(SubsetWrapper& pSubsetWrapper,
rc_client_subset_info_t& pCoreSubsetWrapper,
rc_client_subset_info_t& pLocalSubsetWrapper,
const rc_client_subset_info_t* pSubset, uint32_t nSubsetId,
ra::data::context::GameAssets& vAssets)
{
std::vector<ra::data::models::AchievementModel*> vCoreAchievements;
std::vector<ra::data::models::AchievementModel*> vLocalAchievements;
std::vector<ra::data::models::LeaderboardModel*> vCoreLeaderboards;
std::vector<ra::data::models::LeaderboardModel*> vLocalLeaderboards;
for (auto& pAsset : vAssets)
{
if (pAsset.GetSubsetID() != nSubsetId)
continue;
if (pAsset.GetChanges() == ra::data::models::AssetChanges::Deleted)
continue;
auto* vmAchievement = dynamic_cast<ra::data::models::AchievementModel*>(&pAsset);
if (vmAchievement != nullptr)
{
if (vmAchievement->GetCategory() == ra::data::models::AssetCategory::Local)
vLocalAchievements.push_back(vmAchievement);
else
vCoreAchievements.push_back(vmAchievement);
continue;
}
auto* vmLeaderboard = dynamic_cast<ra::data::models::LeaderboardModel*>(&pAsset);
if (vmLeaderboard != nullptr)
{
if (vmLeaderboard->GetCategory() == ra::data::models::AssetCategory::Local)
vLocalLeaderboards.push_back(vmLeaderboard);
else
vCoreLeaderboards.push_back(vmLeaderboard);
continue;
}
}
memset(&pCoreSubsetWrapper, 0, sizeof(pCoreSubsetWrapper));
memset(&pLocalSubsetWrapper, 0, sizeof(pLocalSubsetWrapper));
pCoreSubsetWrapper.public_.id = pSubset->public_.id;
pCoreSubsetWrapper.public_.title = pSubset->public_.title;
snprintf(pCoreSubsetWrapper.public_.badge_name,
sizeof(pCoreSubsetWrapper.public_.badge_name), "%s", pSubset->public_.badge_name);
SyncSubset(&pCoreSubsetWrapper, &pSubsetWrapper, vCoreAchievements, vCoreLeaderboards);
pLocalSubsetWrapper.public_.id = ra::data::context::GameAssets::LocalSubsetId;
pLocalSubsetWrapper.public_.title = "Local";
snprintf(pLocalSubsetWrapper.public_.badge_name, sizeof(pLocalSubsetWrapper.public_.badge_name), "%s",
pSubset->public_.badge_name);
if (!vLocalAchievements.empty() || !vLocalLeaderboards.empty())
SyncSubset(&pLocalSubsetWrapper, &pSubsetWrapper, vLocalAchievements, vLocalLeaderboards);
else
pLocalSubsetWrapper.active = false;
pCoreSubsetWrapper.next = &pLocalSubsetWrapper;
}
public:
void SyncAssets(rc_client_t* pClient)
{
if (!pClient->game)
return;
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
auto& vAssets = pGameContext.Assets();
if (m_pClient == nullptr)
{
m_pClient = pClient;
m_pPublishedSubset = pClient->game->subsets;
}
Expects(m_pPublishedSubset != nullptr);
std::unique_ptr<SubsetWrapper> pSubsetWrapper;
pSubsetWrapper.reset(new SubsetWrapper);
rc_buffer_init(&pSubsetWrapper->pBuffer);
size_t nSubsets = 0;
for (auto* pSubset = m_pPublishedSubset; pSubset; pSubset = pSubset->next)
nSubsets++;
pSubsetWrapper->pCoreSubset.resize(nSubsets);
pSubsetWrapper->pLocalSubset.resize(nSubsets);
BuildSubsetWrapper(*pSubsetWrapper, pSubsetWrapper->pCoreSubset.at(0), pSubsetWrapper->pLocalSubset.at(0),
m_pPublishedSubset, m_pPublishedSubset->public_.id, vAssets);
nSubsets = 1;
for (auto* pSubset = m_pPublishedSubset->next; pSubset; pSubset = pSubset->next, nSubsets++)
{
BuildSubsetWrapper(*pSubsetWrapper, pSubsetWrapper->pCoreSubset.at(nSubsets),
pSubsetWrapper->pLocalSubset.at(nSubsets), pSubset, pSubset->public_.id, vAssets);
pSubsetWrapper->pLocalSubset.at(nSubsets - 1).next = &pSubsetWrapper->pCoreSubset.at(nSubsets);
}
std::swap(pSubsetWrapper, m_pSubsetWrapper);
if (pSubsetWrapper != nullptr)
{
for (auto* pMemory : pSubsetWrapper->vAllocatedMemory)
free(pMemory);
}
rc_mutex_lock(&pClient->state.mutex);
pClient->game->subsets = &m_pSubsetWrapper->pCoreSubset.at(0);
rc_mutex_unlock(&pClient->state.mutex);
}
void AttachMemory(void* pMemory)
{
if (m_pSubsetWrapper)
m_pSubsetWrapper->vAllocatedMemory.push_back(pMemory);
else
m_vAllocatedMemory.push_back(pMemory);
}
bool DetachMemory(void* pMemory) noexcept
{
if (!m_pSubsetWrapper)
return false;
return DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pMemory);
}
const rc_client_achievement_info_t* GetPublishedAchievementInfo(ra::AchievementID nId) const
{
for (auto* pSubset = m_pPublishedSubset; pSubset; pSubset = pSubset->next)
{
const auto* pAchievement = FindAchievement(pSubset, nId);
if (pAchievement != nullptr)
return pAchievement;
}
return nullptr;
}
const rc_client_leaderboard_info_t* GetPublishedLeaderboardInfo(ra::LeaderboardID nId) const
{
for (auto* pSubset = m_pPublishedSubset; pSubset; pSubset = pSubset->next)
{
const auto* pLeaderboard = FindLeaderboard(pSubset, nId);
if (pLeaderboard != nullptr)
return pLeaderboard;
}
return nullptr;
}
};
void AchievementRuntime::SyncAssets()
{
if (m_pClientSynchronizer == nullptr)
m_pClientSynchronizer.reset(new ClientSynchronizer());
m_pClientSynchronizer->SyncAssets(GetClient());
}
void AchievementRuntime::AttachMemory(void* pMemory)
{
if (m_pClientSynchronizer != nullptr)
m_pClientSynchronizer->AttachMemory(pMemory);
}
bool AchievementRuntime::DetachMemory(void* pMemory) noexcept
{
if (m_pClientSynchronizer != nullptr)
return m_pClientSynchronizer->DetachMemory(pMemory);
return false;
}
static rc_client_achievement_info_t* GetAchievementInfo(rc_client_t* pClient, ra::AchievementID nId) noexcept
{
rc_client_subset_info_t* subset = pClient->game->subsets;
for (; subset; subset = subset->next)
{
rc_client_achievement_info_t* achievement = subset->achievements;
const rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
for (; achievement < stop; ++achievement)
{
if (achievement->public_.id == nId)
return achievement;
}
}
return nullptr;
}
rc_trigger_t* AchievementRuntime::GetAchievementTrigger(ra::AchievementID nId) const noexcept
{
rc_client_achievement_info_t* achievement = GetAchievementInfo(GetClient(), nId);
return (achievement != nullptr) ? achievement->trigger : nullptr;
}
const rc_client_achievement_info_t* AchievementRuntime::GetPublishedAchievementInfo(ra::AchievementID nId) const
{
if (m_pClientSynchronizer != nullptr)
return m_pClientSynchronizer->GetPublishedAchievementInfo(nId);
return nullptr;
}
const rc_client_leaderboard_info_t* AchievementRuntime::GetPublishedLeaderboardInfo(ra::LeaderboardID nId) const
{
if (m_pClientSynchronizer != nullptr)
return m_pClientSynchronizer->GetPublishedLeaderboardInfo(nId);
return nullptr;
}
std::string AchievementRuntime::GetAchievementBadge(const rc_client_achievement_t& pAchievement)
{
std::string sBadgeName = pAchievement.badge_name;
if (!sBadgeName.empty() && sBadgeName.front() == 'L')
{
// local image, get from model
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
auto* vmLocalAchievement = pGameContext.Assets().FindAchievement(pAchievement.id);
if (vmLocalAchievement != nullptr)
sBadgeName = ra::Narrow(vmLocalAchievement->GetBadge());
}
return sBadgeName;
}
void AchievementRuntime::RaiseClientEvent(rc_client_achievement_info_t& pAchievement, uint32_t nEventType) const noexcept
{
auto* client = GetClient();
if (client->game && client->callbacks.event_handler)
{
rc_client_event_t pEvent;
memset(&pEvent, 0, sizeof(pEvent));
pEvent.achievement = &pAchievement.public_;
pEvent.type = nEventType;
client->callbacks.event_handler(&pEvent, client);
}
}
void AchievementRuntime::UpdateActiveAchievements()
{
auto* client = GetClient();
if (client->game)
{
rc_mutex_lock(&client->state.mutex);
if (client->game && (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) == 0)
{
rc_client_update_active_achievements(client->game);
#ifndef RA_UTEST
// locked badges are only retrieved at startup if the user hasn't earned the achievement
// when manually activating an achievement, make sure the locked badge is available.
auto& pImageRepository = ra::services::ServiceLocator::GetMutable<ra::ui::IImageRepository>();
const auto* pSubset = client->game->subsets;
for (; pSubset; pSubset = pSubset->next) {
const auto* pAchievement = pSubset->achievements;
const auto* pAchievementStop = pAchievement + pSubset->public_.num_achievements;
for (; pAchievement < pAchievementStop; ++pAchievement)
{
if (pAchievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED &&
pAchievement->public_.badge_name[0] != 'L') // ignore local images
{
pImageRepository.FetchImage(ra::ui::ImageType::Badge,
std::string(pAchievement->public_.badge_name) + "_lock",
pAchievement->public_.badge_locked_url ? pAchievement->public_.badge_locked_url : "");
}
}
}
#endif
}
rc_mutex_unlock(&client->state.mutex);
}
}
void AchievementRuntime::UpdateActiveLeaderboards() noexcept
{
auto* client = GetClient();
if (client->game)
{
rc_mutex_lock(&client->state.mutex);
if (client->game)
rc_client_update_active_leaderboards(client->game);
rc_mutex_unlock(&client->state.mutex);
}
}
static rc_client_leaderboard_info_t* GetLeaderboardInfo(rc_client_t* pClient, ra::LeaderboardID nId) noexcept
{
if (!pClient || !pClient->game)
return nullptr;
rc_client_subset_info_t* subset = pClient->game->subsets;
for (; subset; subset = subset->next)
{
rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
const rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
for (; leaderboard < stop; ++leaderboard)
{
if (leaderboard->public_.id == nId)
return leaderboard;
}
}
return nullptr;
}
rc_lboard_t* AchievementRuntime::GetLeaderboardDefinition(ra::LeaderboardID nId) const noexcept
{
rc_client_leaderboard_info_t* leaderboard = GetLeaderboardInfo(GetClient(), nId);
return (leaderboard != nullptr) ? leaderboard->lboard : nullptr;
}
void AchievementRuntime::ReleaseLeaderboardTracker(ra::LeaderboardID nId) noexcept
{
auto* pClient = GetClient();
rc_client_leaderboard_info_t* leaderboard = GetLeaderboardInfo(pClient, nId);
if (leaderboard)
{
rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker;
if (tracker)
{
rc_client_release_leaderboard_tracker(pClient->game, leaderboard);
if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE)
{
rc_client_event_t pEvent;
memset(&pEvent, 0, sizeof(pEvent));
pEvent.leaderboard_tracker = &tracker->public_;
pEvent.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE;
pClient->callbacks.event_handler(&pEvent, pClient);
tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE;
}
}
}
}
bool AchievementRuntime::HasRichPresence() const noexcept
{
return m_nRichPresenceParseResult != RC_OK || rc_client_has_rich_presence(GetClient());
}
std::wstring AchievementRuntime::GetRichPresenceDisplayString() const
{
if (m_nRichPresenceParseResult != RC_OK)
{
return ra::StringPrintf(L"Parse error %d (line %d): %s", m_nRichPresenceParseResult,
m_nRichPresenceErrorLine, rc_error_str(m_nRichPresenceParseResult));
}
if (!HasRichPresence())
return L"No Rich Presence defined.";
char sRichPresence[256];
if (rc_client_get_rich_presence_message(GetClient(), sRichPresence, sizeof(sRichPresence)) > 0)
return ra::Widen(sRichPresence);
return L"";
}
bool AchievementRuntime::ActivateRichPresence(const std::string& sScript)
{
auto* game = GetClient()->game;
if (!game)
{
// game still loading - assume success
return true;
}
auto* runtime = &game->runtime;
Expects(runtime != nullptr);
rc_mutex_lock(&GetClient()->state.mutex);
if (sScript.empty())
{
m_nRichPresenceParseResult = RC_OK;
if (runtime->richpresence != nullptr)
runtime->richpresence->richpresence = nullptr;
}
else
{
m_nRichPresenceParseResult = rc_runtime_activate_richpresence(runtime, sScript.c_str(), nullptr, 0);
if (m_nRichPresenceParseResult != RC_OK)
m_nRichPresenceParseResult = rc_richpresence_size_lines(sScript.c_str(), &m_nRichPresenceErrorLine);
}
rc_mutex_unlock(&GetClient()->state.mutex);
return (m_nRichPresenceParseResult == RC_OK);
}
/* ---- Login ----- */
void AchievementRuntime::BeginLoginWithPassword(const std::string& sUsername, const std::string& sPassword,
rc_client_callback_t fCallback, void* pCallbackData)
{
GSL_SUPPRESS_R3
auto* pCallbackWrapper = new CallbackWrapper(m_pClient.get(), fCallback, pCallbackData);
BeginLoginWithPassword(sUsername.c_str(), sPassword.c_str(), pCallbackWrapper);
}
rc_client_async_handle_t* AchievementRuntime::BeginLoginWithPassword(const char* sUsername, const char* sPassword,
CallbackWrapper* pCallbackWrapper) noexcept
{
return rc_client_begin_login_with_password(GetClient(), sUsername, sPassword, AchievementRuntime::LoginCallback,
pCallbackWrapper);
}
void AchievementRuntime::BeginLoginWithToken(const std::string& sUsername, const std::string& sApiToken,
rc_client_callback_t fCallback, void* pCallbackData)
{
GSL_SUPPRESS_R3
auto* pCallbackWrapper = new CallbackWrapper(m_pClient.get(), fCallback, pCallbackData);
BeginLoginWithToken(sUsername.c_str(), sApiToken.c_str(), pCallbackWrapper);
}
rc_client_async_handle_t* AchievementRuntime::BeginLoginWithToken(const char* sUsername, const char* sApiToken,
CallbackWrapper* pCallbackWrapper) noexcept
{
return rc_client_begin_login_with_token(GetClient(), sUsername, sApiToken, AchievementRuntime::LoginCallback,
pCallbackWrapper);
}
GSL_SUPPRESS_CON3
void AchievementRuntime::LoginCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata)
{
if (nResult == RC_OK)
{
const auto* user = rc_client_get_user_info(pClient);
if (!user)
{
nResult = RC_INVALID_STATE;
sErrorMessage = "User data not available.";
}
else
{
// initialize the user context
auto& pUserContext = ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>();
if (pUserContext.IsLoginDisabled())
{
nResult = RC_INVALID_STATE;
sErrorMessage = "Login has been disabled.";
}
else
{
pUserContext.Initialize(user->username, user->display_name, user->token);
pUserContext.SetScore(user->score);
ra::ui::viewmodels::LoginViewModel::PostLoginInitialization();
}
}
}
auto* wrapper = static_cast<CallbackWrapper*>(pUserdata);
Expects(wrapper != nullptr);
wrapper->DoCallback(nResult, sErrorMessage);
delete wrapper;
}
/* ---- Load Game ----- */
void AchievementRuntime::BeginLoadGame(const std::string& sHash, unsigned id, rc_client_callback_t fCallback,
void* pCallbackData)
{
GSL_SUPPRESS_R3
auto* pCallbackWrapper = new LoadGameCallbackWrapper(m_pClient.get(), fCallback, pCallbackData);
BeginLoadGame(sHash.c_str(), id, pCallbackWrapper);
}
static void ProcessPatchData(const rc_api_server_response_t* server_response,
const rc_api_fetch_game_sets_response_t* game_data_response)
{
Expects(game_data_response != nullptr);
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
// extract the old rich presence script from the last cached server response so we can tell if the
// local value has changed. store it as the local value, and we'll merge in the real local value later.
auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
auto pOldData = pLocalStorage.ReadText(StorageItemType::GameData, std::to_wstring(pGameContext.ActiveGameId()));
if (pOldData != nullptr)
{
rapidjson::Document pDocument;
if (LoadDocument(pDocument, *pOldData) && pDocument.HasMember("RichPresencePatch") &&
pDocument["RichPresencePatch"].IsString())
{
auto* pRichPresence = pGameContext.Assets().FindRichPresence();
Expects(pRichPresence != nullptr);
pRichPresence->SetScript(pDocument["RichPresencePatch"].GetString());
pRichPresence->UpdateLocalCheckpoint();
}
}
// cache the patchdata so it can be used in offline mode or by external tools
auto pData = pLocalStorage.WriteText(StorageItemType::GameData, std::to_wstring(pGameContext.ActiveGameId()));
if (pData != nullptr)
{
std::string sPatchData(server_response->body, server_response->body_length);
pData->Write(sPatchData);
}
}
GSL_SUPPRESS_CON3
void AchievementRuntime::PostProcessGameDataResponse(const rc_api_server_response_t* server_response,
rc_api_fetch_game_sets_response_t* game_data_response,
rc_client_t*, void* pUserdata)
{
auto* wrapper = static_cast<LoadGameCallbackWrapper*>(pUserdata);
Expects(wrapper != nullptr);
Expects(game_data_response != nullptr);
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
auto pRichPresence = std::make_unique<ra::data::models::RichPresenceModel>();
pRichPresence->SetScript(game_data_response->rich_presence_script);
pRichPresence->CreateServerCheckpoint();
pRichPresence->CreateLocalCheckpoint();
pGameContext.Assets().Append(std::move(pRichPresence));
#ifndef RA_UTEST
// prefetch the game icon
auto& pImageRepository = ra::services::ServiceLocator::GetMutable<ra::ui::IImageRepository>();
pImageRepository.FetchImage(ra::ui::ImageType::Icon, game_data_response->image_name, game_data_response->image_url);
#endif
for (uint32_t i = 0; i < game_data_response->num_sets; ++i)
{
const auto* pSet = &game_data_response->sets[i];
#ifndef RA_UTEST
// prefetch the subset icon
pImageRepository.FetchImage(ra::ui::ImageType::Icon, pSet->image_name, pSet->image_url);
#endif
const auto* pAchievement = pSet->achievements;
const auto* pAchievementStop = pAchievement + pSet->num_achievements;
for (; pAchievement < pAchievementStop; ++pAchievement)
wrapper->m_mAchievementDefinitions[pAchievement->id] = pAchievement->definition;
const auto* pLeaderboard = pSet->leaderboards;
const auto* pLeaderboardStop = pLeaderboard + pSet->num_leaderboards;
for (; pLeaderboard < pLeaderboardStop; ++pLeaderboard)
wrapper->m_mLeaderboardDefinitions[pLeaderboard->id] = pLeaderboard->definition;
}
pGameContext.InitializeSubsets(game_data_response);
// don't write virtual game data to disc
if (!ra::data::context::GameContext::IsVirtualGameId(game_data_response->id))
ProcessPatchData(server_response, game_data_response);
}
rc_client_async_handle_t* AchievementRuntime::BeginLoadGame(const char* sHash, unsigned id,
CallbackWrapper* pCallbackWrapper) noexcept
{
auto* client = GetClient();
// unload the game and free any additional memory we allocated for local changes
UnloadGame();
// if an ID was provided, store the hash->id mapping
if (id != 0)
{
auto* client_hash = rc_client_find_game_hash(client, sHash);
if (ra::to_signed(client_hash->game_id) < 0)
client_hash->game_id = id;
}
// start the load process
client->callbacks.post_process_game_sets_response = PostProcessGameDataResponse;
return rc_client_begin_load_game(client, sHash, AchievementRuntime::LoadGameCallback, pCallbackWrapper);
}
rc_client_async_handle_t* AchievementRuntime::BeginIdentifyAndLoadGame(uint32_t console_id, const char* file_path,
const uint8_t* data, size_t data_size,
CallbackWrapper* pCallbackWrapper) noexcept
{
auto* client = GetClient();
// unload the game and free any additional memory we allocated for local changes
rc_client_unload_game(client);
m_pClientSynchronizer.reset();
s_mAchievementPopups.clear();
// start the load process
client->callbacks.post_process_game_sets_response = PostProcessGameDataResponse;
return rc_client_begin_identify_and_load_game(client, console_id, file_path, data, data_size,
AchievementRuntime::LoadGameCallback, pCallbackWrapper);
}
GSL_SUPPRESS_CON3
void AchievementRuntime::LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata)
{
auto* wrapper = static_cast<LoadGameCallbackWrapper*>(pUserdata);
Expects(wrapper != nullptr);
if (nResult == RC_OK || nResult == RC_NO_GAME_LOADED)
{
if (pClient->game && ra::data::context::GameContext::IsVirtualGameId(pClient->game->public_.id))
{
ra::ui::viewmodels::UnknownGameViewModel vmUnknownGame;
const auto nGameId = ra::data::context::GameContext::GetRealGameId(pClient->game->public_.id);
const auto nHashCompatibility = ra::data::context::GameContext::GetHashCompatibility(pClient->game->public_.id);
if (nHashCompatibility == ra::data::context::GameContext::HashCompatibility::Untested)
vmUnknownGame.SetProblemHeader(L"The compatibility of the provided game is unknown.");
else
vmUnknownGame.SetProblemHeader(L"The provided game has been marked as incompatible.");
RA_LOG_INFO("Hash %s identified as unsupported for game %u", pClient->game->public_.hash, nGameId);
// we don't have the actual game title, so we can't just call InitializeTestCompatibilityMode
vmUnknownGame.InitializeGameTitles(ra::itoe<ConsoleID>(pClient->game->public_.console_id));
vmUnknownGame.SetSystemName(ra::Widen(rc_console_name(pClient->game->public_.console_id)));
vmUnknownGame.SetChecksum(ra::Widen(pClient->game->public_.hash));
vmUnknownGame.SetSelectedGameId(nGameId);
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
auto sEstimatedGameTitle = ra::Widen(pEmulatorContext.GetGameTitle());
vmUnknownGame.SetEstimatedGameName(sEstimatedGameTitle);
if (vmUnknownGame.ShowModal() == ra::ui::DialogResult::OK)
{
if (vmUnknownGame.GetTestMode())
{
ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>().SetMode(
ra::data::context::GameContext::Mode::CompatibilityTest);
auto& pRuntime = ra::services::ServiceLocator::GetMutable<ra::services::AchievementRuntime>();
// create the virtual association so rc_client will fetch by game id instead of by hash
auto* client_hash = rc_client_find_game_hash(pRuntime.GetClient(), pClient->game->public_.hash);
client_hash->game_id = nGameId;
client_hash->is_unknown = 1;
pRuntime.BeginLoadGame(pClient->game->public_.hash, nGameId, wrapper);
return;
}
}
}
// this has to be done before calling InitializeFromAchievementRuntime so the address validation
// doesn't flag every achievement as invalid.
if (IsExternalRcheevosClient() && pClient->game)
{
const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
if (pConsoleContext.Id() != ra::itoe<ConsoleID>(pClient->game->public_.console_id) &&
pClient->game->public_.console_id != 0)
{
_RA_SetConsoleID(pClient->game->public_.console_id);
}
}
// initialize the game context
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
pGameContext.InitializeFromAchievementRuntime(wrapper->m_mAchievementDefinitions,
wrapper->m_mLeaderboardDefinitions);
}
else
{
}
wrapper->DoCallback(nResult, sErrorMessage);
delete wrapper;
}
rc_client_async_handle_t* AchievementRuntime::BeginIdentifyAndChangeMedia(const char* file_path,
const uint8_t* data, size_t data_size, CallbackWrapper* pCallbackWrapper) noexcept
{
auto* client = GetClient();
return rc_client_begin_identify_and_change_media(client, file_path, data, data_size, AchievementRuntime::ChangeMediaCallback, pCallbackWrapper);
}
rc_client_async_handle_t* AchievementRuntime::BeginChangeMedia(const char* sHash, CallbackWrapper* pCallbackWrapper) noexcept
{
auto* client = GetClient();
return rc_client_begin_change_media(client, sHash, AchievementRuntime::ChangeMediaCallback, pCallbackWrapper);
}
GSL_SUPPRESS_CON3
void AchievementRuntime::ChangeMediaCallback(int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata)
{
auto* wrapper = static_cast<CallbackWrapper*>(pUserdata);
Expects(wrapper != nullptr);
if (nResult == RC_HARDCORE_DISABLED)
ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>().DisableHardcoreMode();
wrapper->DoCallback(nResult, sErrorMessage);
delete wrapper;
}
void AchievementRuntime::UnloadGame() noexcept
{
rc_client_unload_game(GetClient());
m_pClientSynchronizer.reset();
s_mAchievementPopups.clear();
}
/* ---- DoFrame ----- */
static void PrepareForPauseOnChangeEvents(rc_client_t* pClient,
std::vector<rc_client_achievement_info_t*>& vAchievementsWithHits,
std::map<rc_client_leaderboard_info_t*, ra::data::models::LeaderboardModel::LeaderboardParts>& mLeaderboardsWithHits,
std::map<rc_client_leaderboard_info_t*, ra::data::models::LeaderboardModel::LeaderboardParts>& mActiveLeaderboards)
{
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
for (auto& vmAsset : pGameContext.Assets())
{
const auto* vmAchievement = dynamic_cast<ra::data::models::AchievementModel*>(&vmAsset);
if (vmAchievement != nullptr)
{
if (vmAchievement->IsPauseOnReset())
{
auto* pAchievement = GetAchievementInfo(pClient, vmAchievement->GetID());
if (pAchievement && pAchievement->trigger && pAchievement->trigger->has_hits)
vAchievementsWithHits.push_back(pAchievement);
}
continue;
}
const auto* vmLeaderboard = dynamic_cast<ra::data::models::LeaderboardModel*>(&vmAsset);
if (vmLeaderboard != nullptr)
{
using namespace ra::bitwise_ops;
auto nPauseOnReset = vmLeaderboard->GetPauseOnReset();
if (nPauseOnReset != ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
auto* pLeaderboard = GetLeaderboardInfo(pClient, vmLeaderboard->GetID());
if (pLeaderboard && pLeaderboard->lboard)
{
if (!pLeaderboard->lboard->start.has_hits)
nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start;
if (!pLeaderboard->lboard->cancel.has_hits)
nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel;
if (!pLeaderboard->lboard->submit.has_hits)
nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit;
if (!pLeaderboard->lboard->value.value.value)
nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Value;
if (nPauseOnReset != ra::data::models::LeaderboardModel::LeaderboardParts::None)
mLeaderboardsWithHits[pLeaderboard] = nPauseOnReset;
}
}
auto nPauseOnTrigger = vmLeaderboard->GetPauseOnTrigger();
if (nPauseOnTrigger != ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
auto* pLeaderboard = GetLeaderboardInfo(pClient, vmLeaderboard->GetID());
if (pLeaderboard && pLeaderboard->lboard)
{
if (pLeaderboard->lboard->start.state == RC_TRIGGER_STATE_TRIGGERED)
nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start;
if (pLeaderboard->lboard->cancel.state == RC_TRIGGER_STATE_TRIGGERED)
nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel;
if (pLeaderboard->lboard->submit.state == RC_TRIGGER_STATE_TRIGGERED)
nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit;
if (nPauseOnTrigger != ra::data::models::LeaderboardModel::LeaderboardParts::None)
mActiveLeaderboards[pLeaderboard] = nPauseOnTrigger;
}
}
}
}
}
static void RaisePauseOnChangeEvents(std::vector<rc_client_achievement_info_t*>& vAchievementsWithHits)
{
for (const auto* pAchievement : vAchievementsWithHits)
{
if (pAchievement != nullptr && pAchievement->trigger && !pAchievement->trigger->has_hits)
{
auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable<ra::services::FrameEventQueue>();
pFrameEventQueue.QueuePauseOnReset(ra::Widen(pAchievement->public_.title));
}
}
}
static void RaisePauseOnChangeEvents(
std::map<rc_client_leaderboard_info_t*, ra::data::models::LeaderboardModel::LeaderboardParts>& mLeaderboardsWithHits,
std::map<rc_client_leaderboard_info_t*, ra::data::models::LeaderboardModel::LeaderboardParts>& mActiveLeaderboards)
{
for (auto pair : mLeaderboardsWithHits)
{
if (pair.first && pair.first->lboard)
{
using namespace ra::bitwise_ops;
auto nPauseOnReset = pair.second;
const auto* pLeaderboard = pair.first->lboard;
if (pLeaderboard->start.has_hits)
nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start;
if (pLeaderboard->cancel.has_hits)
nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel;
if (pLeaderboard->submit.has_hits)
nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit;
if (pLeaderboard->value.value.value)
nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Value;
if (nPauseOnReset != ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable<ra::services::FrameEventQueue>();
if ((nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Start) !=
ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
pFrameEventQueue.QueuePauseOnReset(ra::StringPrintf(L"Start: %s", pair.first->public_.title));
}
if ((nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Cancel) !=
ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
pFrameEventQueue.QueuePauseOnReset(ra::StringPrintf(L"Cancel: %s", pair.first->public_.title));
}
if ((nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Submit) !=
ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
pFrameEventQueue.QueuePauseOnReset(ra::StringPrintf(L"Submit: %s", pair.first->public_.title));
}
if ((nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Value) !=
ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
pFrameEventQueue.QueuePauseOnReset(ra::StringPrintf(L"Value: %s", pair.first->public_.title));
}
}
}
}
for (auto pair : mActiveLeaderboards)
{
if (pair.first && pair.first->lboard)
{
using namespace ra::bitwise_ops;
auto nPauseOnTrigger = pair.second;
const auto* pLeaderboard = pair.first->lboard;
if (pLeaderboard->start.state != RC_TRIGGER_STATE_TRIGGERED)
nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start;
if (pLeaderboard->cancel.state != RC_TRIGGER_STATE_TRIGGERED)
nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel;
if (pLeaderboard->submit.state != RC_TRIGGER_STATE_TRIGGERED)
nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit;
if (nPauseOnTrigger != ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable<ra::services::FrameEventQueue>();
if ((nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Start) !=
ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
pFrameEventQueue.QueuePauseOnTrigger(ra::StringPrintf(L"Start: %s", pair.first->public_.title));
}
if ((nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Cancel) !=
ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
pFrameEventQueue.QueuePauseOnTrigger(ra::StringPrintf(L"Cancel: %s", pair.first->public_.title));
}
if ((nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Submit) !=
ra::data::models::LeaderboardModel::LeaderboardParts::None)
{
pFrameEventQueue.QueuePauseOnTrigger(ra::StringPrintf(L"Submit: %s", pair.first->public_.title));
}
}
}
}
}
void AchievementRuntime::DoFrame()
{
m_hDoFrameThread = GetCurrentThreadId();
if (m_bPaused)
{
rc_client_idle(GetClient());
return;
}
std::vector<rc_client_achievement_info_t*> vAchievementsWithHits;
std::map<rc_client_leaderboard_info_t*, ra::data::models::LeaderboardModel::LeaderboardParts> mLeaderboardsWithHits;
std::map<rc_client_leaderboard_info_t*, ra::data::models::LeaderboardModel::LeaderboardParts> mActiveLeaderboards;
PrepareForPauseOnChangeEvents(GetClient(), vAchievementsWithHits, mLeaderboardsWithHits, mActiveLeaderboards);
rc_client_do_frame(GetClient());
if (!vAchievementsWithHits.empty())
RaisePauseOnChangeEvents(vAchievementsWithHits);
if (!mLeaderboardsWithHits.empty() || !mActiveLeaderboards.empty())
RaisePauseOnChangeEvents(mLeaderboardsWithHits, mActiveLeaderboards);
}
void AchievementRuntime::Idle() const noexcept
{
auto* pClient = GetClient();
if (!pClient)
return;
const auto* pScheduledCallback = pClient->state.scheduled_callbacks;
if (!pScheduledCallback)
return;
const rc_clock_t now = pClient->callbacks.get_time_millisecs(pClient);
bool bShouldIdle = false;
{
rc_mutex_lock(&pClient->state.mutex);
pScheduledCallback = pClient->state.scheduled_callbacks;
while (pScheduledCallback && pScheduledCallback->when == 0)
pScheduledCallback = pScheduledCallback->next;
if (pScheduledCallback && pScheduledCallback->when < now - 5000)
{
// pending callback has been waiting for more than five
// seconds. force a call to rc_client_idle
bShouldIdle = true;
}
rc_mutex_unlock(&pClient->state.mutex);
}
if (bShouldIdle)
rc_client_idle(pClient);
}
void AchievementRuntime::InvalidateAddress(ra::ByteAddress nAddress) noexcept
{
// this should only be called from DetectUnsupportedAchievements or indirectly via Process,
// both of which aquire the lock, so we shouldn't try to acquire it here.
// we can also avoid checking m_bInitialized
rc_runtime_invalidate_address(&GetClient()->game->runtime, nAddress);
}
static void HandleAchievementTriggeredEvent(const rc_client_achievement_t& pAchievement)
{
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
auto* vmAchievement = pGameContext.Assets().FindAchievement(pAchievement.id);
if (!vmAchievement)
{
RA_LOG_ERR("Received achievement triggered event for unknown achievement %u", pAchievement.id);
return;
}
// immediately update the state to Triggered (instead of waiting for AssetListViewModel::DoFrame to do it).
// this captures the unlock time and rich presence state, even if KeepActive it selected.
vmAchievement->SetState(ra::data::models::AssetState::Triggered);
if (vmAchievement->IsPauseOnTrigger())
{
auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable<ra::services::FrameEventQueue>();
pFrameEventQueue.QueuePauseOnTrigger(vmAchievement->GetName());
}
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
bool bTakeScreenshot = pConfiguration.IsFeatureEnabled(ra::services::Feature::AchievementTriggeredScreenshot);
bool bSubmit = false;
bool bIsError = false;
std::unique_ptr<ra::ui::viewmodels::PopupMessageViewModel> vmPopup(new ra::ui::viewmodels::PopupMessageViewModel);
vmPopup->SetDescription(ra::StringPrintf(L"%s (%u)", pAchievement.title, pAchievement.points));
vmPopup->SetDetail(ra::Widen(pAchievement.description));
vmPopup->SetImage(ra::ui::ImageType::Badge, AchievementRuntime::GetAchievementBadge(pAchievement));
vmPopup->SetPopupType(ra::ui::viewmodels::Popup::AchievementTriggered);
switch (vmAchievement->GetCategory())
{
case ra::data::models::AssetCategory::Local:
vmPopup->SetTitle(L"Local Achievement Unlocked");
bSubmit = false;
bTakeScreenshot = false;
break;
case ra::data::models::AssetCategory::Unofficial:
vmPopup->SetTitle(L"Unofficial Achievement Unlocked");
bSubmit = false;
break;
default:
vmPopup->SetTitle(L"Achievement Unlocked");
bSubmit = true;
break;
}
if (vmAchievement->IsModified() || // actual modifications
(bSubmit && vmAchievement->GetChanges() != ra::data::models::AssetChanges::None)) // unpublished changes
{
auto sHeader = vmPopup->GetTitle();
sHeader.insert(0, L"Modified ");
if (vmAchievement->GetCategory() != ra::data::models::AssetCategory::Local)
sHeader.append(L" LOCALLY");
vmPopup->SetTitle(sHeader);
RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement.id,
vmAchievement->IsModified() ? "modified" : "unpublished");
bIsError = true;
bSubmit = false;
bTakeScreenshot = false;
}
if (bSubmit && pGameContext.GetMode() == ra::data::context::GameContext::Mode::CompatibilityTest)
{
auto sHeader = vmPopup->GetTitle();
sHeader.insert(0, L"Test ");
vmPopup->SetTitle(sHeader);
RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement.id, "test compatibility mode");
bSubmit = false;
}
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
if (bSubmit && pEmulatorContext.WasMemoryModified())
{
vmPopup->SetTitle(L"Achievement Unlocked LOCALLY");
vmPopup->SetErrorDetail(L"Error: RAM tampered with");
RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement.id, "RAM tampered with");
bIsError = true;
bSubmit = false;
}
if (bSubmit && _RA_HardcoreModeIsActive() && pEmulatorContext.IsMemoryInsecure())
{
vmPopup->SetTitle(L"Achievement Unlocked LOCALLY");
vmPopup->SetErrorDetail(L"Error: RAM insecure");
RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement.id, "RAM insecure");
bIsError = true;
bSubmit = false;
}
if (bSubmit && pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline))
{
vmPopup->SetTitle(L"Offline Achievement Unlocked");
bSubmit = false;
}
#ifndef NDEBUG
if (!bSubmit)
Expects(!CanSubmitAchievementUnlock(pAchievement.id, nullptr));
#endif
const wchar_t* sAudioPath = L"Overlay\\unlock.wav";
if (bIsError)
{
sAudioPath = L"Overlay\\acherror.wav";
}
else if (bSubmit)
{
const bool bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
const auto nRarity = bHardcore ? pAchievement.rarity_hardcore : pAchievement.rarity;
if (nRarity >= 10.0)
{
vmPopup->SetTitle(ra::StringPrintf(L"Achievement Unlocked - %0.2f%%", nRarity));
}
else if (nRarity > 0.0)
{
vmPopup->SetTitle(ra::StringPrintf(L"Rare Achievement Unlocked - %0.2f%%", nRarity));
const wchar_t* sPath = L"Overlay\\rareunlock.wav";
const auto& pFileSystem = ra::services::ServiceLocator::Get<ra::services::IFileSystem>();
const std::wstring sFullPath = pFileSystem.BaseDirectory() + sPath;
if (pFileSystem.GetFileSize(sFullPath) > 0)
sAudioPath = sPath;
}
else
{
// rarity not set - don't report it
}
}
ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(sAudioPath);
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered) !=
ra::ui::viewmodels::PopupLocation::None)
{
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
const auto nPopupId = pOverlayManager.QueueMessage(vmPopup);
if (bTakeScreenshot)
{
std::wstring sPath =
ra::StringPrintf(L"%s%u.png", pConfiguration.GetScreenshotDirectory(), pAchievement.id);
pOverlayManager.CaptureScreenshot(nPopupId, sPath);
}
s_mAchievementPopups[pAchievement.id] = nPopupId;
}
}
static void HandleChallengeIndicatorShowEvent(const rc_client_achievement_t& pAchievement)
{
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
const auto* vmAchievement = pGameContext.Assets().FindAchievement(pAchievement.id);
if (!vmAchievement)
{
RA_LOG_ERR("Received achievement triggered event for unknown achievement %u", pAchievement.id);
return;
}
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Challenge) !=
ra::ui::viewmodels::PopupLocation::None)
{
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
pOverlayManager.AddChallengeIndicator(vmAchievement->GetID(), ra::ui::ImageType::Badge,
ra::Narrow(vmAchievement->GetBadge()));
}
}
static void HandleChallengeIndicatorHideEvent(const rc_client_achievement_t& pAchievement)
{
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
pOverlayManager.RemoveChallengeIndicator(pAchievement.id);
}
static void HandleProgressIndicatorUpdateEvent(const rc_client_achievement_t& pAchievement)
{
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
const auto* vmAchievement = pGameContext.Assets().FindAchievement(pAchievement.id);
if (!vmAchievement)
{
RA_LOG_ERR("Received achievement triggered event for unknown achievement %u", pAchievement.id);
return;
}
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Progress) != ra::ui::viewmodels::PopupLocation::None)
{
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
// use locked badge if available
auto sBadgeName = ra::Narrow(vmAchievement->GetBadge());
if (!ra::StringStartsWith(sBadgeName, "local\\"))
sBadgeName += "_lock";
pOverlayManager.UpdateProgressTracker(ra::ui::ImageType::Badge, sBadgeName,
ra::Widen(pAchievement.measured_progress));
}
}
static void HandleProgressIndicatorShowEvent(const rc_client_achievement_t& pAchievement)
{
HandleProgressIndicatorUpdateEvent(pAchievement);
}
static void HandleProgressIndicatorHideEvent()
{
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
pOverlayManager.UpdateProgressTracker(ra::ui::ImageType::None, "", L"");
}
static void ShowCompletionPopup(uint32_t nGameId, const std::wstring& sTitle, uint32_t nAchievements, uint32_t nPoints, const std::string& sBadgeName)
{
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Mastery) == ra::ui::viewmodels::PopupLocation::None)
return;
const bool bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
const auto nPlayTimeSeconds =
ra::services::ServiceLocator::Get<ra::data::context::SessionTracker>().GetTotalPlaytime(pGameContext.ActiveGameId());
const auto nPlayTimeMinutes = std::chrono::duration_cast<std::chrono::minutes>(nPlayTimeSeconds).count();
std::unique_ptr<ra::ui::viewmodels::PopupMessageViewModel> vmMessage(new ra::ui::viewmodels::PopupMessageViewModel);
vmMessage->SetTitle(ra::StringPrintf(L"%s %s", bHardcore ? L"Mastered" : L"Completed", sTitle));
vmMessage->SetDescription(ra::StringPrintf(L"%u achievements, %u points", nAchievements, nPoints));
vmMessage->SetDetail(ra::StringPrintf(L"%s | Play time: %dh%02dm", pUserContext.GetDisplayName(),
nPlayTimeMinutes / 60, nPlayTimeMinutes % 60));
vmMessage->SetImage(ra::ui::ImageType::Icon, sBadgeName);
vmMessage->SetPopupType(ra::ui::viewmodels::Popup::Mastery);
ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\unlock.wav");
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
const auto nPopupId = pOverlayManager.QueueMessage(vmMessage);
if (pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotificationScreenshot))
{
std::wstring sPath = ra::StringPrintf(L"%sGame%u.png", pConfiguration.GetScreenshotDirectory(), nGameId);
pOverlayManager.CaptureScreenshot(nPopupId, sPath);
}
}
static void HandleSubsetCompletedEvent(const rc_client_subset_t& pSubset)
{
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
for (const auto& pGameSubset : pGameContext.Subsets())
{
if (pGameSubset.AchievementSetID() == pSubset.id)
{
uint32_t nPoints = 0;
for (const auto& pAsset : pGameContext.Assets())
{
const auto* pAchievement = dynamic_cast<const ra::data::models::AchievementModel*>(&pAsset);
if (pAchievement && pAchievement->GetSubsetID() == pSubset.id && pAchievement->GetCategory() == ra::data::models::AssetCategory::Core)
nPoints += pAchievement->GetPoints();
}
const auto sTitle = ra::StringPrintf(L"%s (%s)", pGameSubset.Title(), pGameContext.GameTitle());
ShowCompletionPopup(pGameSubset.GameID(), sTitle, pSubset.num_achievements, nPoints, pSubset.badge_name);
break;
}
}
}
static void HandleGameCompletedEvent(const rc_client_t& pClient)
{
const auto* pGame = rc_client_get_game_info(&pClient);
Expects(pGame != nullptr);
rc_client_user_game_summary_t summary;
rc_client_get_user_game_summary(&pClient, &summary);
ShowCompletionPopup(pGame->id, ra::Widen(pGame->title), summary.num_core_achievements, summary.points_core, pGame->badge_name);
}
static void HandleLeaderboardStartedEvent(const rc_client_leaderboard_t& pLeaderboard)
{
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
const auto* vmLeaderboard = pGameContext.Assets().FindLeaderboard(pLeaderboard.id);
if (!vmLeaderboard)
{
RA_LOG_ERR("Received leaderboard started event for unknown leaderboard %u", pLeaderboard.id);
return;
}
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted) !=
ra::ui::viewmodels::PopupLocation::None &&
pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
{
ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\lb.wav");
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
pOverlayManager.QueueMessage(ra::ui::viewmodels::Popup::LeaderboardStarted, L"Leaderboard attempt started",
ra::Widen(pLeaderboard.title), ra::Widen(pLeaderboard.description));
}
}
static void HandleLeaderboardFailedEvent(const rc_client_leaderboard_t& pLeaderboard)
{
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
const auto* vmLeaderboard = pGameContext.Assets().FindLeaderboard(pLeaderboard.id);
if (!vmLeaderboard)
{
RA_LOG_ERR("Received leaderboard started event for unknown leaderboard %u", pLeaderboard.id);
return;
}
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled) !=
ra::ui::viewmodels::PopupLocation::None &&
pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
{
ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\lbcancel.wav");
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
pOverlayManager.QueueMessage(ra::ui::viewmodels::Popup::LeaderboardCanceled, L"Leaderboard attempt failed",
ra::Widen(pLeaderboard.title), ra::Widen(pLeaderboard.description));
}
}
static void ShowSimplifiedScoreboard(const rc_client_leaderboard_t& pLeaderboard)
{
auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) ==
ra::ui::viewmodels::PopupLocation::None ||
!pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
{
return;
}
ra::ui::viewmodels::ScoreboardViewModel vmScoreboard;
vmScoreboard.SetHeaderText(ra::Widen(pLeaderboard.title));
const auto& pUserName = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetDisplayName();
auto& pEntryViewModel = vmScoreboard.Entries().Add();
pEntryViewModel.SetRank(0);
pEntryViewModel.SetScore(ra::Widen(pLeaderboard.tracker_value));
pEntryViewModel.SetUserName(ra::Widen(pUserName));
pEntryViewModel.SetHighlighted(true);
ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().QueueScoreboard(
pLeaderboard.id, std::move(vmScoreboard));
}
static void HandleLeaderboardSubmittedEvent(const rc_client_leaderboard_t& pLeaderboard)
{
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
const auto* vmLeaderboard = pGameContext.Assets().FindLeaderboard(pLeaderboard.id);
if (!vmLeaderboard)
{
RA_LOG_ERR("Received leaderboard started event for unknown leaderboard %u", pLeaderboard.id);
return;
}
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
return;
std::unique_ptr<ra::ui::viewmodels::PopupMessageViewModel> vmPopup(new ra::ui::viewmodels::PopupMessageViewModel);
vmPopup->SetDescription(ra::Widen(pLeaderboard.title));
std::wstring sTitle = L"Leaderboard Submitted";
bool bSubmit = true;
switch (vmLeaderboard->GetCategory())
{
case ra::data::models::AssetCategory::Local:
sTitle.insert(0, L"Local ");
vmPopup->SetDetail(L"Local leaderboards are not submitted.");
bSubmit = false;
break;
case ra::data::models::AssetCategory::Unofficial:
sTitle.insert(0, L"Unofficial ");
vmPopup->SetDetail(L"Unofficial leaderboards are not submitted.");
bSubmit = false;
break;
default:
break;
}
if (vmLeaderboard->IsModified() || // actual modifications
(bSubmit && vmLeaderboard->GetChanges() != ra::data::models::AssetChanges::None)) // unpublished changes
{
sTitle.insert(0, L"Modified ");
vmPopup->SetDetail(L"Modified leaderboards are not submitted.");
bSubmit = false;
}
if (bSubmit && pGameContext.GetMode() == ra::data::context::GameContext::Mode::CompatibilityTest)
{
vmPopup->SetDetail(L"Leaderboards are not submitted in compatibility test mode.");
bSubmit = false;
}
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
if (bSubmit && pEmulatorContext.WasMemoryModified())
{
vmPopup->SetErrorDetail(L"Error: RAM tampered with");
bSubmit = false;
}
if (bSubmit)
{
if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
{
vmPopup->SetErrorDetail(L"Submission requires Hardcore mode");
bSubmit = false;
}
else if (pEmulatorContext.IsMemoryInsecure())
{
vmPopup->SetErrorDetail(L"Error: RAM insecure");
bSubmit = false;
}
else if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline))
{
vmPopup->SetDetail(L"Leaderboards are not submitted in offline mode.");
bSubmit = false;
}
}
if (!bSubmit)
{
vmPopup->SetTitle(sTitle);
ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\info.wav");
ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().QueueMessage(vmPopup);
#ifndef NDEBUG
Expects(!CanSubmitLeaderboardEntry(pLeaderboard.id, nullptr));
#endif
ShowSimplifiedScoreboard(pLeaderboard);
return;
}
// no popup shown here, the scoreboard will be shown instead
}
static void HandleLeaderboardTrackerUpdateEvent(const rc_client_leaderboard_tracker_t& pTracker)
{
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
auto* pScoreTracker = pOverlayManager.GetScoreTracker(pTracker.id);
if (pScoreTracker)
pScoreTracker->SetDisplayText(ra::Widen(pTracker.display));
}
static void HandleLeaderboardTrackerShowEvent(const rc_client_leaderboard_tracker_t& pTracker)
{
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker) !=
ra::ui::viewmodels::PopupLocation::None &&
pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
{
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
auto& pScoreTracker = pOverlayManager.AddScoreTracker(pTracker.id);
pScoreTracker.SetDisplayText(ra::Widen(pTracker.display));
}
}
static void HandleLeaderboardTrackerHideEvent(const rc_client_leaderboard_tracker_t& pTracker)
{
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
pOverlayManager.RemoveScoreTracker(pTracker.id);
}
static void HandleLeaderboardScoreboardEvent(const rc_client_leaderboard_scoreboard_t& pScoreboard,
const rc_client_leaderboard_t& pLeaderboard)
{
auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) ==
ra::ui::viewmodels::PopupLocation::None ||
!pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
{
return;
}
ra::ui::viewmodels::ScoreboardViewModel vmScoreboard;
vmScoreboard.SetHeaderText(ra::Widen(pLeaderboard.title));
const auto& pUserName = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetDisplayName();
constexpr uint32_t nEntriesDisplayed = 7; // display is currently hard-coded to show 7 entries
bool bSeenPlayer = false;
const auto* pEntry = pScoreboard.top_entries;
const auto* pStop = pEntry + std::min(pScoreboard.num_top_entries, nEntriesDisplayed);
for (; pEntry < pStop; ++pEntry)
{
auto& pEntryViewModel = vmScoreboard.Entries().Add();
pEntryViewModel.SetRank(pEntry->rank);
pEntryViewModel.SetScore(ra::Widen(pEntry->score));
pEntryViewModel.SetUserName(ra::Widen(pEntry->username));
if (pEntry->username == pUserName)
{
bSeenPlayer = true;
pEntryViewModel.SetHighlighted(true);
if (strcmp(pScoreboard.best_score, pScoreboard.submitted_score) != 0)
pEntryViewModel.SetScore(ra::StringPrintf(L"(%s) %s", pScoreboard.submitted_score, pScoreboard.best_score));
}
}
if (!bSeenPlayer)
{
auto* pEntryViewModel = vmScoreboard.Entries().GetItemAt(6);
if (pEntryViewModel != nullptr)
{
pEntryViewModel->SetRank(pScoreboard.new_rank);
if (strcmp(pScoreboard.best_score, pScoreboard.submitted_score) != 0)
pEntryViewModel->SetScore(
ra::StringPrintf(L"(%s) %s", pScoreboard.submitted_score, pScoreboard.best_score));
else
pEntryViewModel->SetScore(ra::Widen(pScoreboard.best_score));
pEntryViewModel->SetUserName(ra::Widen(pUserName));
pEntryViewModel->SetHighlighted(true);
}
}
ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().QueueScoreboard(
pLeaderboard.id, std::move(vmScoreboard));
}
static void HandleServerError(const rc_client_server_error_t& pServerError)
{
if (strcmp(pServerError.api, "award_achievement") == 0)
{
const auto nAchievementId = pServerError.related_id;
const auto nPopupId = s_mAchievementPopups[nAchievementId];
const auto sErrorMessage =
pServerError.error_message ? ra::Widen(pServerError.error_message) : L"Error submitting unlock";
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
auto pPopup = pOverlayManager.GetMessage(nPopupId);
if (pPopup != nullptr)
{
pPopup->SetTitle(L"Achievement Unlock FAILED");
pPopup->SetErrorDetail(sErrorMessage);
pPopup->RebuildRenderImage();
}
else
{
std::unique_ptr<ra::ui::viewmodels::PopupMessageViewModel> vmPopup(
new ra::ui::viewmodels::PopupMessageViewModel);
vmPopup->SetTitle(L"Achievement Unlock FAILED");
vmPopup->SetErrorDetail(sErrorMessage);
vmPopup->SetPopupType(ra::ui::viewmodels::Popup::AchievementTriggered);
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
const auto* pAchievement = pGameContext.Assets().FindAchievement(nAchievementId);
if (pAchievement != nullptr)
{
vmPopup->SetDescription(
ra::StringPrintf(L"%s (%u)", pAchievement->GetName(), pAchievement->GetPoints()));
vmPopup->SetImage(ra::ui::ImageType::Badge, ra::Narrow(pAchievement->GetBadge()));
}
else
{
vmPopup->SetDescription(ra::StringPrintf(L"Achievement %u", nAchievementId));
}
ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\acherror.wav");
pOverlayManager.QueueMessage(vmPopup);
}
return;
}
if (strcmp(pServerError.api, "submit_lboard_entry") == 0)
{
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
return;
const auto nLeaderboardId = pServerError.related_id;
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(nLeaderboardId);
std::wstring sLeaderboardName = (pLeaderboard != nullptr) ?
pLeaderboard->GetName() : ra::StringPrintf(L"Leaderboard %u", nLeaderboardId);
const auto sErrorMessage = pServerError.error_message ?
ra::Widen(pServerError.error_message) : L"Error submitting leaderboard entry";
std::unique_ptr<ra::ui::viewmodels::PopupMessageViewModel> vmMessage(new ra::ui::viewmodels::PopupMessageViewModel);
vmMessage->SetTitle(L"Leaderboard Submit FAILED");
vmMessage->SetDescription(sLeaderboardName);
vmMessage->SetErrorDetail(sErrorMessage);
auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
pOverlayManager.QueueMessage(vmMessage);
ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\acherror.wav");
return;
}
ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
ra::StringPrintf(L"%s:%u %s", pServerError.api, pServerError.related_id, pServerError.error_message));
}
static void HandleResetEvent()
{
auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
pEmulatorContext.Reset();
}
GSL_SUPPRESS_CON3
void AchievementRuntime::EventHandler(const rc_client_event_t* pEvent, rc_client_t* pClient)
{
Expects(pClient != nullptr);
Expects(pEvent != nullptr);
switch (pEvent->type)
{
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE:
HandleLeaderboardTrackerUpdateEvent(*pEvent->leaderboard_tracker);
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW:
HandleLeaderboardTrackerShowEvent(*pEvent->leaderboard_tracker);
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE:
HandleLeaderboardTrackerHideEvent(*pEvent->leaderboard_tracker);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:
HandleChallengeIndicatorShowEvent(*pEvent->achievement);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE:
HandleChallengeIndicatorHideEvent(*pEvent->achievement);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW:
HandleProgressIndicatorShowEvent(*pEvent->achievement);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE:
HandleProgressIndicatorHideEvent();
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE:
HandleProgressIndicatorUpdateEvent(*pEvent->achievement);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED:
HandleAchievementTriggeredEvent(*pEvent->achievement);
break;
case RC_CLIENT_EVENT_LEADERBOARD_STARTED:
HandleLeaderboardStartedEvent(*pEvent->leaderboard);
break;
case RC_CLIENT_EVENT_LEADERBOARD_FAILED:
HandleLeaderboardFailedEvent(*pEvent->leaderboard);
break;
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED:
HandleLeaderboardSubmittedEvent(*pEvent->leaderboard);
break;
case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD:
HandleLeaderboardScoreboardEvent(*pEvent->leaderboard_scoreboard, *pEvent->leaderboard);
break;
case RC_CLIENT_EVENT_GAME_COMPLETED:
HandleGameCompletedEvent(*pClient);
break;
case RC_CLIENT_EVENT_SUBSET_COMPLETED:
HandleSubsetCompletedEvent(*pEvent->subset);
break;
case RC_CLIENT_EVENT_RESET:
HandleResetEvent();
break;
case RC_CLIENT_EVENT_SERVER_ERROR:
HandleServerError(*pEvent->server_error);
break;
default:
RA_LOG_WARN("Unhandled rc_client_event %u", pEvent->type);
break;
}
}
/* ---- Runtime State ----- */
void AchievementRuntime::ResetRuntime() noexcept
{
rc_client_reset(GetClient());
}
static void ProcessStateString(Tokenizer& pTokenizer, unsigned int nId, rc_trigger_t* pTrigger,
const std::string& sSalt, const std::string& sMemString)
{
struct ConditionState
{
unsigned int nHits;
unsigned int nSourceVal;
unsigned int nSourcePrev;
unsigned int nTargetVal;
unsigned int nTargetPrev;
};
std::vector<ConditionState> vConditions;
const size_t nStart = pTokenizer.CurrentPosition();
// each group appears as an entry for the current nId
while (!pTokenizer.EndOfString())
{
const size_t nPosition = pTokenizer.CurrentPosition();
const unsigned int nGroupId = pTokenizer.ReadNumber();
if (nGroupId != nId || pTokenizer.PeekChar() != ':')
{
pTokenizer.Seek(nPosition);
break;
}
pTokenizer.Advance();
const unsigned int nNumCond = pTokenizer.ReadNumber();
pTokenizer.Advance();
vConditions.reserve(vConditions.size() + nNumCond);
for (size_t i = 0; i < nNumCond; ++i)
{
// Parse next condition state
auto& cond = vConditions.emplace_back();
cond.nHits = pTokenizer.ReadNumber();
pTokenizer.Advance();
cond.nSourceVal = pTokenizer.ReadNumber();
pTokenizer.Advance();
cond.nSourcePrev = pTokenizer.ReadNumber();
pTokenizer.Advance();
cond.nTargetVal = pTokenizer.ReadNumber();
pTokenizer.Advance();
cond.nTargetPrev = pTokenizer.ReadNumber();
pTokenizer.Advance();
}
}
const size_t nEnd = pTokenizer.CurrentPosition();
// read the given md5s
std::string sGivenMD5Progress = pTokenizer.ReadTo(':');
pTokenizer.Advance();
std::string sGivenMD5Achievement = pTokenizer.ReadTo(':');
pTokenizer.Advance();
if (!pTrigger)
{
// achievement wasn't found, ignore it. still had to parse to advance pIter
}
else
{
// recalculate the current achievement checksum
std::string sMD5Achievement = RAGenerateMD5(sMemString);
if (sGivenMD5Achievement != sMD5Achievement)
{
// achievement has been modified since data was captured, can't apply, just reset
rc_reset_trigger(pTrigger);
}
else
{
// regenerate the md5 and see if it sticks
const char* pStart = pTokenizer.GetPointer(nStart);
std::string sModifiedProgressString =
ra::StringPrintf("%s%.*s%s%u", sSalt, nEnd - nStart, pStart, sSalt, nId);
std::string sMD5Progress = RAGenerateMD5(sModifiedProgressString);
if (sMD5Progress != sGivenMD5Progress)
{
// state checksum fail - assume user tried to modify it and ignore, just reset
rc_reset_trigger(pTrigger);
}
else
{
// compatible - merge
size_t nCondition = 0;
rc_condset_t* pGroup = pTrigger->requirement;
while (pGroup != nullptr)
{
rc_condition_t* pCondition = pGroup->conditions;
while (pCondition != nullptr)
{
const auto& condSource = vConditions.at(nCondition++);
pCondition->current_hits = condSource.nHits;
if (rc_operand_is_memref(&pCondition->operand1))
{
pCondition->operand1.value.memref->value.value = condSource.nSourceVal;
pCondition->operand1.value.memref->value.prior = condSource.nSourcePrev;
pCondition->operand1.value.memref->value.changed = 0;
}
if (rc_operand_is_memref(&pCondition->operand2))
{
pCondition->operand2.value.memref->value.value = condSource.nTargetVal;
pCondition->operand2.value.memref->value.prior = condSource.nTargetPrev;
pCondition->operand2.value.memref->value.changed = 0;
}
pCondition = pCondition->next;
}
if (pGroup == pTrigger->requirement)
pGroup = pTrigger->alternative;
else
pGroup = pGroup->next;
}
}
}
}
}
static bool LoadProgressV1(rc_client_t* pClient, const std::string& sProgress)
{
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
Tokenizer pTokenizer(sProgress);
while (!pTokenizer.EndOfString())
{
const unsigned int nId = pTokenizer.PeekNumber();
auto* pAchievement = GetAchievementInfo(pClient, nId);
if (pAchievement == nullptr)
{
// achievement not active, still have to process state string to skip over it
std::string sEmpty;
ProcessStateString(pTokenizer, nId, nullptr, sEmpty, sEmpty);
}
else
{
const auto* vmAchievement = pGameContext.Assets().FindAchievement(nId);
std::string sMemString = vmAchievement ? vmAchievement->GetTrigger() : "";
ProcessStateString(pTokenizer, nId, pAchievement->trigger, pUserContext.GetUsername(), sMemString);
pAchievement->trigger->state = RC_TRIGGER_STATE_ACTIVE;
pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE;
}
}
return true;
}
_NODISCARD static char ComparisonSizeFromPrefix(_In_ char cPrefix) noexcept
{
char buffer[] = "0xH0";
const char* ptr = buffer;
buffer[2] = cPrefix;
uint8_t size = RC_MEMSIZE_16_BITS;
unsigned address = 0;
rc_parse_memref(&ptr, &size, &address);
return size;
}
static bool LoadProgressV2(rc_client_t* pClient, ra::services::TextReader& pFile)
{
auto& pRuntime = pClient->game->runtime;
rc_preparse_state_t pParseState;
rc_init_preparse_state(&pParseState);
pParseState.parse.existing_memrefs = pRuntime.memrefs;
std::string sLine;
while (pFile.GetLine(sLine))
{
if (sLine.empty())
continue;
Tokenizer tokenizer(sLine);
const auto c = tokenizer.PeekChar();
tokenizer.Advance();
if (c == '$') // memory reference
{
rc_memref_t pMemRef{};
const auto cPrefix = tokenizer.PeekChar();
tokenizer.Advance();
pMemRef.value.size = ComparisonSizeFromPrefix(cPrefix);
const auto sAddr = tokenizer.ReadTo(':');
pMemRef.address = ra::ByteAddressFromString(sAddr);
unsigned delta = 0;
while (tokenizer.PeekChar() != '#' && !tokenizer.EndOfString())
{
tokenizer.Advance();
const auto cField = tokenizer.PeekChar();
tokenizer.Advance(2);
const auto nValue = tokenizer.ReadNumber();
switch (cField)
{
case 'v':
pMemRef.value.value = nValue;
break;
case 'd':
delta = nValue;
break;
case 'p':
pMemRef.value.prior = nValue;
break;
}
}
pMemRef.value.changed = (pMemRef.value.value != delta);
if (tokenizer.PeekChar() == '#')
{
const auto sLineMD5 = RAGenerateMD5(sLine.substr(0, tokenizer.CurrentPosition()));
tokenizer.Advance();
if (tokenizer.PeekChar() == sLineMD5.at(0))
{
tokenizer.Advance();
if (tokenizer.PeekChar() == sLineMD5.at(31))
{
// match! attempt to store it
rc_memref_t* pMemoryReference = rc_alloc_memref(&pParseState.parse, pMemRef.address, pMemRef.value.size);
pMemoryReference->value.value = pMemRef.value.value;
pMemoryReference->value.changed = pMemRef.value.changed;
pMemoryReference->value.prior = pMemRef.value.prior;
}
}
}
}
else if (c == 'A') // achievement
{
const auto nId = tokenizer.ReadNumber();
tokenizer.Advance();
rc_runtime_trigger_t* pRuntimeTrigger = nullptr;
for (size_t i = 0; i < pRuntime.trigger_count; ++i)
{
if (pRuntime.triggers[i].id == nId && pRuntime.triggers[i].trigger)
{
pRuntimeTrigger = &pRuntime.triggers[i];
break;
}
}
if (pRuntimeTrigger == nullptr) // not active, ignore
continue;
auto* pTrigger = pRuntimeTrigger->trigger;
const auto sChecksumMD5 = sLine.substr(tokenizer.CurrentPosition(), 32);
tokenizer.Advance(32);
const auto sMemStringMD5 = RAFormatMD5(pRuntimeTrigger->md5);
if (sMemStringMD5 != sChecksumMD5) // modified since captured
{
rc_reset_trigger(pTrigger);
continue;
}
pTrigger->has_hits = 0;
rc_condition_t* pCondition = nullptr;
if (pTrigger->requirement)
{
pCondition = pTrigger->requirement->conditions;
while (pCondition)
{
tokenizer.Advance();
pCondition->current_hits = tokenizer.ReadNumber();
pTrigger->has_hits |= (pCondition->current_hits != 0);
pCondition = pCondition->next;
}
}
const rc_condset_t* pCondSet = pTrigger->alternative;
while (pCondSet)
{
pCondition = pCondSet->conditions;
while (pCondition)
{
tokenizer.Advance();
pCondition->current_hits = tokenizer.ReadNumber();
pTrigger->has_hits |= (pCondition->current_hits != 0);
pCondition = pCondition->next;
}
pCondSet = pCondSet->next;
}
bool bValid = (tokenizer.PeekChar() == '#');
if (bValid)
{
const auto sLineMD5 = RAGenerateMD5(sLine.substr(0, tokenizer.CurrentPosition()));
tokenizer.Advance();
if (tokenizer.PeekChar() != sLineMD5.at(0))
{
bValid = false;
}
else
{
tokenizer.Advance();
bValid = (tokenizer.PeekChar() == sLineMD5.at(31));
}
}
if (bValid)
{
pTrigger->state = RC_TRIGGER_STATE_ACTIVE;
}
else
{
rc_reset_trigger(pTrigger);
}
}
}
return true;
}
bool AchievementRuntime::LoadProgressFromFile(const char* sLoadStateFilename)
{
if (sLoadStateFilename == nullptr)
{
rc_client_deserialize_progress(GetClient(), nullptr);
return false;
}
std::string sContents;
std::wstring sAchievementStateFile = ra::Widen(sLoadStateFilename) + L".rap";
const auto& pFileSystem = ra::services::ServiceLocator::Get<ra::services::IFileSystem>();
auto pFile = pFileSystem.OpenTextFile(sAchievementStateFile);
if (pFile == nullptr || !pFile->GetLine(sContents))
{
rc_client_deserialize_progress(GetClient(), nullptr);
return false;
}
if (sContents == "RAP")
{
const auto nSize = pFile->GetSize();
std::vector<unsigned char> pBuffer;
pBuffer.resize(nSize);
pFile->SetPosition({0});
pFile->GetBytes(&pBuffer.front(), nSize);
if (rc_client_deserialize_progress(GetClient(), &pBuffer.front()) == RC_OK)
{
RA_LOG_INFO("Runtime state loaded from %s", sLoadStateFilename);
}
}
else if (sContents == "v2")
{
if (LoadProgressV2(GetClient(), *pFile))
{
RA_LOG_INFO("Runtime state (v2) loaded from %s", sLoadStateFilename);
}
}
else
{
if (LoadProgressV1(GetClient(), sContents))
{
RA_LOG_INFO("Runtime state (v1) loaded from %s", sLoadStateFilename);
}
}
return true;
}
bool AchievementRuntime::LoadProgressFromBuffer(const uint8_t* pBuffer)
{
if (rc_client_deserialize_progress(GetClient(), pBuffer) == RC_OK)
{
RA_LOG_INFO("Runtime state loaded from buffer");
}
return true;
}
void AchievementRuntime::SaveProgressToFile(const char* sSaveStateFilename) const
{
if (sSaveStateFilename == nullptr)
return;
std::wstring sAchievementStateFile = ra::Widen(sSaveStateFilename) + L".rap";
auto pFile = ra::services::ServiceLocator::Get<ra::services::IFileSystem>().CreateTextFile(sAchievementStateFile);
if (pFile == nullptr)
{
RA_LOG_WARN("Could not write %s", sAchievementStateFile);
return;
}
const auto nSize = rc_client_progress_size(GetClient());
std::string sSerialized;
sSerialized.resize(nSize);
GSL_SUPPRESS_TYPE1 const auto pData = reinterpret_cast<uint8_t*>(sSerialized.data());
rc_client_serialize_progress(GetClient(), pData);
pFile->Write(sSerialized);
RA_LOG_INFO("Runtime state written to %s", sSaveStateFilename);
}
int AchievementRuntime::SaveProgressToBuffer(uint8_t* pBuffer, int nBufferSize) const
{
const int nSize = gsl::narrow_cast<int>(rc_client_progress_size(GetClient()));
if (nSize <= nBufferSize)
{
rc_client_serialize_progress(GetClient(), pBuffer);
RA_LOG_INFO("Runtime state written to buffer (%d/%d bytes)", nSize, nBufferSize);
}
else if (nBufferSize > 0) // 0 size buffer indicates caller is asking for size, don't log - we'll capture the actual save soon.
{
RA_LOG_WARN("Runtime state not written to buffer (%d/%d bytes)", nSize, nBufferSize);
}
return nSize;
}
} // namespace services
} // namespace ra
extern "C" unsigned int rc_peek_callback(unsigned int nAddress, unsigned int nBytes, _UNUSED void* pData)
{
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
switch (nBytes)
{
case 1:
return pEmulatorContext.ReadMemoryByte(nAddress);
case 2:
return pEmulatorContext.ReadMemory(nAddress, MemSize::SixteenBit);
case 4:
return pEmulatorContext.ReadMemory(nAddress, MemSize::ThirtyTwoBit);
default:
return 0U;
}
}