Repository: RetroAchievements/RAIntegration
Pull request #1021 (2023-08-31 02:00:11)

Use rc_client_t to fetch game data


Patch:
diff --git a/src/Exports.cpp b/src/Exports.cpp
index c073b9d8..24ec62c2 100644
--- a/src/Exports.cpp
+++ b/src/Exports.cpp
@@ -39,6 +39,8 @@
 #include <RAInterface\RA_Emulators.h>
 
 #include <rcheevos\include\rc_api_runtime.h>
+#include <rcheevos\src\rapi\rc_api_common.h>
+#include <rcheevos\src\rcheevos\rc_client_internal.h>
 
 API const char* CCONV _RA_IntegrationVersion() { return RA_INTEGRATION_VERSION; }
 
@@ -106,6 +108,12 @@ static void InitializeOfflineMode()
         pSessionTracker.Initialize(sUsername);
     }
 
+    auto* pClient = ra::services::ServiceLocator::Get<ra::services::RcheevosClient>().GetClient();
+    pClient->user.username = rc_buf_strcpy(&pClient->state.buffer, pUserContext.GetUsername().c_str());
+    pClient->user.display_name = rc_buf_strcpy(&pClient->state.buffer, pUserContext.GetDisplayName().c_str());
+    pClient->user.token = rc_buf_strcpy(&pClient->state.buffer, pConfiguration.GetApiToken().c_str());
+    pClient->state.user = RC_CLIENT_USER_STATE_LOGGED_IN;
+
     pUserContext.DisableLogin();
 }
 
diff --git a/src/RA_Core.cpp b/src/RA_Core.cpp
index 7d8c5db0..ef40fdfe 100644
--- a/src/RA_Core.cpp
+++ b/src/RA_Core.cpp
@@ -118,7 +118,7 @@ API int CCONV _RA_Shutdown()
 
     ra::services::ServiceLocator::GetMutable<ra::data::context::SessionTracker>().EndSession();
 
-    ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>().LoadGame(0U);
+    ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>().LoadGame(0U, "");
 
     if (ra::services::ServiceLocator::Exists<ra::ui::viewmodels::WindowManager>())
     {
diff --git a/src/RA_Integration.vcxproj b/src/RA_Integration.vcxproj
index 127a7421..10cc5b5a 100644
--- a/src/RA_Integration.vcxproj
+++ b/src/RA_Integration.vcxproj
@@ -180,11 +180,9 @@
     <ClInclude Include="api\FetchAchievementInfo.hh" />
     <ClInclude Include="api\FetchBadgeIds.hh" />
     <ClInclude Include="api\FetchCodeNotes.hh" />
-    <ClInclude Include="api\FetchGameData.hh" />
     <ClInclude Include="api\FetchGamesList.hh" />
     <ClInclude Include="api\FetchLeaderboardInfo.hh" />
     <ClInclude Include="api\FetchUserFriends.hh" />
-    <ClInclude Include="api\FetchUserUnlocks.hh" />
     <ClInclude Include="api\impl\ConnectedServer.hh" />
     <ClInclude Include="api\impl\DisconnectedServer.hh" />
     <ClInclude Include="api\impl\OfflineServer.hh" />
@@ -537,7 +535,7 @@
       <GenerateDebugInformation>false</GenerateDebugInformation>
     </Link>
   </ItemDefinitionGroup>
-    <ItemDefinitionGroup Condition="'$(Configuration)'=='Release-vista'">
+  <ItemDefinitionGroup Condition="'$(Configuration)'=='Release-vista'">
     <ClCompile>
       <Optimization>Full</Optimization>
       <IntrinsicFunctions>true</IntrinsicFunctions>
diff --git a/src/RA_Integration.vcxproj.filters b/src/RA_Integration.vcxproj.filters
index cc56d4f8..3986d639 100644
--- a/src/RA_Integration.vcxproj.filters
+++ b/src/RA_Integration.vcxproj.filters
@@ -668,12 +668,6 @@
     <ClInclude Include="api\LatestClient.hh">
       <Filter>API</Filter>
     </ClInclude>
-    <ClInclude Include="api\FetchGameData.hh">
-      <Filter>API</Filter>
-    </ClInclude>
-    <ClInclude Include="api\FetchUserUnlocks.hh">
-      <Filter>API</Filter>
-    </ClInclude>
     <ClInclude Include="api\AwardAchievement.hh">
       <Filter>API</Filter>
     </ClInclude>
diff --git a/src/api/ApiCall.cpp b/src/api/ApiCall.cpp
index 67301a95..4cc40680 100644
--- a/src/api/ApiCall.cpp
+++ b/src/api/ApiCall.cpp
@@ -16,12 +16,10 @@ static ra::api::IServer& Server()
 
 StartSession::Response StartSession::Request::Call() const { return Server().StartSession(*this); }
 Ping::Response Ping::Request::Call() const { return Server().Ping(*this); }
-FetchUserUnlocks::Response FetchUserUnlocks::Request::Call() const { return Server().FetchUserUnlocks(*this); }
 AwardAchievement::Response AwardAchievement::Request::Call() const { return Server().AwardAchievement(*this); }
 SubmitLeaderboardEntry::Response SubmitLeaderboardEntry::Request::Call() const { return Server().SubmitLeaderboardEntry(*this); }
 FetchUserFriends::Response FetchUserFriends::Request::Call() const { return Server().FetchUserFriends(*this); }
 ResolveHash::Response ResolveHash::Request::Call() const { return Server().ResolveHash(*this); }
-FetchGameData::Response FetchGameData::Request::Call() const { return Server().FetchGameData(*this); }
 FetchCodeNotes::Response FetchCodeNotes::Request::Call() const { return Server().FetchCodeNotes(*this); }
 UpdateCodeNote::Response UpdateCodeNote::Request::Call() const { return Server().UpdateCodeNote(*this); }
 DeleteCodeNote::Response DeleteCodeNote::Request::Call() const { return Server().DeleteCodeNote(*this); }
diff --git a/src/api/FetchGameData.hh b/src/api/FetchGameData.hh
deleted file mode 100644
index cfad580d..00000000
--- a/src/api/FetchGameData.hh
+++ /dev/null
@@ -1,68 +0,0 @@
-#ifndef RA_API_FETCH_GAME_DATA_HH
-#define RA_API_FETCH_GAME_DATA_HH
-#pragma once
-
-#include "ApiCall.hh"
-
-namespace ra {
-namespace api {
-
-class FetchGameData
-{
-public:
-    static constexpr const char* const Name() noexcept { return "FetchGameData"; }
-
-    struct Response : ApiResponseBase
-    {
-        std::wstring Title;
-        unsigned int ConsoleId{};
-        std::string ImageIcon;
-        std::string RichPresence;
-
-        struct Achievement
-        {
-            unsigned int Id{};
-            std::string Title;
-            std::string Description;
-            unsigned int CategoryId{};
-            unsigned int Points{};
-            std::string Definition;
-            std::string Author;
-            std::string BadgeName;
-            time_t Created{};
-            time_t Updated{};
-        };
-        std::vector<Achievement> Achievements;
-
-        struct Leaderboard
-        {
-            unsigned int Id{};
-            std::string Title;
-            std::string Description;
-            std::string Definition;
-            int Format{ 0 };
-            bool LowerIsBetter{ false };
-            bool Hidden{ false };
-        };
-        std::vector<Leaderboard> Leaderboards;
-    };
-
-    struct Request : ApiRequestBase
-    {
-        unsigned int GameId{ 0U };
-
-        using Callback = std::function<void(const Response& response)>;
-
-        Response Call() const;
-
-        void CallAsync(Callback&& callback) const
-        {
-            ApiRequestBase::CallAsync<Request, Callback>(*this, std::move(callback));
-        }
-    };
-};
-
-} // namespace api
-} // namespace ra
-
-#endif // !RA_API_FETCH_GAME_DATA_HH
diff --git a/src/api/FetchUserUnlocks.hh b/src/api/FetchUserUnlocks.hh
deleted file mode 100644
index dc76f9dd..00000000
--- a/src/api/FetchUserUnlocks.hh
+++ /dev/null
@@ -1,39 +0,0 @@
-#ifndef RA_API_FETCH_USER_UNLOCKS_HH
-#define RA_API_FETCH_USER_UNLOCKS_HH
-#pragma once
-
-#include "ApiCall.hh"
-
-namespace ra {
-namespace api {
-
-class FetchUserUnlocks
-{
-public:
-    static constexpr const char* const Name() noexcept { return "FetchUserUnlocks"; }
-
-    struct Response : ApiResponseBase
-    {
-        std::set<unsigned int> UnlockedAchievements;
-    };
-
-    struct Request : ApiRequestBase
-    {
-        unsigned int GameId{ 0U };
-        bool Hardcore{ false };
-
-        using Callback = std::function<void(const Response& response)>;
-
-        Response Call() const;
-
-        void CallAsync(Callback&& callback) const
-        {
-            ApiRequestBase::CallAsync<Request, Callback>(*this, std::move(callback));
-        }
-    };
-};
-
-} // namespace api
-} // namespace ra
-
-#endif // !RA_API_FETCH_USER_UNLOCKS_HH
diff --git a/src/api/IServer.hh b/src/api/IServer.hh
index 733ce12e..ef5abceb 100644
--- a/src/api/IServer.hh
+++ b/src/api/IServer.hh
@@ -7,11 +7,9 @@
 #include "api/FetchAchievementInfo.hh"
 #include "api/FetchBadgeIds.hh"
 #include "api/FetchCodeNotes.hh"
-#include "api/FetchGameData.hh"
 #include "api/FetchGamesList.hh"
 #include "api/FetchLeaderboardInfo.hh"
 #include "api/FetchUserFriends.hh"
-#include "api/FetchUserUnlocks.hh"
 #include "api/LatestClient.hh"
 #include "api/Ping.hh"
 #include "api/ResolveHash.hh"
@@ -35,14 +33,12 @@ public:
     // === user functions ===
     virtual StartSession::Response StartSession(const StartSession::Request& request) = 0;
     virtual Ping::Response Ping(const Ping::Request& request) = 0;
-    virtual FetchUserUnlocks::Response FetchUserUnlocks(const FetchUserUnlocks::Request& request) = 0;
     virtual AwardAchievement::Response AwardAchievement(const AwardAchievement::Request& request) = 0;
     virtual SubmitLeaderboardEntry::Response SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request& request) = 0;
     virtual FetchUserFriends::Response FetchUserFriends(const FetchUserFriends::Request& request) = 0;
 
     // === game functions ===
     virtual ResolveHash::Response ResolveHash(const ResolveHash::Request& request) = 0;
-    virtual FetchGameData::Response FetchGameData(const FetchGameData::Request& request) = 0;
     virtual FetchCodeNotes::Response FetchCodeNotes(const FetchCodeNotes::Request& request) = 0;
     virtual UpdateCodeNote::Response UpdateCodeNote(const UpdateCodeNote::Request& request) = 0;
     virtual DeleteCodeNote::Response DeleteCodeNote(const DeleteCodeNote::Request& request) = 0;
diff --git a/src/api/impl/ConnectedServer.cpp b/src/api/impl/ConnectedServer.cpp
index 1288bd69..ca97878c 100644
--- a/src/api/impl/ConnectedServer.cpp
+++ b/src/api/impl/ConnectedServer.cpp
@@ -545,44 +545,6 @@ Ping::Response ConnectedServer::Ping(const Ping::Request& request)
     return response;
 }
 
-FetchUserUnlocks::Response ConnectedServer::FetchUserUnlocks(const FetchUserUnlocks::Request& request)
-{
-    FetchUserUnlocks::Response response;
-
-    rc_api_fetch_user_unlocks_request_t api_params;
-    memset(&api_params, 0, sizeof(api_params));
-
-    const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
-    api_params.username = pUserContext.GetUsername().c_str();
-    api_params.api_token = pUserContext.GetApiToken().c_str();
-    api_params.game_id = request.GameId;
-    api_params.hardcore = request.Hardcore ? 1 : 0;
-
-    rc_api_request_t api_request;
-    if (rc_api_init_fetch_user_unlocks_request(&api_request, &api_params) == RC_OK)
-    {
-        ra::services::Http::Response httpResponse;
-        if (DoRequest(api_request, FetchUserUnlocks::Name(), httpResponse, response))
-        {
-            rc_api_fetch_user_unlocks_response_t api_response;
-            const auto nResult = rc_api_process_fetch_user_unlocks_response(&api_response, httpResponse.Content().c_str());
-
-            if (ValidateResponse(nResult, api_response.response, FetchUserUnlocks::Name(), httpResponse.StatusCode(), response))
-            {
-                response.Result = ApiResult::Success;
-
-                for (unsigned i = 0; i < api_response.num_achievement_ids; ++i)
-                    response.UnlockedAchievements.insert(api_response.achievement_ids[i]);
-            }
-
-            rc_api_destroy_fetch_user_unlocks_response(&api_response);
-        }
-    }
-
-    rc_api_destroy_request(&api_request);
-    return response;
-}
-
 AwardAchievement::Response ConnectedServer::AwardAchievement(const AwardAchievement::Request& request)
 {
     AwardAchievement::Response response;
@@ -748,109 +710,6 @@ ResolveHash::Response ConnectedServer::ResolveHash(const ResolveHash::Request& r
     return response;
 }
 
-FetchGameData::Response ConnectedServer::FetchGameData(const FetchGameData::Request& request)
-{
-    FetchGameData::Response response;
-
-    rc_api_fetch_game_data_request_t api_params;
-    memset(&api_params, 0, sizeof(api_params));
-
-    const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
-    api_params.username = pUserContext.GetUsername().c_str();
-    api_params.api_token = pUserContext.GetApiToken().c_str();
-    api_params.game_id = request.GameId;
-
-    rc_api_request_t api_request;
-    if (rc_api_init_fetch_game_data_request(&api_request, &api_params) == RC_OK)
-    {
-        ra::services::Http::Response httpResponse;
-        if (DoRequest(api_request, FetchGameData::Name(), httpResponse, response))
-        {
-            // process it
-            ProcessGamePatchData(response, httpResponse);
-
-            if (response.Result == ApiResult::Success)
-            {
-                // extract the PatchData and store a copy in the cache for offline mode
-                rc_api_response_t api_response{};
-                GSL_SUPPRESS_ES47
-                rc_json_field_t fields[] = {
-                    RC_JSON_NEW_FIELD("Success"),
-                    RC_JSON_NEW_FIELD("Error"),
-                    RC_JSON_NEW_FIELD("PatchData")
-                };
-
-                rc_api_server_response_t server_response{};
-                server_response.body = httpResponse.Content().c_str();
-                server_response.body_length = httpResponse.Content().length();
-                server_response.http_status_code = ra::etoi(httpResponse.StatusCode());
-
-                if (rc_json_parse_server_response(&api_response, &server_response, fields, sizeof(fields) / sizeof(fields[0])) == RC_OK &&
-                    fields[2].value_start && fields[2].value_end)
-                {
-                    auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
-                    auto pData = pLocalStorage.WriteText(ra::services::StorageItemType::GameData, std::to_wstring(request.GameId));
-                    if (pData != nullptr)
-                    {
-                        std::string sPatchData;
-                        sPatchData.append(fields[2].value_start, fields[2].value_end - fields[2].value_start);
-                        pData->Write(sPatchData);
-                    }
-                }
-            }
-        }
-    }
-
-    rc_api_destroy_request(&api_request);
-    return response;
-}
-
-void ConnectedServer::ProcessGamePatchData(ra::api::FetchGameData::Response &response, const ra::services::Http::Response& httpResponse)
-{
-    rc_api_fetch_game_data_response_t api_response;
-    const auto nResult = rc_api_process_fetch_game_data_response(&api_response, httpResponse.Content().c_str());
-
-    if (ValidateResponse(nResult, api_response.response, FetchGameData::Name(), httpResponse.StatusCode(), response))
-    {
-        response.Result = ApiResult::Success;
-        response.Title = ra::Widen(api_response.title);
-        response.ConsoleId = api_response.console_id;
-        response.ImageIcon = api_response.image_name;
-        response.RichPresence = api_response.rich_presence_script;
-
-        for (unsigned i = 0; i < api_response.num_achievements; ++i)
-        {
-            const auto* pSrcAchievement = &api_response.achievements[i];
-            auto& pAchievement = response.Achievements.emplace_back();
-            pAchievement.Id = pSrcAchievement->id;
-            pAchievement.Title = pSrcAchievement->title;
-            pAchievement.Description = pSrcAchievement->description;
-            pAchievement.CategoryId = pSrcAchievement->category;
-            pAchievement.Points = pSrcAchievement->points;
-            pAchievement.Definition = pSrcAchievement->definition;
-            pAchievement.Author = pSrcAchievement->author;
-            pAchievement.BadgeName = pSrcAchievement->badge_name;
-            pAchievement.Created = pSrcAchievement->created;
-            pAchievement.Updated = pSrcAchievement->updated;
-        }
-
-        for (unsigned i = 0; i < api_response.num_leaderboards; ++i)
-        {
-            const auto* pSrcLeaderboard = &api_response.leaderboards[i];
-            auto& pLeaderboard = response.Leaderboards.emplace_back();
-            pLeaderboard.Id = pSrcLeaderboard->id;
-            pLeaderboard.Title = pSrcLeaderboard->title;
-            pLeaderboard.Description = pSrcLeaderboard->description;
-            pLeaderboard.Definition = pSrcLeaderboard->definition;
-            pLeaderboard.Format = pSrcLeaderboard->format;
-            pLeaderboard.LowerIsBetter = pSrcLeaderboard->lower_is_better;
-            pLeaderboard.Hidden = pSrcLeaderboard->hidden;
-        }
-    }
-
-    rc_api_destroy_fetch_game_data_response(&api_response);
-}
-
 FetchCodeNotes::Response ConnectedServer::FetchCodeNotes(const FetchCodeNotes::Request& request)
 {
     FetchCodeNotes::Response response;
diff --git a/src/api/impl/ConnectedServer.hh b/src/api/impl/ConnectedServer.hh
index ec7be42c..2c5cc009 100644
--- a/src/api/impl/ConnectedServer.hh
+++ b/src/api/impl/ConnectedServer.hh
@@ -17,12 +17,10 @@ public:
 
     StartSession::Response StartSession(const StartSession::Request& request) override;
     Ping::Response Ping(const Ping::Request& request) override;
-    FetchUserUnlocks::Response FetchUserUnlocks(const FetchUserUnlocks::Request& request) override;
     AwardAchievement::Response AwardAchievement(const AwardAchievement::Request& request) override;
     SubmitLeaderboardEntry::Response SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request& request) override;
     FetchUserFriends::Response FetchUserFriends(const FetchUserFriends::Request& request) override;
     ResolveHash::Response ResolveHash(const ResolveHash::Request& request) override;
-    FetchGameData::Response FetchGameData(const FetchGameData::Request& request) override;
     FetchCodeNotes::Response FetchCodeNotes(const FetchCodeNotes::Request& request) override;
     UpdateCodeNote::Response UpdateCodeNote(const UpdateCodeNote::Request& request) override;
     DeleteCodeNote::Response DeleteCodeNote(const DeleteCodeNote::Request& request) override;
@@ -37,7 +35,6 @@ public:
     FetchBadgeIds::Response FetchBadgeIds(const FetchBadgeIds::Request& request) override;
     UploadBadge::Response UploadBadge(const UploadBadge::Request& request) override;
 
-    static void ProcessGamePatchData(FetchGameData::Response &response, const ra::services::Http::Response& httpResponse);
     static void ProcessCodeNotes(FetchCodeNotes::Response &response, const void* api_response);
 
 private:
diff --git a/src/api/impl/OfflineServer.cpp b/src/api/impl/OfflineServer.cpp
index 649caace..fb5b92ee 100644
--- a/src/api/impl/OfflineServer.cpp
+++ b/src/api/impl/OfflineServer.cpp
@@ -29,39 +29,6 @@ SubmitLeaderboardEntry::Response OfflineServer::SubmitLeaderboardEntry(const Sub
     return response;
 }
 
-FetchGameData::Response OfflineServer::FetchGameData(const FetchGameData::Request& request)
-{
-    FetchGameData::Response response;
-    rapidjson::Document document;
-
-    // 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(request.GameId));
-    if (pData == nullptr)
-    {
-        response.Result = ApiResult::Failed;
-        response.ErrorMessage = ra::StringPrintf("Achievement data for game %u not found in cache", request.GameId);
-    }
-    else 
-    {
-        std::string sContents = "{\"Success\":true,\"PatchData\":";
-        std::string sLine;
-        while (pData->GetLine(sLine))
-        {
-            if (!sContents.empty())
-                sContents.push_back('\n');
-
-            sContents.append(sLine);
-        }
-        sContents.push_back('}');
-
-        ra::services::Http::Response httpResponse(ra::services::Http::StatusCode::OK, sContents);
-        ConnectedServer::ProcessGamePatchData(response, httpResponse);
-    }
-
-    return response;
-}
-
 FetchCodeNotes::Response OfflineServer::FetchCodeNotes(const FetchCodeNotes::Request& request)
 {
     FetchCodeNotes::Response response;
diff --git a/src/api/impl/OfflineServer.hh b/src/api/impl/OfflineServer.hh
index f259b0ec..093f0116 100644
--- a/src/api/impl/OfflineServer.hh
+++ b/src/api/impl/OfflineServer.hh
@@ -14,7 +14,6 @@ public:
     AwardAchievement::Response AwardAchievement(const AwardAchievement::Request&) override;
     SubmitLeaderboardEntry::Response SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request&) override;
 
-    FetchGameData::Response FetchGameData(const FetchGameData::Request& request) override;
     FetchCodeNotes::Response FetchCodeNotes(const FetchCodeNotes::Request& request) override;
 
     LatestClient::Response LatestClient(const LatestClient::Request& request) override;
diff --git a/src/api/impl/ServerBase.hh b/src/api/impl/ServerBase.hh
index dc638d2e..2f6ed646 100644
--- a/src/api/impl/ServerBase.hh
+++ b/src/api/impl/ServerBase.hh
@@ -27,11 +27,6 @@ public:
         return UnsupportedApi<Ping::Response>(Ping::Name());
     }
 
-    FetchUserUnlocks::Response FetchUserUnlocks(const FetchUserUnlocks::Request&) override
-    {
-        return UnsupportedApi<FetchUserUnlocks::Response>(FetchUserUnlocks::Name());
-    }
-
     AwardAchievement::Response AwardAchievement(const AwardAchievement::Request&) override
     {
         return UnsupportedApi<AwardAchievement::Response>(AwardAchievement::Name());
@@ -54,11 +49,6 @@ public:
         return UnsupportedApi<ResolveHash::Response>(ResolveHash::Name());
     }
 
-    FetchGameData::Response FetchGameData(const FetchGameData::Request&) override
-    {
-        return UnsupportedApi<FetchGameData::Response>(FetchGameData::Name());
-    }
-
     FetchCodeNotes::Response FetchCodeNotes(const FetchCodeNotes::Request&) override
     {
         return UnsupportedApi<FetchCodeNotes::Response>(FetchCodeNotes::Name());
diff --git a/src/data/context/GameContext.cpp b/src/data/context/GameContext.cpp
index 1963fad8..4bf9c95e 100644
--- a/src/data/context/GameContext.cpp
+++ b/src/data/context/GameContext.cpp
@@ -8,8 +8,6 @@
 #include "RA_StringUtils.h"
 
 #include "api\AwardAchievement.hh"
-#include "api\FetchGameData.hh"
-#include "api\FetchUserUnlocks.hh"
 #include "api\SubmitLeaderboardEntry.hh"
 
 #include "data\context\ConsoleContext.hh"
@@ -26,6 +24,7 @@
 #include "services\IAudioSystem.hh"
 #include "services\IConfiguration.hh"
 #include "services\ILocalStorage.hh"
+#include "services\RcheevosClient.hh"
 #include "services\impl\FileTextReader.hh"
 #include "services\impl\FileTextWriter.hh"
 #include "services\impl\StringTextReader.hh"
@@ -37,11 +36,60 @@
 #include "ui\viewmodels\ScoreboardViewModel.hh"
 #include "ui\viewmodels\WindowManager.hh"
 
+#include <rcheevos\src\rcheevos\rc_client_internal.h>
+
 namespace ra {
 namespace data {
 namespace context {
 
-void GameContext::LoadGame(unsigned int nGameId, Mode nMode)
+static bool ValidateConsole(int nServerConsoleId)
+{
+    const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
+    if (ra::etoi(pConsoleContext.Id()) != nServerConsoleId)
+    {
+        switch (nServerConsoleId)
+        {
+            case RC_CONSOLE_GAMEBOY:
+                // GB memory map is just a subset of GBC, and GBC runs GB games, so allow loading a
+                // GB game in GBC context.
+                if (pConsoleContext.Id() == ra::etoi(ConsoleID::GBC))
+                    return true;
+
+                break;
+
+            case RC_CONSOLE_NINTENDO_DS:
+                // DS memory map is just a subset of DSi, and DSi runs DS games, so allow loading a
+                // DS game in DSi context.
+                if (pConsoleContext.Id() == ra::etoi(ConsoleID::DSi))
+                    return true;
+
+                break;
+
+            case RC_CONSOLE_EVENTS:
+                // Hashes for Events are generated by modifying an existing ROM in a very specific manner
+                // To generate the correct hash, Events are loaded through the console associated to the
+                // unmodified ROM and therefore will never match the console context.
+                return true;
+
+            default:
+                break;
+        }
+
+        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
+            L"Identified game does not match expected console.",
+            ra::StringPrintf(
+                L"The game being loaded is associated to the %s console, but the emulator has initialized "
+                "the %s console. This is not allowed as the memory maps may not be compatible between "
+                "consoles.",
+                rc_console_name(nServerConsoleId), pConsoleContext.Name()));
+
+        return false;
+    }
+
+    return true;
+}
+
+void GameContext::LoadGame(unsigned int nGameId, const std::string& sGameHash, Mode nMode)
 {
     OnBeforeActiveGameChanged();
 
@@ -95,157 +143,106 @@ void GameContext::LoadGame(unsigned int nGameId, Mode nMode)
         if (pData != nullptr)
         {
             rapidjson::Document pDocument;
-            if (LoadDocument(pDocument, *pData) && pDocument.HasMember("RichPresencePatch") && pDocument["RichPresencePatch"].IsString())
+            if (LoadDocument(pDocument, *pData) && pDocument.HasMember("RichPresencePatch") &&
+                pDocument["RichPresencePatch"].IsString())
                 sOldRichPresence = pDocument["RichPresencePatch"].GetString();
         }
     }
 
     // download the game data
-    ra::api::FetchGameData::Request request;
-    request.GameId = nGameId;
+    ra::services::RcheevosClient::Synchronizer pSynchronizer;
 
-    const auto response = request.Call();
-    if (response.Failed())
-    {
-        m_vAssets.EndUpdate();
-        m_nGameId = 0;
+    auto& pClient = ra::services::ServiceLocator::GetMutable<ra::services::RcheevosClient>();
 
-        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Failed to download game data",
-                                                                  ra::Widen(response.ErrorMessage));
-        EndLoad();
-        return;
-    }
+    // enable spectator mode to prevent unlocks when testing compatibility
+    rc_client_set_spectator_mode_enabled(pClient.GetClient(), nMode == Mode::CompatibilityTest);
 
-    const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
-    const auto nServerConsoleId = ra::itoe<ConsoleID>(response.ConsoleId);
-    if (nServerConsoleId != pConsoleContext.Id())
+    const bool bWasPaused = pRuntime.IsPaused();
+    pRuntime.SetPaused(true);
+
+    struct LoadGameUserData
     {
-        if (nServerConsoleId == ConsoleID::GB && pConsoleContext.Id() == ConsoleID::GBC)
-        {
-            // GB memory map is just a subset of GBC, and GBC runs GB games, so allow loading a
-            // GB game in GBC context.
-        }
-        else if (nServerConsoleId == ConsoleID::DS && pConsoleContext.Id() == ConsoleID::DSi)
-        {
-            // DS memory map is just a subset of DSi, and DSi runs DS games, so allow loading a
-            // DS game in DSi context.
-        }
-        else if (ra::etoi(nServerConsoleId) == RC_CONSOLE_EVENTS)
-        {
-            // Hashes for Events are generated by modifying an existing ROM in a very specific manner
-            // To generate the correct hash, Events are loaded through the console associated to the
-            // unmodified ROM and therefore will never match the console context.
-        }
-        else
-        {
-            m_vAssets.EndUpdate();
-            m_nGameId = 0;
+        bool bWasPaused = false;
+        std::string sOldRichPresence;
+    }* pLoadGameUserData;
+    pLoadGameUserData = new LoadGameUserData;
+    pLoadGameUserData->bWasPaused = bWasPaused;
+    pLoadGameUserData->sOldRichPresence = std::move(sOldRichPresence);
+
+    pClient.BeginLoadGame(sGameHash, nGameId,
+        [](int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) {
+            auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
+            auto* pLoadGameUserData = static_cast<struct LoadGameUserData*>(pUserdata);
+            pGameContext.FinishLoadGame(nResult, sErrorMessage, pLoadGameUserData->bWasPaused,
+                                        pLoadGameUserData->sOldRichPresence);
+            delete pLoadGameUserData;
+        },
+        pLoadGameUserData);
+}
 
-            ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Identified game does not match expected console.",
-                ra::StringPrintf(L"The game being loaded is associated to the %s console, but the emulator has initialized "
-                    "the %s console. This is not allowed as the memory maps may not be compatible between consoles.",
-                    rc_console_name(response.ConsoleId), pConsoleContext.Name()));
+void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bWasPaused, const std::string& sOldRichPresence)
+{
+    if (nResult != RC_OK)
+    {
+        m_nGameId = 0;
 
-            EndLoad();
-            return;
-        }
+        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Failed to load game data",
+                                                                    ra::Widen(sErrorMessage));
     }
-
-    // start fetching the code notes
+    else
     {
         BeginLoad();
         auto pCodeNotes = std::make_unique<ra::data::models::CodeNotesModel>();
-        pCodeNotes->Refresh(m_nGameId,
+        pCodeNotes->Refresh(
+            m_nGameId,
             [this](ra::ByteAddress nAddress, const std::wstring& sNewNote) {
                 OnCodeNoteChanged(nAddress, sNewNote);
             },
             [this]() { EndLoad(); });
         m_vAssets.Append(std::move(pCodeNotes));
-    }
-
-    // game properties
-    m_sGameTitle = response.Title;
-    m_sGameImage = response.ImageIcon;
-
-    // rich presence
-    {
-        auto pRichPresence = std::make_unique<ra::data::models::RichPresenceModel>();
-        pRichPresence->SetScript(response.RichPresence);
-        pRichPresence->CreateServerCheckpoint();
-        pRichPresence->CreateLocalCheckpoint();
-        m_vAssets.Append(std::move(pRichPresence));
-    }
-
-    // achievements
-    const bool bWasPaused = pRuntime.IsPaused();
-    pRuntime.SetPaused(true);
-
-#ifndef RA_UTEST
-    auto& pImageRepository = ra::services::ServiceLocator::GetMutable<ra::ui::IImageRepository>();
-#endif
-
-    unsigned int nNumCoreAchievements = 0;
-    unsigned int nTotalCoreAchievementPoints = 0;
-    for (const auto& pAchievementData : response.Achievements)
-    {
-        // if the server has provided an unexpected category (usually 0), ignore it.
-        const auto nCategory = ra::itoe<ra::data::models::AssetCategory>(pAchievementData.CategoryId);
-        if (nCategory != ra::data::models::AssetCategory::Core && nCategory != ra::data::models::AssetCategory::Unofficial)
-            continue;
-
-        auto vmAchievement = std::make_unique<ra::data::models::AchievementModel>();
-        vmAchievement->SetID(pAchievementData.Id);
-        vmAchievement->SetName(ra::Widen(pAchievementData.Title));
-        vmAchievement->SetDescription(ra::Widen(pAchievementData.Description));
-        vmAchievement->SetCategory(nCategory);
-        vmAchievement->SetPoints(pAchievementData.Points);
-        vmAchievement->SetAuthor(ra::Widen(pAchievementData.Author));
-        vmAchievement->SetBadge(ra::Widen(pAchievementData.BadgeName));
-        vmAchievement->SetTrigger(pAchievementData.Definition);
-        vmAchievement->SetCreationTime(pAchievementData.Created);
-        vmAchievement->SetUpdatedTime(pAchievementData.Updated);
-        vmAchievement->CreateServerCheckpoint();
-        vmAchievement->CreateLocalCheckpoint();
-        m_vAssets.Append(std::move(vmAchievement));
-
-#ifndef RA_UTEST
-        // prefetch the achievement image
-        pImageRepository.FetchImage(ra::ui::ImageType::Badge, pAchievementData.BadgeName);
-#endif
 
-        if (nCategory == ra::data::models::AssetCategory::Core)
+        auto& pClient = ra::services::ServiceLocator::GetMutable<ra::services::RcheevosClient>();
+        auto* pGame = rc_client_get_game_info(pClient.GetClient());
+        if (pGame == nullptr || pGame->id == 0)
         {
-            ++nNumCoreAchievements;
-            nTotalCoreAchievementPoints += pAchievementData.Points;
+            // invalid hash
+        }
+        else if (!ValidateConsole(pGame->console_id))
+        {
+            m_nGameId = 0;
+            m_sGameTitle.clear();
+        }
+        else
+        {
+            rc_client_user_game_summary_t pSummary;
+            rc_client_get_user_game_summary(pClient.GetClient(), &pSummary);
+
+            // show "game loaded" popup
+            ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\info.wav");
+            std::wstring sDescription = ra::StringPrintf(L"%u achievements, %u points",
+                                                            pSummary.num_core_achievements, pSummary.points_core);
+            if (pSummary.num_unsupported_achievements)
+                sDescription += ra::StringPrintf(L" (%u unsupported)", pSummary.num_unsupported_achievements);
+
+            ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().QueueMessage(
+                ra::StringPrintf(L"Loaded %s", pGame->title), sDescription,
+                ra::StringPrintf(L"You have earned %u achievements", pSummary.num_unlocked_achievements),
+                ra::ui::ImageType::Icon, pGame->badge_name);
+
+            // merge local assets
+            std::vector<ra::data::models::AssetModelBase*> vEmptyAssetsList;
+            m_vAssets.ReloadAssets(vEmptyAssetsList);
         }
     }
 
-    // leaderboards
-    for (const auto& pLeaderboardData : response.Leaderboards)
+    if (!bWasPaused)
     {
-        auto vmLeaderboard = std::make_unique<ra::data::models::LeaderboardModel>();
-        vmLeaderboard->SetID(pLeaderboardData.Id);
-        vmLeaderboard->SetName(ra::Widen(pLeaderboardData.Title));
-        vmLeaderboard->SetDescription(ra::Widen(pLeaderboardData.Description));
-        vmLeaderboard->SetCategory(ra::data::models::AssetCategory::Core);
-        vmLeaderboard->SetValueFormat(ra::itoe<ValueFormat>(pLeaderboardData.Format));
-        vmLeaderboard->SetLowerIsBetter(pLeaderboardData.LowerIsBetter);
-        vmLeaderboard->SetHidden(pLeaderboardData.Hidden);
-        vmLeaderboard->SetDefinition(pLeaderboardData.Definition);
-        vmLeaderboard->CreateServerCheckpoint();
-        vmLeaderboard->CreateLocalCheckpoint();
-        m_vAssets.Append(std::move(vmLeaderboard));
-    }
-
-    ActivateLeaderboards();
-
-    // merge local assets
-    std::vector<ra::data::models::AssetModelBase*> vEmptyAssetsList;
-    m_vAssets.ReloadAssets(vEmptyAssetsList);
-
+        ra::services::ServiceLocator::GetMutable<ra::services::AchievementRuntime>().SetPaused(false);
 #ifndef RA_UTEST
-    DoFrame();
+        auto& pAssetList = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().AssetList;
+        pAssetList.SetProcessingActive(true);
 #endif
+    }
 
     // activate rich presence (or remove if not defined)
     auto* pRichPresence = m_vAssets.FindRichPresence();
@@ -287,16 +284,6 @@ void GameContext::LoadGame(unsigned int nGameId, Mode nMode)
         }
     }
 
-    // show "game loaded" popup
-    ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\info.wav");
-    const auto nPopup = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().QueueMessage(
-        ra::StringPrintf(L"Loaded %s", response.Title),
-        ra::StringPrintf(L"%u achievements, %u points", nNumCoreAchievements, nTotalCoreAchievementPoints),
-        ra::ui::ImageType::Icon, m_sGameImage);
-
-    // get user unlocks asynchronously
-    RefreshUnlocks(!bWasPaused, nPopup);
-
     // finish up
     m_vAssets.EndUpdate();
 
@@ -304,6 +291,126 @@ void GameContext::LoadGame(unsigned int nGameId, Mode nMode)
     OnActiveGameChanged();
 }
 
+void GameContext::InitializeFromRcheevosClient(const std::map<uint32_t, std::string> mAchievementDefinitions,
+                                               const std::map<uint32_t, std::string> mLeaderboardDefinitions)
+{
+    const auto* pClient = ra::services::ServiceLocator::Get<ra::services::RcheevosClient>().GetClient();
+    const auto* pGame = rc_client_get_game_info(pClient);
+    m_sGameTitle = ra::Widen(pGame->title);
+
+#ifndef RA_UTEST
+    auto& pImageRepository = ra::services::ServiceLocator::GetMutable<ra::ui::IImageRepository>();
+#endif
+
+    for (auto* pSubset = pClient->game->subsets; pSubset; pSubset = pSubset->next)
+    {
+        // achievements
+        auto* pAchievementData = pSubset->achievements;
+        const auto* pAchievementStop = pAchievementData + pSubset->public_.num_achievements;
+        for (; pAchievementData < pAchievementStop; ++pAchievementData)
+        {
+            // if the server has provided an unexpected category (usually 0), ignore it.
+            ra::data::models::AssetCategory nCategory = ra::data::models::AssetCategory::None;
+            switch (pAchievementData->public_.category)
+            {
+                case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE:
+                    nCategory = ra::data::models::AssetCategory::Core;
+                    break;
+                case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL:
+                    nCategory = ra::data::models::AssetCategory::Unofficial;
+
+                    // all unofficial achievements should start inactive.
+                    // rc_client automatically activates them.
+                    pAchievementData->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE;
+                    break;
+                default:
+                    continue;
+            }
+
+            auto vmAchievement = std::make_unique<ra::data::models::AchievementModel>();
+            vmAchievement->SetID(pAchievementData->public_.id);
+            vmAchievement->SetName(ra::Widen(pAchievementData->public_.title));
+            vmAchievement->SetDescription(ra::Widen(pAchievementData->public_.description));
+            vmAchievement->SetCategory(nCategory);
+            vmAchievement->SetPoints(pAchievementData->public_.points);
+            vmAchievement->SetAuthor(ra::Widen(pAchievementData->author));
+            vmAchievement->SetBadge(ra::Widen(pAchievementData->public_.badge_name));
+            vmAchievement->SetCreationTime(pAchievementData->created_time);
+            vmAchievement->SetUpdatedTime(pAchievementData->updated_time);
+
+            const auto sDefinition = mAchievementDefinitions.find(pAchievementData->public_.id);
+            if (sDefinition != mAchievementDefinitions.end())
+                vmAchievement->SetTrigger(sDefinition->second);
+
+            vmAchievement->CreateServerCheckpoint();
+            vmAchievement->CreateLocalCheckpoint();
+
+            switch (pAchievementData->public_.state)
+            {
+                case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE:
+                    vmAchievement->SetState(ra::data::models::AssetState::Active);
+                    break;
+                case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE:
+                    vmAchievement->SetState(ra::data::models::AssetState::Inactive);
+                    break;
+                case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED:
+                    vmAchievement->SetState(ra::data::models::AssetState::Triggered);
+                    break;
+                case RC_CLIENT_ACHIEVEMENT_STATE_DISABLED:
+                    vmAchievement->SetState(ra::data::models::AssetState::Disabled);
+                    break;
+            }
+
+            m_vAssets.Append(std::move(vmAchievement));
+
+#ifndef RA_UTEST
+            // prefetch the achievement image
+            pImageRepository.FetchImage(ra::ui::ImageType::Badge, pAchievementData->public_.badge_name);
+#endif
+        }
+
+        // leaderboards
+        auto* pLeaderboardData = pSubset->leaderboards;
+        const auto* pLeaderboardStop = pLeaderboardData + pSubset->public_.num_leaderboards;
+        for (; pLeaderboardData < pLeaderboardStop; ++pLeaderboardData)
+        {
+            auto vmLeaderboard = std::make_unique<ra::data::models::LeaderboardModel>();
+            vmLeaderboard->SetID(pLeaderboardData->public_.id);
+            vmLeaderboard->SetName(ra::Widen(pLeaderboardData->public_.title));
+            vmLeaderboard->SetDescription(ra::Widen(pLeaderboardData->public_.description));
+            vmLeaderboard->SetCategory(ra::data::models::AssetCategory::Core);
+            vmLeaderboard->SetValueFormat(ra::itoe<ValueFormat>(pLeaderboardData->format));
+            vmLeaderboard->SetLowerIsBetter(pLeaderboardData->public_.lower_is_better);
+            vmLeaderboard->SetHidden(pLeaderboardData->hidden);
+
+            const auto sDefinition = mLeaderboardDefinitions.find(pLeaderboardData->public_.id);
+            if (sDefinition != mLeaderboardDefinitions.end())
+                vmLeaderboard->SetDefinition(sDefinition->second);
+
+            vmLeaderboard->CreateServerCheckpoint();
+            vmLeaderboard->CreateLocalCheckpoint();
+
+            switch (pLeaderboardData->public_.state)
+            {
+                case RC_CLIENT_LEADERBOARD_STATE_ACTIVE:
+                    vmLeaderboard->SetState(ra::data::models::AssetState::Active);
+                    break;
+                case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
+                    vmLeaderboard->SetState(ra::data::models::AssetState::Inactive);
+                    break;
+                case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
+                    vmLeaderboard->SetState(ra::data::models::AssetState::Primed);
+                    break;
+                case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
+                    vmLeaderboard->SetState(ra::data::models::AssetState::Disabled);
+                    break;
+            }
+
+            m_vAssets.Append(std::move(vmLeaderboard));
+        }
+    }
+}
+
 void GameContext::OnBeforeActiveGameChanged()
 {
     // create a copy of the list of pointers in case it's modified by one of the callbacks
@@ -356,25 +463,35 @@ void GameContext::EndLoad()
 
 void GameContext::RefreshUnlocks(bool bUnpause, int nPopup)
 {
-    if (m_nMode == Mode::CompatibilityTest)
+    std::set<unsigned int> vUnlockedAchievements;
+
+    if (m_nGameId != 0 && m_nMode != Mode::CompatibilityTest)
     {
-        std::set<unsigned int> vUnlockedAchievements;
-        UpdateUnlocks(vUnlockedAchievements, bUnpause, nPopup);
-        return;
-    }
+        const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
+        const bool bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
+        const uint8_t nFlag = gsl::narrow_cast<uint8_t>(bHardcore ? ra::etoi(RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE)
+                                                                  : ra::etoi(RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE));
 
-    BeginLoad();
+        const auto* pClient = ra::services::ServiceLocator::Get<ra::services::RcheevosClient>().GetClient();
+        if (!pClient->game) // game still loading, ignore
+            return;
 
-    const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
+        for (auto* pSubset = pClient->game->subsets; pSubset; pSubset = pSubset->next)
+        {
+            // achievements
+            auto* pAchievementData = pSubset->achievements;
+            const auto* pAchievementStop = pAchievementData + pSubset->public_.num_achievements;
+            for (; pAchievementData < pAchievementStop; ++pAchievementData)
+            {
+                if (pAchievementData->public_.unlocked & nFlag)
+                    vUnlockedAchievements.insert(pAchievementData->public_.id);
+            }
+        }
+    }
 
-    ra::api::FetchUserUnlocks::Request request;
-    request.GameId = m_nGameId;
-    request.Hardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
-    request.CallAsync([this, bUnpause, nPopup](const ra::api::FetchUserUnlocks::Response& response)
-    {
-        UpdateUnlocks(response.UnlockedAchievements, bUnpause, nPopup);
-        EndLoad();
-    });
+    BeginLoad();
+    UpdateUnlocks(vUnlockedAchievements, bUnpause, nPopup);
+    EndLoad();
 }
 
 void GameContext::UpdateUnlocks(const std::set<unsigned int>& vUnlockedAchievements, bool bUnpause, int nPopup)
diff --git a/src/data/context/GameContext.hh b/src/data/context/GameContext.hh
index 17cfa2d7..6d4ed89d 100644
--- a/src/data/context/GameContext.hh
+++ b/src/data/context/GameContext.hh
@@ -30,7 +30,13 @@ public:
     /// <summary>
     /// Loads the data for the specified game id.
     /// </summary>
-    virtual void LoadGame(unsigned int nGameId, Mode nMode = Mode::Normal);
+    virtual void LoadGame(unsigned int nGameId, const std::string& sGameHash, Mode nMode = Mode::Normal);
+
+    /// <summary>
+    /// Loads the data from the game currently loaded in the RcheevosClient.
+    /// </summary>
+    void InitializeFromRcheevosClient(const std::map<uint32_t, std::string> mAchievementDefinitions,
+                                      const std::map<uint32_t, std::string> mLeaderboardDefinitions);
 
     /// <summary>
     /// Determines whether a game is being loaded.
@@ -129,6 +135,7 @@ public:
 
 private:
     using NotifyTargetSet = std::set<NotifyTarget*>;
+    void FinishLoadGame(int nResult, const char* sErrorMessage, bool bWasPaused, const std::string& sOldRichPresence);
 
 protected:
     void RefreshUnlocks(bool bUnpause, int nPopup);
diff --git a/src/data/models/TriggerValidation.cpp b/src/data/models/TriggerValidation.cpp
index c6f3981c..54785dfb 100644
--- a/src/data/models/TriggerValidation.cpp
+++ b/src/data/models/TriggerValidation.cpp
@@ -122,6 +122,11 @@ bool TriggerValidation::Validate(const std::string& sTrigger, std::wstring& sErr
             nResult = rc_validate_trigger_for_console(pTrigger, sErrorBuffer, sizeof(sErrorBuffer), ra::etoi(pConsoleContext.Id()));
         }
     }
+    else
+    {
+        // shouldn't get here, but if we do (unit tests), validate the logic but not the addresses.
+        nResult = rc_validate_trigger(pTrigger, sErrorBuffer, sizeof(sErrorBuffer), 0xFFFFFFFF);
+    }
 
     if (nResult)
     {
diff --git a/src/services/GameIdentifier.cpp b/src/services/GameIdentifier.cpp
index 09acba96..3f70b48b 100644
--- a/src/services/GameIdentifier.cpp
+++ b/src/services/GameIdentifier.cpp
@@ -180,7 +180,7 @@ void GameIdentifier::ActivateGame(unsigned int nGameId)
         pOverlayManager.ClearPopups();
 
         auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
-        pGameContext.LoadGame(nGameId, m_nPendingMode);
+        pGameContext.LoadGame(nGameId, m_sPendingHash, m_nPendingMode);
         pGameContext.SetGameHash((nGameId == m_nPendingGameId) ? m_sPendingHash : "");
 
         ra::services::ServiceLocator::GetMutable<ra::data::context::SessionTracker>().BeginSession(nGameId);
@@ -224,7 +224,7 @@ void GameIdentifier::ActivateGame(unsigned int nGameId)
         pOverlayManager.ClearPopups();
 
         auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
-        pGameContext.LoadGame(0U, m_nPendingMode);
+        pGameContext.LoadGame(0U, m_sPendingHash, m_nPendingMode);
         pGameContext.SetGameHash((m_nPendingGameId == 0) ? m_sPendingHash : "");
     }
 
diff --git a/src/services/RcheevosClient.cpp b/src/services/RcheevosClient.cpp
index 66113b47..2b02644b 100644
--- a/src/services/RcheevosClient.cpp
+++ b/src/services/RcheevosClient.cpp
@@ -6,6 +6,7 @@
 
 #include "services/Http.hh"
 #include "services/IAudioSystem.hh"
+#include "services/ILocalStorage.hh"
 #include "services/ServiceLocator.hh"
 
 #include "ui/viewmodels/MessageBoxViewModel.hh"
@@ -16,6 +17,8 @@
 #include "RA_Log.h"
 #include "RA_StringUtils.h"
 
+#include <rcheevos\include\rc_api_runtime.h>
+#include <rcheevos\src\rapi\rc_api_common.h> // for parsing cached patchdata response
 #include <rcheevos\src\rcheevos\rc_internal.h>
 #include <rcheevos\src\rcheevos\rc_client_internal.h>
 
@@ -24,11 +27,52 @@
 namespace ra {
 namespace services {
 
+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& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
+    if (pEmulatorContext.WasMemoryModified())
+        return 0;
+
+    if (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->GetChanges() != ra::data::models::AssetChanges::None)
+        return 0;
+
+    return 1;
+}
+
+static int CanSubmitLeaderboardEntry(uint32_t, rc_client_t*)
+{
+    return CanSubmit();
+}
+
 RcheevosClient::RcheevosClient()
 {
     m_pClient.reset(rc_client_create(RcheevosClient::ReadMemory, RcheevosClient::ServerCallAsync));
 
+    m_pClient->callbacks.can_submit_achievement_unlock = CanSubmitAchievementUnlock;
+    m_pClient->callbacks.can_submit_leaderboard_entry = CanSubmitLeaderboardEntry;
+
+#ifndef RA_UTEST
     rc_client_enable_logging(m_pClient.get(), RC_CLIENT_LOG_LEVEL_VERBOSE, RcheevosClient::LogMessage);
+#endif
+
+    rc_client_set_unofficial_enabled(m_pClient.get(), 1);
 }
 
 RcheevosClient::~RcheevosClient()
@@ -147,6 +191,122 @@ void RcheevosClient::LoginCallback(int nResult, const char* sErrorMessage,
     delete wrapper;
 }
 
+/* ---- Load Game ----- */
+
+void RcheevosClient::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 ExtractPatchData(const rc_api_server_response_t* server_response, uint32_t nGameId)
+{
+    // extract the PatchData and store a copy in the cache for offline mode
+    rc_api_response_t api_response{};
+    GSL_SUPPRESS_ES47
+    rc_json_field_t fields[] = {
+        RC_JSON_NEW_FIELD("Success"),
+        RC_JSON_NEW_FIELD("Error"),
+        RC_JSON_NEW_FIELD("PatchData")
+    };
+
+
+    if (rc_json_parse_server_response(&api_response, server_response, fields, sizeof(fields) / sizeof(fields[0])) == RC_OK &&
+        fields[2].value_start && fields[2].value_end)
+    {
+        auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
+        auto pData = pLocalStorage.WriteText(ra::services::StorageItemType::GameData, std::to_wstring(nGameId));
+        if (pData != nullptr)
+        {
+            std::string sPatchData;
+            sPatchData.append(fields[2].value_start, fields[2].value_end - fields[2].value_start);
+            pData->Write(sPatchData);
+        }
+    }
+}
+
+GSL_SUPPRESS_CON3
+void RcheevosClient::PostProcessGameDataResponse(const rc_api_server_response_t* server_response,
+    struct rc_api_fetch_game_data_response_t* game_data_response,
+    rc_client_t* client, void* pUserdata)
+{
+    auto* wrapper = static_cast<LoadGameCallbackWrapper*>(pUserdata);
+    Expects(wrapper != 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
+    if (client->game)
+    {
+        auto& pImageRepository = ra::services::ServiceLocator::GetMutable<ra::ui::IImageRepository>();
+        pImageRepository.FetchImage(ra::ui::ImageType::Icon, client->game->public_.badge_name);
+    }
+#else
+    (void*)client;
+#endif
+
+    const rc_api_achievement_definition_t* pAchievement = game_data_response->achievements;
+    const rc_api_achievement_definition_t* pAchievementStop = pAchievement + game_data_response->num_achievements;
+    for (; pAchievement < pAchievementStop; ++pAchievement)
+        wrapper->m_mAchievementDefinitions[pAchievement->id] = pAchievement->definition;
+
+    const rc_api_leaderboard_definition_t* pLeaderboard = game_data_response->leaderboards;
+    const rc_api_leaderboard_definition_t* pLeaderboardStop = pLeaderboard + game_data_response->num_leaderboards;
+    for (; pLeaderboard < pLeaderboardStop; ++pLeaderboard)
+        wrapper->m_mLeaderboardDefinitions[pLeaderboard->id] = pLeaderboard->definition;
+
+    ExtractPatchData(server_response, game_data_response->id);
+}
+
+rc_client_async_handle_t* RcheevosClient::BeginLoadGame(const char* sHash, unsigned id,
+                                                        CallbackWrapper* pCallbackWrapper) noexcept
+{
+    auto* client = GetClient();
+
+    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;
+    }
+
+    client->callbacks.post_process_game_data_response = PostProcessGameDataResponse;
+
+    return rc_client_begin_load_game(client, sHash, RcheevosClient::LoadGameCallback, pCallbackWrapper);
+}
+
+GSL_SUPPRESS_CON3
+void RcheevosClient::LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata)
+{
+    auto* wrapper = static_cast<LoadGameCallbackWrapper*>(pUserdata);
+    Expects(wrapper != nullptr);
+
+    if (nResult == RC_OK || nResult == RC_NO_GAME_LOADED)
+    {
+        // initialize the game context
+        auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
+        pGameContext.InitializeFromRcheevosClient(wrapper->m_mAchievementDefinitions,
+                                                  wrapper->m_mLeaderboardDefinitions);
+    }
+    else
+    {
+
+    }
+
+    wrapper->DoCallback(nResult, sErrorMessage);
+
+    delete wrapper;
+}
+
 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
 
 class RcheevosClientExports : private RcheevosClient
diff --git a/src/services/RcheevosClient.hh b/src/services/RcheevosClient.hh
index 3b0991c4..11ac163e 100644
--- a/src/services/RcheevosClient.hh
+++ b/src/services/RcheevosClient.hh
@@ -13,6 +13,9 @@
 
 #include <rcheevos\include\rc_client.h>
 
+struct rc_api_fetch_game_data_response_t;
+
+
 namespace ra {
 namespace services {
 
@@ -36,6 +39,9 @@ public:
     void BeginLoginWithPassword(const std::string& sUsername, const std::string& sPassword,
                                 rc_client_callback_t fCallback, void* pCallbackData);
 
+    void BeginLoadGame(const std::string& sHash, unsigned id,
+                       rc_client_callback_t fCallback, void* pCallbackData);
+
     class Synchronizer
     {
     public:
@@ -44,7 +50,7 @@ public:
 #ifdef RA_UTEST
             // unit tests are single-threaded. if m_bWaiting is true, it would wait indefinitely.
             if (m_bWaiting)
-                throw std::runtime_error("Sycnhronous request was not handled");
+                Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(L"Sycnhronous request was not handled.");
 #else
             std::unique_lock<std::mutex> lock(m_pMutex);
             if (m_bWaiting)
@@ -58,9 +64,17 @@ public:
             m_pCondVar.notify_all();
         }
 
+        void SetErrorMessage(const std::string& sErrorMessage)
+        {
+            m_sErrorMessage = sErrorMessage;
+        }
+
+        const std::string& GetErrorMessage() const noexcept { return m_sErrorMessage; }
+
     private:
         std::mutex m_pMutex;
         std::condition_variable m_pCondVar;
+        std::string m_sErrorMessage;
         bool m_bWaiting = true;
     };
 
@@ -107,6 +121,24 @@ private:
     rc_client_async_handle_t* BeginLoginWithToken(const char* sUsername, const char* sApiToken,
                                                   CallbackWrapper* pCallbackWrapper) noexcept;
     static void LoginCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata);
+
+    class LoadGameCallbackWrapper : public CallbackWrapper
+    {
+    public:
+        LoadGameCallbackWrapper(rc_client_t* client, rc_client_callback_t callback, void* callback_userdata) noexcept :
+            CallbackWrapper(client, callback, callback_userdata)
+        {}
+
+        std::map<uint32_t, std::string> m_mAchievementDefinitions;
+        std::map<uint32_t, std::string> m_mLeaderboardDefinitions;
+    };
+
+    rc_client_async_handle_t* BeginLoadGame(const char* sHash, unsigned id, CallbackWrapper* pCallbackWrapper) noexcept;
+    static void LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata);
+
+    static void PostProcessGameDataResponse(const rc_api_server_response_t* server_response,
+                                            struct rc_api_fetch_game_data_response_t* game_data_response,
+                                            rc_client_t* client, void* pUserdata);
 };
 
 } // namespace services
diff --git a/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp b/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp
index 50298140..c11ce943 100644
--- a/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp
+++ b/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp
@@ -2,14 +2,19 @@
 
 #include "RA_StringUtils.h"
 
-#include "api\FetchGameData.hh"
 #include "api\impl\OfflineServer.hh"
 
 #include "data\context\GameContext.hh"
 #include "data\context\SessionTracker.hh"
+#include "data\context\UserContext.hh"
 
+#include "services\Http.hh"
+#include "services\IConfiguration.hh"
+#include "services\ILocalStorage.hh"
 #include "services\IThreadPool.hh"
 
+#include <rcheevos\include\rc_api_runtime.h>
+
 namespace ra {
 namespace ui {
 namespace viewmodels {
@@ -23,6 +28,8 @@ void OverlayRecentGamesPageViewModel::Refresh()
         m_vItems.RemoveAt(nIndex);
 
     std::vector<unsigned int> vPendingGames;
+    const bool bOffline = ra::services::ServiceLocator::Get<ra::services::IConfiguration>()
+        .IsFeatureEnabled(ra::services::Feature::Offline);
 
     const auto& pSessionTracker = ra::services::ServiceLocator::Get<ra::data::context::SessionTracker>();
     for (const auto& pGameStats : pSessionTracker.SessionData())
@@ -41,6 +48,10 @@ void OverlayRecentGamesPageViewModel::Refresh()
         {
             UpdateGameEntry(pvmItem, pIter->second, pIter2->second, pGameStats.TotalPlayTime);
         }
+        else if (bOffline)
+        {
+            UpdateGameEntry(pvmItem, L"Unknown", "00000", pGameStats.TotalPlayTime);
+        }
         else
         {
             UpdateGameEntry(pvmItem, L"Loading", "00000", pGameStats.TotalPlayTime);
@@ -56,21 +67,50 @@ void OverlayRecentGamesPageViewModel::Refresh()
         ra::services::ServiceLocator::GetMutable<ra::services::IThreadPool>().RunAsync(
             [this, vPendingGames = std::move(vPendingGames)]()
         {
-            ra::api::impl::OfflineServer cache;
+            const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
+            rc_api_fetch_game_data_request_t api_params;
+            memset(&api_params, 0, sizeof(api_params));
+            api_params.username = pUserContext.GetUsername().c_str();
+            api_params.api_token = pUserContext.GetApiToken().c_str();
+
+            auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
             for (const auto nGameId : vPendingGames)
             {
-                ra::api::FetchGameData::Request request;
-                request.GameId = nGameId;
-                const auto response = cache.FetchGameData(request);
-                if (response.Succeeded())
+                auto pData = pLocalStorage.ReadText(ra::services::StorageItemType::GameData, std::to_wstring(nGameId));
+                if (pData != nullptr)
                 {
-                    UpdateGameEntry(nGameId, response.Title, response.ImageIcon);
+                    std::string sContents = "{\"Success\":true,\"PatchData\":";
+                    std::string sLine;
+                    while (pData->GetLine(sLine))
+                    {
+                        if (!sContents.empty())
+                            sContents.push_back('\n');
+
+                        sContents.append(sLine);
+                    }
+                    sContents.push_back('}');
+
+                    rc_api_fetch_game_data_response_t response;
+                    if (rc_api_process_fetch_game_data_response(&response, sContents.c_str()) == RC_OK)
+                        UpdateGameEntry(nGameId, ra::Widen(response.title), response.image_name);
                 }
                 else
                 {
-                    request.CallAsync([this, nGameId](const ra::api::FetchGameData::Response & response)
+                    api_params.game_id = nGameId;
+
+                    rc_api_request_t request;
+                    if (rc_api_init_fetch_game_data_request(&request, &api_params) != RC_OK)
+                        continue;
+
+                    ra::services::Http::Request httpRequest(request.url);
+                    httpRequest.SetContentType(request.content_type);
+                    httpRequest.SetPostData(request.post_data);
+
+                    httpRequest.CallAsync([this, nGameId](const ra::services::Http::Response& pResponse)
                     {
-                        UpdateGameEntry(nGameId, response.Title, response.ImageIcon);
+                        rc_api_fetch_game_data_response_t response;
+                        if (rc_api_process_fetch_game_data_response(&response, pResponse.Content().c_str()) == RC_OK)
+                            UpdateGameEntry(nGameId, ra::Widen(response.title), response.image_name);
                     });
                 }
             }


Test patch:
diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj
index 8adf45b0..db038db6 100644
--- a/tests/RA_Integration.Tests.vcxproj
+++ b/tests/RA_Integration.Tests.vcxproj
@@ -382,6 +382,7 @@
     <ClCompile Include="data\models\RichPresenceModel_Tests.cpp" />
     <ClCompile Include="data\models\TriggerValidation_Tests.cpp" />
     <ClCompile Include="Exports_Tests.cpp" />
+    <ClCompile Include="mocks\MockRcheevosClient.cpp" />
     <ClCompile Include="services\AchievementRuntime_Tests.cpp" />
     <ClCompile Include="services\FileLocalStorage_Tests.cpp" />
     <ClCompile Include="services\FrameEventQueue_Tests.cpp" />
diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters
index db594b54..b404634e 100644
--- a/tests/RA_Integration.Tests.vcxproj.filters
+++ b/tests/RA_Integration.Tests.vcxproj.filters
@@ -462,6 +462,9 @@
     <ClCompile Include="..\src\services\RcheevosClient.cpp">
       <Filter>Code</Filter>
     </ClCompile>
+    <ClCompile Include="mocks\MockRcheevosClient.cpp">
+      <Filter>Mocks</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <None Include="base.props" />
diff --git a/tests/api/ConnectedServer_Tests.cpp b/tests/api/ConnectedServer_Tests.cpp
index c839e105..964db0c7 100644
--- a/tests/api/ConnectedServer_Tests.cpp
+++ b/tests/api/ConnectedServer_Tests.cpp
@@ -439,38 +439,6 @@ public:
         Assert::AreEqual(std::string(), response.ErrorMessage);
     }
 
-    // ====================================================
-    // FetchGameData
-
-    TEST_METHOD(TestFetchGameDataCachesPatchData)
-    {
-        MockUserContext mockUserContext;
-        mockUserContext.Initialize("Username", "ApiToken");
-
-        MockHttpRequester mockHttp([](const Http::Request&)
-        {
-            return Http::Response(Http::StatusCode::OK,
-                "{\"Success\":true,\"PatchData\":"
-                    "{\"ID\":99, \"Title\":\"Game Name\", \"ConsoleID\":5, \"ImageIcon\":\"/Images/BADGE.png\", \"Achievements\":[], \"Leaderboards\":[]}"
-                "}");
-        });
-
-        MockLocalStorage mockLocalStorage;
-
-        ra::services::ServiceLocator::ServiceOverride<ra::api::IServer> serviceOverride(new ConnectedServer("host.com"), true);
-        auto& server = ra::services::ServiceLocator::GetMutable<ra::api::IServer>();
-
-        FetchGameData::Request request;
-        request.GameId = 99;
-        auto response = server.FetchGameData(request);
-
-        Assert::AreEqual(ApiResult::Success, response.Result);
-        Assert::AreEqual(std::string(), response.ErrorMessage);
-
-        std::string sPatchData = "{\"ID\":99, \"Title\":\"Game Name\", \"ConsoleID\":5, \"ImageIcon\":\"/Images/BADGE.png\", \"Achievements\":[], \"Leaderboards\":[]}";
-        Assert::AreEqual(sPatchData, mockLocalStorage.GetStoredData(ra::services::StorageItemType::GameData, L"99"));
-    }
-
     // ====================================================
     // FetchCodeNotes
 
diff --git a/tests/data/context/EmulatorContext_Tests.cpp b/tests/data/context/EmulatorContext_Tests.cpp
index 2459b05f..aca34ff9 100644
--- a/tests/data/context/EmulatorContext_Tests.cpp
+++ b/tests/data/context/EmulatorContext_Tests.cpp
@@ -14,6 +14,7 @@
 #include "tests\mocks\MockHttpRequester.hh"
 #include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockOverlayManager.hh"
+#include "tests\mocks\MockRcheevosClient.hh"
 #include "tests\mocks\MockServer.hh"
 #include "tests\mocks\MockThreadPool.hh"
 #include "tests\mocks\MockUserContext.hh"
@@ -41,6 +42,7 @@ private:
         ra::services::mocks::MockFileSystem mockFileSystem;
         ra::services::mocks::MockHttpRequester mockHttpRequester;
         ra::services::mocks::MockThreadPool mockThreadPool;
+        ra::services::mocks::MockRcheevosClient mockRcheevosClient;
         ra::ui::mocks::MockDesktop mockDesktop;
         ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager;
         ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;
@@ -48,6 +50,7 @@ private:
         GSL_SUPPRESS_F6 EmulatorContextHarness() noexcept
             : mockHttpRequester([](const ra::services::Http::Request&) { return ra::services::Http::Response(ra::services::Http::StatusCode::OK, ""); })
         {
+            mockRcheevosClient.MockGame();
         }
 
         void MockVersions(const std::string& sClientVersion, const std::string& sServerVersion, const std::string& sMinimumVersion)
@@ -662,15 +665,21 @@ public:
         pLeaderboard.SetState(ra::data::models::AssetState::Active);
         Assert::IsTrue(pLeaderboard.IsActive());
 
-        bool bUnlocksRequested = false;
-        emulator.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([&bUnlocksRequested](const ra::api::FetchUserUnlocks::Request& request, ra::api::FetchUserUnlocks::Response& response)
-        {
-            bUnlocksRequested = true;
-            Assert::AreEqual(1234U, request.GameId);
-            Assert::IsFalse(request.Hardcore);
-            response.Result = ra::api::ApiResult::Success;
-            return true;
-        });
+        emulator.mockRcheevosClient.MockAchievement(44U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE;
+        emulator.mockRcheevosClient.MockAchievement(45U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
+        emulator.mockRcheevosClient.MockAchievement(46U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH;
+
+        auto& pAchievement44 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement44.SetID(44U);
+        pAchievement44.SetState(ra::data::models::AssetState::Active);
+        auto& pAchievement45 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement45.SetID(45U);
+        auto& pAchievement46 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement46.SetID(46U);
+
+        Assert::IsTrue(pAchievement44.IsActive());
+        Assert::IsFalse(pAchievement45.IsActive());
+        Assert::IsFalse(pAchievement46.IsActive());
 
         emulator.DisableHardcoreMode();
 
@@ -680,9 +689,10 @@ public:
         // deactivating hardcore mode should not disable leaderboards
         Assert::IsTrue(pLeaderboard.IsActive());
 
-        // deactivating hardcore mode should request non-hardcore unlocks
-        emulator.mockThreadPool.ExecuteNextTask();
-        Assert::IsTrue(bUnlocksRequested);
+        // deactivating hardcore mode should update active achievements
+        Assert::IsTrue(pAchievement44.IsActive());
+        Assert::IsFalse(pAchievement45.IsActive());
+        Assert::IsFalse(pAchievement46.IsActive());
 
         // deactivating hardcore mode should not reset the emulator
         Assert::IsFalse(bWasReset);
@@ -732,15 +742,16 @@ public:
             return ra::ui::DialogResult::Yes;
         });
 
-        bool bUnlocksRequested = false;
-        emulator.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([&bUnlocksRequested](const ra::api::FetchUserUnlocks::Request& request, ra::api::FetchUserUnlocks::Response& response)
-        {
-            bUnlocksRequested = true;
-            Assert::AreEqual(1U, request.GameId);
-            Assert::IsTrue(request.Hardcore);
-            response.Result = ra::api::ApiResult::Success;
-            return true;
-        });
+        emulator.mockRcheevosClient.MockAchievement(44U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE;
+        emulator.mockRcheevosClient.MockAchievement(45U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
+        emulator.mockRcheevosClient.MockAchievement(46U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH;
+
+        auto& pAchievement44 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement44.SetID(44U);
+        auto& pAchievement45 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement45.SetID(45U);
+        auto& pAchievement46 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement46.SetID(46U);
 
         auto& pLeaderboard = emulator.mockGameContext.Assets().NewLeaderboard();
         pLeaderboard.SetID(32U);
@@ -760,9 +771,10 @@ public:
         // enabling hardcore mode should enable leaderboards
         Assert::IsTrue(pLeaderboard.IsActive());
 
-        // enabling hardcore mode should request hardcore unlocks
-        emulator.mockThreadPool.ExecuteNextTask();
-        Assert::IsTrue(bUnlocksRequested);
+        // enabling hardcore mode should activate achievements not unlocked in hardcore
+        Assert::IsTrue(pAchievement44.IsActive());
+        Assert::IsTrue(pAchievement45.IsActive());
+        Assert::IsFalse(pAchievement46.IsActive());
 
         // enabling hardcore mode should reset the emulator
         Assert::IsTrue(bWasReset);
@@ -878,15 +890,16 @@ public:
         emulator.mockGameContext.SetGameId(1U);
         emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
 
-        bool bUnlocksRequested = false;
-        emulator.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([&bUnlocksRequested](const ra::api::FetchUserUnlocks::Request& request, ra::api::FetchUserUnlocks::Response& response)
-        {
-            bUnlocksRequested = true;
-            Assert::AreEqual(1U, request.GameId);
-            Assert::IsTrue(request.Hardcore);
-            response.Result = ra::api::ApiResult::Success;
-            return true;
-        });
+        emulator.mockRcheevosClient.MockAchievement(44U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE;
+        emulator.mockRcheevosClient.MockAchievement(45U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
+        emulator.mockRcheevosClient.MockAchievement(46U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH;
+
+        auto& pAchievement44 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement44.SetID(44U);
+        auto& pAchievement45 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement45.SetID(45U);
+        auto& pAchievement46 = emulator.mockGameContext.Assets().NewAchievement();
+        pAchievement46.SetID(46U);
 
         auto& pLeaderboard = emulator.mockGameContext.Assets().NewLeaderboard();
         pLeaderboard.SetID(32U);
@@ -906,9 +919,10 @@ public:
         // enabling hardcore mode should enable leaderboards
         Assert::IsTrue(pLeaderboard.IsActive());
 
-        // enabling hardcore mode should request hardcore unlocks
-        emulator.mockThreadPool.ExecuteNextTask();
-        Assert::IsTrue(bUnlocksRequested);
+        // enabling hardcore mode should activate achievements not unlocked in hardcore
+        Assert::IsTrue(pAchievement44.IsActive());
+        Assert::IsTrue(pAchievement45.IsActive());
+        Assert::IsFalse(pAchievement46.IsActive());
 
         // enabling hardcore mode should reset the emulator
         Assert::IsTrue(bWasReset);
diff --git a/tests/data/context/GameContext_Tests.cpp b/tests/data/context/GameContext_Tests.cpp
index cd8fa36e..6b73ff37 100644
--- a/tests/data/context/GameContext_Tests.cpp
+++ b/tests/data/context/GameContext_Tests.cpp
@@ -8,6 +8,8 @@
 
 #include "ui\viewmodels\MessageBoxViewModel.hh"
 
+#include <rcheevos/src/rcheevos/rc_version.h>
+
 #include "tests\RA_UnitTestHelpers.h"
 #include "tests\data\DataAsserts.hh"
 
@@ -19,6 +21,7 @@
 #include "tests\mocks\MockDesktop.hh"
 #include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockOverlayManager.hh"
+#include "tests\mocks\MockRcheevosClient.hh"
 #include "tests\mocks\MockServer.hh"
 #include "tests\mocks\MockSessionTracker.hh"
 #include "tests\mocks\MockThreadPool.hh"
@@ -62,6 +65,7 @@ public:
         ra::services::mocks::MockLocalStorage mockStorage;
         ra::services::mocks::MockThreadPool mockThreadPool;
         ra::services::mocks::MockAudioSystem mockAudioSystem;
+        ra::services::mocks::MockRcheevosClient mockRcheevosClient;
         ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager;
         ra::data::context::mocks::MockConsoleContext mockConsoleContext;
         ra::data::context::mocks::MockEmulatorContext mockEmulator;
@@ -141,6 +145,45 @@ public:
             }
         }
 
+        void MockLoadGameAPIs(unsigned nGameID, const std::string& sHash,
+            const std::string& sUnlocks = "", const std::string& sAchievements = "",
+            const std::string& sLeaderboards = "", const std::string& sRichPresence = "",
+            ConsoleID nConsoleID = ConsoleID::MegaDrive)
+        {
+            mockConsoleContext.SetId(nConsoleID);
+            mockConsoleContext.SetName(ra::Widen(rc_console_name(static_cast<int>(nConsoleID))));
+
+            mockRcheevosClient.MockUser("Username", "ApiToken");
+            mockRcheevosClient.MockResponse("r=gameid&m=" + sHash,
+                "{\"Success\":true,\"GameID\":" + std::to_string(nGameID) + "}");
+            mockRcheevosClient.MockResponse(
+                "r=patch&u=Username&t=ApiToken&g=" + std::to_string(nGameID),
+                "{\"Success\":true,\"PatchData\":{"
+                    "\"ID\":" + std::to_string(nGameID) + ","
+                    "\"Title\":\"GameTitle\","
+                    "\"ConsoleID\":" + std::to_string(ra::etoi(nConsoleID)) + ","
+                    "\"ImageIcon\":\"/Images/9743.png\","
+                    "\"Achievements\":[" + sAchievements + "],"
+                    "\"Leaderboards\":[" + sLeaderboards + "],"
+                    "\"RichPresencePatch\":\"" + sRichPresence + "\""
+                "}}");
+            mockRcheevosClient.MockResponse(
+                "r=startsession&u=Username&t=ApiToken&g=" + std::to_string(nGameID) +
+                                                "&l=" RCHEEVOS_VERSION_STRING,
+                sUnlocks.empty() ? "{\"Success\":true}" : "{\"Success\":true," + sUnlocks + "}");
+        }
+
+        std::string MockAchievementJson(unsigned nAchievementID, unsigned nPoints,
+            const std::string sMemAddr = "1=1")
+        {
+            const std::string sAchievementID = std::to_string(nAchievementID);
+            return "{\"ID\":" + sAchievementID + ",\"Title\":\"Ach" + sAchievementID + "\"," +
+                "\"Description\":\"Desc" + sAchievementID + "\",\"Flags\":3," +
+                "\"Points\":" + std::to_string(nPoints) + ",\"MemAddr\":\"" + sMemAddr + "\"," +
+                "\"Author\":\"User1\",\"BadgeName\":\"00234\"," +
+                "\"Created\":1367266583,\"Modified\":1376929305}";
+        }
+
     private:
         ra::data::models::RichPresenceModel* GetRichPresence()
         {
@@ -197,46 +240,22 @@ public:
     TEST_METHOD(TestLoadGameTitle)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request& request, ra::api::FetchGameData::Response& response)
-        {
-            Assert::AreEqual(1U, request.GameId);
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
 
-            response.Title = L"Game";
-            return true;
-        });
-
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::AreEqual(1U, game.GameId());
         Assert::AreEqual(ra::data::context::GameContext::Mode::Normal, game.GetMode());
-        Assert::AreEqual(std::wstring(L"Game"), game.GameTitle());
+        Assert::AreEqual(std::wstring(L"GameTitle"), game.GameTitle());
     }
 
     TEST_METHOD(TestLoadGamePopup)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request& request, ra::api::FetchGameData::Response& response)
-        {
-            Assert::AreEqual(1U, request.GameId);
-
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Title = "Ach1";
-            ach1.Description = "Desc1";
-            ach1.Author = "Auth1";
-            ach1.BadgeName = "12345";
-            ach1.CategoryId = 3;
-            ach1.Created = 1234567890;
-            ach1.Updated = 1234599999;
-            ach1.Definition = "1=1";
-            ach1.Points = 5;
-
-            response.Title = L"GameTitle";
-            response.ImageIcon = "9743";
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "",
+            game.MockAchievementJson(123U, 5U));
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(std::wstring(L"Overlay\\info.wav")));
 
@@ -251,14 +270,7 @@ public:
     TEST_METHOD(TestLoadGameTitleConsoleMismatch)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request& request, ra::api::FetchGameData::Response& response)
-        {
-            Assert::AreEqual(1U, request.GameId);
-
-            response.Title = L"Game";
-            response.ConsoleId = ra::etoi(ConsoleID::MegaDrive);
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
 
         game.mockConsoleContext.SetId(ConsoleID::MasterSystem);
         game.mockConsoleContext.SetName(L"Master System");
@@ -273,7 +285,7 @@ public:
             return ra::ui::DialogResult::OK;
         });
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::AreEqual(0U, game.GameId());
         Assert::AreEqual(ra::data::context::GameContext::Mode::Normal, game.GetMode());
@@ -283,15 +295,7 @@ public:
     TEST_METHOD(TestLoadGameTitleConsoleMismatchGBvGBC)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request& request, ra::api::FetchGameData::Response& response)
-        {
-            Assert::AreEqual(1U, request.GameId);
-
-            response.Title = L"Game";
-            response.ConsoleId = ra::etoi(ConsoleID::GB);
-            response.ImageIcon = "9743";
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", "", ConsoleID::GB);
 
         game.mockConsoleContext.SetId(ConsoleID::GBC);
         game.mockConsoleContext.SetName(L"GameBoy Color");
@@ -303,14 +307,14 @@ public:
             return ra::ui::DialogResult::OK;
         });
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(std::wstring(L"Overlay\\info.wav")));
 
         const auto* pPopup = game.mockOverlayManager.GetMessage(1);
         Expects(pPopup != nullptr);
         Assert::IsNotNull(pPopup);
-        Assert::AreEqual(std::wstring(L"Loaded Game"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Loaded GameTitle"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"0 achievements, 0 points"), pPopup->GetDescription());
         Assert::AreEqual(std::string("9743"), pPopup->GetImage().Name());
 
@@ -320,15 +324,7 @@ public:
     TEST_METHOD(TestLoadGameTitleConsoleMismatchDSvDSi)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request& request, ra::api::FetchGameData::Response& response)
-        {
-            Assert::AreEqual(1U, request.GameId);
-
-            response.Title = L"Game";
-            response.ConsoleId = ra::etoi(ConsoleID::DS);
-            response.ImageIcon = "9743";
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", "", ConsoleID::DS);
 
         game.mockConsoleContext.SetId(ConsoleID::DSi);
         game.mockConsoleContext.SetName(L"Nintendo DSi");
@@ -340,14 +336,14 @@ public:
             return ra::ui::DialogResult::OK;
         });
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(std::wstring(L"Overlay\\info.wav")));
 
         const auto* pPopup = game.mockOverlayManager.GetMessage(1);
         Expects(pPopup != nullptr);
         Assert::IsNotNull(pPopup);
-        Assert::AreEqual(std::wstring(L"Loaded Game"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Loaded GameTitle"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"0 achievements, 0 points"), pPopup->GetDescription());
         Assert::AreEqual(std::string("9743"), pPopup->GetImage().Name());
 
@@ -357,15 +353,7 @@ public:
     TEST_METHOD(TestLoadGameTitleConsoleMismatchNESvEvent)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>(
-            [](const ra::api::FetchGameData::Request& request, ra::api::FetchGameData::Response& response) {
-                Assert::AreEqual(1U, request.GameId);
-
-                response.Title = L"Event";
-                response.ConsoleId = RC_CONSOLE_EVENTS;
-                response.ImageIcon = "9743";
-                return true;
-            });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", "", static_cast<ConsoleID>(RC_CONSOLE_EVENTS));
 
         game.mockConsoleContext.SetId(ConsoleID::NES);
         game.mockConsoleContext.SetName(L"Nintendo");
@@ -377,14 +365,14 @@ public:
                 return ra::ui::DialogResult::OK;
             });
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(std::wstring(L"Overlay\\info.wav")));
 
         const auto* pPopup = game.mockOverlayManager.GetMessage(1);
         Expects(pPopup != nullptr);
         Assert::IsNotNull(pPopup);
-        Assert::AreEqual(std::wstring(L"Loaded Event"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Loaded GameTitle"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"0 achievements, 0 points"), pPopup->GetDescription());
         Assert::AreEqual(std::string("9743"), pPopup->GetImage().Name());
 
@@ -404,36 +392,32 @@ public:
         NotifyHarness notifyHarness;
 
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.Title = L"GameTitle";
-            response.ImageIcon = "9743";
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
+        game.MockLoadGameAPIs(2U, "fedcba9876543210123456789abcdef");
 
         game.AddNotifyTarget(notifyHarness);
-        game.LoadGame(0U);
+        game.LoadGame(0U, "");
 
         Assert::AreEqual(0U, game.GameId());
         Assert::IsFalse(notifyHarness.m_bNotified);
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
         Assert::AreEqual(1U, game.GameId());
         Assert::IsTrue(notifyHarness.m_bNotified);
 
         notifyHarness.m_bNotified = false;
-        game.LoadGame(2U);
+        game.LoadGame(2U, "fedcba9876543210123456789abcdef");
         Assert::AreEqual(2U, game.GameId());
         Assert::IsTrue(notifyHarness.m_bNotified);
 
         notifyHarness.m_bNotified = false;
-        game.LoadGame(0U);
+        game.LoadGame(0U, "");
         Assert::AreEqual(0U, game.GameId());
         Assert::IsTrue(notifyHarness.m_bNotified);
 
         notifyHarness.m_bNotified = false;
         game.RemoveNotifyTarget(notifyHarness);
-        game.LoadGame(2U);
+        game.LoadGame(2U, "fedcba9876543210123456789abcdef");
         Assert::AreEqual(2U, game.GameId());
         Assert::IsFalse(notifyHarness.m_bNotified);
     }
@@ -441,13 +425,10 @@ public:
     TEST_METHOD(TestLoadGameRichPresence)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.RichPresence = "Display:\nHello, World";
-            return true;
-        });
+        const std::string sRichPresence = "Display:\nHello, World";
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", sRichPresence);
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.HasRichPresence());
         Assert::AreEqual(std::string("Display:\nHello, World\n"),
@@ -465,14 +446,11 @@ public:
     TEST_METHOD(TestLoadGameDoesNotOverwriteRichPresenceFromFile)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.RichPresence = "Display:\nHello, World";
-            return true;
-        });
+        const std::string sRichPresence = "Display:\nHello, World";
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", sRichPresence);
 
         game.mockStorage.MockStoredData(ra::services::StorageItemType::RichPresence, L"1", "Display:\nFrom File\n");
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.HasRichPresence());
         Assert::AreEqual(std::string("Display:\nFrom File\n"),
@@ -492,15 +470,13 @@ public:
     TEST_METHOD(TestLoadGameRichPresenceUpdatedFromServer)
     {
         GameContextHarness game;
+        const std::string sRichPresence = "Display:\nHello, World";
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", sRichPresence);
+
         game.mockStorage.MockStoredData(ra::services::StorageItemType::GameData, L"1", "{\"RichPresencePatch\": \"Display:\\nOld\\n\"}");
         game.mockStorage.MockStoredData(ra::services::StorageItemType::RichPresence, L"1", "Display:\nOld\n");
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.RichPresence = "Display:\nHello, World";
-            return true;
-        });
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.HasRichPresence());
         Assert::AreEqual(std::string("Display:\nHello, World\n"),
@@ -518,15 +494,13 @@ public:
     TEST_METHOD(TestLoadGameRichPresenceUpdatedFromServerLocalModifications)
     {
         GameContextHarness game;
+        const std::string sRichPresence = "Display:\nHello, World";
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", sRichPresence);
+
         game.mockStorage.MockStoredData(ra::services::StorageItemType::GameData, L"1", "{\"RichPresencePatch\": \"Display:\\nOld\\n\"}");
         game.mockStorage.MockStoredData(ra::services::StorageItemType::RichPresence, L"1", "Display:\nLocal\n");
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.RichPresence = "Display:\nHello, World";
-            return true;
-        });
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.HasRichPresence());
         Assert::AreEqual(std::string("Display:\nLocal\n"),
@@ -544,13 +518,11 @@ public:
     TEST_METHOD(TestLoadGameRichPresenceNotOnServer)
     {
         GameContextHarness game;
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
+
         game.mockStorage.MockStoredData(ra::services::StorageItemType::GameData, L"1", "{\"RichPresencePatch\": null}");
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response&)
-        {
-            return true;
-        });
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsFalse(game.HasRichPresence());
         Assert::IsFalse(game.mockStorage.HasStoredData(ra::services::StorageItemType::RichPresence, L"1"));
@@ -563,13 +535,9 @@ public:
     TEST_METHOD(TestLoadGameNoRichPresence)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>(
-            [](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response) {
-                response.RichPresence = "";
-                return true;
-            });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsFalse(game.HasRichPresence());
         Assert::AreEqual(std::string(),
@@ -583,14 +551,10 @@ public:
     TEST_METHOD(TestLoadGameRichPresenceOnlyFromFile)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>(
-            [](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response) {
-                response.RichPresence = "";
-                return true;
-            });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
 
         game.mockStorage.MockStoredData(ra::services::StorageItemType::RichPresence, L"1", "Display:\nFrom File\n");
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         Assert::IsTrue(game.HasRichPresence());
         Assert::AreEqual(std::string("Display:\nFrom File\n"),
@@ -608,35 +572,16 @@ public:
     TEST_METHOD(TestLoadGameAchievements)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Title = "Ach1";
-            ach1.Description = "Desc1";
-            ach1.Author = "Auth1";
-            ach1.BadgeName = "12345";
-            ach1.CategoryId = 3;
-            ach1.Created = 1234567890;
-            ach1.Updated = 1234599999;
-            ach1.Definition = "1=1";
-            ach1.Points = 5;
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.Title = "Ach2";
-            ach2.Description = "Desc2";
-            ach2.Author = "Auth2";
-            ach2.BadgeName = "12345";
-            ach2.CategoryId = 5;
-            ach2.Created = 1234567890;
-            ach2.Updated = 1234599999;
-            ach2.Definition = "1=1";
-            ach2.Points = 15;
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "",
+             "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,"
+               "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999},"
+             "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,"
+               "\"Points\":15,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         game.RemoveNonAchievementAssets();
         Assert::AreEqual({ 2U }, game.Assets().Count());
@@ -666,43 +611,41 @@ public:
         Assert::AreEqual(std::wstring(L"12345"), vmAch2->GetBadge());
         Assert::AreEqual(std::string("1=1"), vmAch2->GetTrigger());
         Assert::IsFalse(vmAch2->IsModified());
+
+        const auto pPatchData = game.mockStorage.GetStoredData(ra::services::StorageItemType::GameData, L"1");
+        Assert::AreNotEqual(pPatchData.find("\"ID\":1,"), std::string::npos);
+        Assert::AreEqual(pPatchData.find("\"PatchData\":"), std::string::npos); /* PatchData should be stripped out */
     }
 
     TEST_METHOD(TestLoadGameInvalidAchievementFlags)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Title = "Ach1";
-            ach1.Description = "Desc1";
-            ach1.Author = "Auth1";
-            ach1.BadgeName = "12345";
-            ach1.CategoryId = 0; // not a valid category
-            ach1.Created = 1234567890;
-            ach1.Updated = 1234599999;
-            ach1.Definition = "1=1";
-            ach1.Points = 5;
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.Title = "Ach2";
-            ach2.Description = "Desc2";
-            ach2.Author = "Auth2";
-            ach2.BadgeName = "12345";
-            ach2.CategoryId = 5;
-            ach2.Created = 1234567890;
-            ach2.Updated = 1234599999;
-            ach2.Definition = "1=1";
-            ach2.Points = 15;
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "",
+             "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":0," // not a valid Flags
+               "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999},"
+             "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,"
+               "\"Points\":15,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         game.RemoveNonAchievementAssets();
-        Assert::AreEqual({ 1U }, game.Assets().Count());
+        Assert::AreEqual({ 2U }, game.Assets().Count());
+
+        const auto* vmAch1 = game.Assets().FindAchievement(5U);
+        Assert::IsNotNull(vmAch1);
+        Ensures(vmAch1 != nullptr);
+        Assert::AreEqual(5U, vmAch1->GetID());
+        Assert::AreEqual(std::wstring(L"Ach1"), vmAch1->GetName());
+        Assert::AreEqual(std::wstring(L"Desc1"), vmAch1->GetDescription());
+        Assert::AreEqual(std::wstring(L"Auth1"), vmAch1->GetAuthor());
+        Assert::AreEqual(ra::data::models::AssetCategory::Unofficial, vmAch1->GetCategory());
+        Assert::AreEqual(5, vmAch1->GetPoints());
+        Assert::AreEqual(std::wstring(L"12345"), vmAch1->GetBadge());
+        Assert::AreEqual(std::string("1=1"), vmAch1->GetTrigger());
+        Assert::IsFalse(vmAch1->IsModified());
 
         const auto* vmAch2 = game.Assets().FindAchievement(7U);
         Assert::IsNotNull(vmAch2);
@@ -720,33 +663,14 @@ public:
     TEST_METHOD(TestLoadGameMergeLocalAchievements)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Title = "Ach1";
-            ach1.Description = "Desc1";
-            ach1.Author = "Auth1";
-            ach1.BadgeName = "12345";
-            ach1.CategoryId = 3;
-            ach1.Created = 1234567890;
-            ach1.Updated = 1234599999;
-            ach1.Definition = "1=1";
-            ach1.Points = 5;
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.Title = "Ach2";
-            ach2.Description = "Desc2";
-            ach2.Author = "Auth2";
-            ach2.BadgeName = "12345";
-            ach2.CategoryId = 5;
-            ach2.Created = 1234567890;
-            ach2.Updated = 1234599999;
-            ach2.Definition = "1=1";
-            ach2.Points = 15;
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "",
+             "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,"
+               "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999},"
+             "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,"
+               "\"Points\":15,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
         game.mockStorage.MockStoredData(ra::services::StorageItemType::UserAchievements, L"1",
             "Version\n"
@@ -756,7 +680,7 @@ public:
             "0:R:1=1:Ach4:Desc4::::Auth4:10:1234511111:1234500000:::556\n"
         );
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         auto* pAch = game.Assets().FindAchievement(5U);
         Assert::IsNotNull(pAch);
@@ -809,10 +733,7 @@ public:
     TEST_METHOD(TestLoadGameMergeLocalAchievementsWithIds)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response&)
-        {
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
 
         game.mockStorage.MockStoredData(ra::services::StorageItemType::UserAchievements, L"1",
             "Version\n"
@@ -822,7 +743,7 @@ public:
             "999000003:1=1:Ach4:Desc4::::Auth4:10:1234511111:1234500000:::556\n"
         );
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         game.RemoveNonAchievementAssets();
         Assert::AreEqual({ 3U }, game.Assets().Count());
@@ -879,26 +800,14 @@ public:
     TEST_METHOD(TestLoadGameLeaderboards)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            auto& lb1 = response.Leaderboards.emplace_back();
-            lb1.Id = 7U;
-            lb1.Title = "LB1";
-            lb1.Description = "Desc1";
-            lb1.Definition = "STA:1=1::CAN:1=1::SUB:1=1::VAL:1";
-            lb1.Format = RC_FORMAT_SECONDS;
-
-            auto& lb2 = response.Leaderboards.emplace_back();
-            lb2.Id = 8U;
-            lb2.Title = "LB2";
-            lb2.Description = "Desc2";
-            lb2.Definition = "STA:1=1::CAN:1=1::SUB:1=1::VAL:1";
-            lb2.Format = RC_FORMAT_FRAMES;
-
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "",
+            "{\"ID\":7,\"Title\":\"LB1\",\"Description\":\"Desc1\","
+             "\"Mem\":\"STA:1=1::CAN:1=1::SUB:1=1::VAL:1\",\"Format\":\"SECS\"},"
+            "{\"ID\":8,\"Title\":\"LB2\",\"Description\":\"Desc2\","
+             "\"Mem\":\"STA:1=1::CAN:1=1::SUB:1=1::VAL:1\",\"Format\":\"FRAMES\"}"
+        );
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         const auto* pLb1 = game.Assets().FindLeaderboard(7U);
         Assert::IsNotNull(pLb1);
@@ -916,65 +825,27 @@ public:
     TEST_METHOD(TestLoadGameReplacesAchievements)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Title = "Ach1";
-            ach1.Description = "Desc1";
-            ach1.Author = "Auth1";
-            ach1.BadgeName = "12345";
-            ach1.CategoryId = 3;
-            ach1.Created = 1234567890;
-            ach1.Updated = 1234599999;
-            ach1.Definition = "1=1";
-            ach1.Points = 5;
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.Title = "Ach2";
-            ach2.Description = "Desc2";
-            ach2.Author = "Auth2";
-            ach2.BadgeName = "12345";
-            ach2.CategoryId = 5;
-            ach2.Created = 1234567890;
-            ach2.Updated = 1234599999;
-            ach2.Definition = "1=1";
-            ach2.Points = 15;
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "",
+             "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,"
+               "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999},"
+             "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,"
+               "\"Points\":15,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 9;
-            ach1.Title = "Ach9";
-            ach1.Description = "Desc9";
-            ach1.Author = "Auth9";
-            ach1.BadgeName = "12345";
-            ach1.CategoryId = 3;
-            ach1.Created = 1234567890;
-            ach1.Updated = 1234599999;
-            ach1.Definition = "1=1";
-            ach1.Points = 9;
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 11;
-            ach2.Title = "Ach11";
-            ach2.Description = "Desc11";
-            ach2.Author = "Auth11";
-            ach2.BadgeName = "12345";
-            ach2.CategoryId = 5;
-            ach2.Created = 1234567890;
-            ach2.Updated = 1234599999;
-            ach2.Definition = "1=1";
-            ach2.Points = 11;
-            return true;
-        });
+        game.MockLoadGameAPIs(2U, "fedcba9876543210123456789abcdef", "",
+             "{\"ID\":9,\"Title\":\"Ach9\",\"Description\":\"Desc9\",\"Flags\":3,"
+               "\"Points\":9,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999},"
+             "{\"ID\":11,\"Title\":\"Ach11\",\"Description\":\"Desc11\",\"Flags\":5,"
+               "\"Points\":11,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
-        game.LoadGame(2U);
+        game.LoadGame(2U, "fedcba9876543210123456789abcdef");
 
         Assert::IsNull(game.Assets().FindAchievement(5U));
         Assert::IsNull(game.Assets().FindAchievement(7U));
@@ -1011,40 +882,21 @@ public:
     TEST_METHOD(TestLoadGameZeroRemovesAchievements)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Title = "Ach1";
-            ach1.Description = "Desc1";
-            ach1.Author = "Auth1";
-            ach1.BadgeName = "12345";
-            ach1.CategoryId = 3;
-            ach1.Created = 1234567890;
-            ach1.Updated = 1234599999;
-            ach1.Definition = "1=1";
-            ach1.Points = 5;
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.Title = "Ach2";
-            ach2.Description = "Desc2";
-            ach2.Author = "Auth2";
-            ach2.BadgeName = "12345";
-            ach2.CategoryId = 5;
-            ach2.Created = 1234567890;
-            ach2.Updated = 1234599999;
-            ach2.Definition = "1=1";
-            ach2.Points = 15;
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "",
+             "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,"
+               "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999},"
+             "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,"
+               "\"Points\":15,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+               "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         game.RemoveNonAchievementAssets();
         Assert::AreEqual({ 2U }, game.Assets().Count());
 
-        game.LoadGame(0U);
+        game.LoadGame(0U, "");
 
         Assert::IsNull(game.Assets().FindAchievement(5U));
         Assert::IsNull(game.Assets().FindAchievement(7U));
@@ -1056,40 +908,17 @@ public:
     TEST_METHOD(TestLoadGameUserUnlocks)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.Title = L"My Game";
-            response.ImageIcon = "3333";
-
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Points = 5;
-            ach1.CategoryId = ra::etoi(ra::data::models::AssetCategory::Core);
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.Points = 10;
-            ach2.CategoryId = ra::etoi(ra::data::models::AssetCategory::Core);
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([](const ra::api::FetchUserUnlocks::Request& request, ra::api::FetchUserUnlocks::Response& response)
-        {
-            Assert::AreEqual(1U, request.GameId);
-            Assert::IsFalse(request.Hardcore);
-
-            response.UnlockedAchievements.insert(7U);
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321",
+            "\"HardcoreUnlocks\":[{\"ID\":7,\"When\":1234567890}]",
+            "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,"
+              "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999},"
+            "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,"
+              "\"Points\":10,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
-        game.mockServer.HandleRequest<ra::api::FetchCodeNotes>([](const ra::api::FetchCodeNotes::Request&, ra::api::FetchCodeNotes::Response&)
-        {
-            return true;
-        });
-
-        game.LoadGame(1U);
-        game.mockThreadPool.ExecuteNextTask(); // FetchUserUnlocks and FetchCodeNotes are async
-        game.mockThreadPool.ExecuteNextTask();
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         const auto* pAch1 = game.Assets().FindAchievement(5U);
         Assert::IsNotNull(pAch1);
@@ -1104,60 +933,32 @@ public:
         const auto* pPopup = game.mockOverlayManager.GetMessage(1);
         Expects(pPopup != nullptr);
         Assert::IsNotNull(pPopup);
-        Assert::AreEqual(std::wstring(L"Loaded My Game"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Loaded GameTitle"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"2 achievements, 15 points"), pPopup->GetDescription());
         Assert::AreEqual(std::wstring(L"You have earned 1 achievements"), pPopup->GetDetail());
-        Assert::AreEqual(std::string("3333"), pPopup->GetImage().Name());
+        Assert::AreEqual(std::string("9743"), pPopup->GetImage().Name());
     }
 
     TEST_METHOD(TestLoadGameUserUnlocksUnofficial)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.Title = L"My Game";
-            response.ImageIcon = "3333";
-
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Points = 5;
-            ach1.CategoryId = ra::etoi(ra::data::models::AssetCategory::Core);
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.Points = 10;
-            ach2.CategoryId = ra::etoi(ra::data::models::AssetCategory::Core);
-
-            auto& ach3 = response.Achievements.emplace_back();
-            ach3.Id = 9;
-            ach3.Points = 25;
-            ach3.CategoryId = ra::etoi(ra::data::models::AssetCategory::Unofficial);
-
-            auto& ach4 = response.Achievements.emplace_back();
-            ach4.Id = 11;
-            ach4.Points = 50;
-            ach4.CategoryId = ra::etoi(ra::data::models::AssetCategory::Unofficial);
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([](const ra::api::FetchUserUnlocks::Request& request, ra::api::FetchUserUnlocks::Response& response)
-        {
-            Assert::AreEqual(1U, request.GameId);
-            Assert::IsFalse(request.Hardcore);
-
-            response.UnlockedAchievements.insert(7U); // core achievement
-            response.UnlockedAchievements.insert(9U); // unofficial achievement
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchCodeNotes>([](const ra::api::FetchCodeNotes::Request&, ra::api::FetchCodeNotes::Response&)
-        {
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321",
+            "\"HardcoreUnlocks\":[{\"ID\":7,\"When\":1234567890},{\"ID\":9,\"When\":1234567890}]",
+            "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,"
+              "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999},"
+            "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,"
+              "\"Points\":10,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999},"
+            "{\"ID\":9,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,"
+              "\"Points\":25,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999},"
+            "{\"ID\":11,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,"
+              "\"Points\":50,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
-        game.LoadGame(1U);
-        game.mockThreadPool.ExecuteNextTask(); // FetchUserUnlocks and FetchCodeNotes are async
-        game.mockThreadPool.ExecuteNextTask();
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         const auto* pAch1 = game.Assets().FindAchievement(5U);
         Assert::IsNotNull(pAch1);
@@ -1185,37 +986,26 @@ public:
         const auto* pPopup = game.mockOverlayManager.GetMessage(1);
         Expects(pPopup != nullptr);
         Assert::IsNotNull(pPopup);
-        Assert::AreEqual(std::wstring(L"Loaded My Game"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Loaded GameTitle"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"2 achievements, 15 points"), pPopup->GetDescription());
         Assert::AreEqual(std::wstring(L"You have earned 1 achievements"), pPopup->GetDetail());
-        Assert::AreEqual(std::string("3333"), pPopup->GetImage().Name());
+        Assert::AreEqual(std::string("9743"), pPopup->GetImage().Name());
     }
 
     TEST_METHOD(TestLoadGameUserUnlocksCompatibilityMode)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.CategoryId = ra::etoi(ra::data::models::AssetCategory::Core);
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.CategoryId = ra::etoi(ra::data::models::AssetCategory::Core);
-            return true;
-        });
-
-        game.mockServer.ExpectUncalled<ra::api::FetchUserUnlocks>();
-
-        game.mockServer.HandleRequest<ra::api::FetchCodeNotes>([](const ra::api::FetchCodeNotes::Request&, ra::api::FetchCodeNotes::Response&)
-        {
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321",
+            "\"HardcoreUnlocks\":[{\"ID\":7,\"When\":1234567890}]",
+            "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,"
+              "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999},"
+            "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,"
+              "\"Points\":10,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
-        game.LoadGame(1U, ra::data::context::GameContext::Mode::CompatibilityTest);
-        game.mockThreadPool.ExecuteNextTask(); // FetchUserUnlocks and FetchCodeNotes are async
-        game.mockThreadPool.ExecuteNextTask();
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321", ra::data::context::GameContext::Mode::CompatibilityTest);
 
         const auto* pAch1 = game.Assets().FindAchievement(5U);
         Assert::IsNotNull(pAch1);
@@ -1231,35 +1021,14 @@ public:
     TEST_METHOD(TestLoadGameUserUnlocksLocalModifications)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.Title = L"My Game";
-            response.ImageIcon = "3333";
-
-            auto& ach1 = response.Achievements.emplace_back();
-            ach1.Id = 5;
-            ach1.Points = 5;
-            ach1.CategoryId = ra::etoi(ra::data::models::AssetCategory::Core);
-
-            auto& ach2 = response.Achievements.emplace_back();
-            ach2.Id = 7;
-            ach2.Points = 10;
-            ach2.CategoryId = ra::etoi(ra::data::models::AssetCategory::Core);
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([](const ra::api::FetchUserUnlocks::Request& request, ra::api::FetchUserUnlocks::Response&)
-        {
-            Assert::AreEqual(1U, request.GameId);
-            Assert::IsFalse(request.Hardcore);
-
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchCodeNotes>([](const ra::api::FetchCodeNotes::Request&, ra::api::FetchCodeNotes::Response&)
-        {
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "",
+            "{\"ID\":5,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,"
+              "\"Points\":5,\"MemAddr\":\"1=1\",\"Author\":\"Auth1\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999},"
+            "{\"ID\":7,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,"
+              "\"Points\":10,\"MemAddr\":\"1=1\",\"Author\":\"Auth2\",\"BadgeName\":\"12345\","
+              "\"Created\":1234567890,\"Modified\":123459999}"
+        );
 
         game.mockStorage.MockStoredData(ra::services::StorageItemType::UserAchievements, L"1",
             "Version\n"
@@ -1267,9 +1036,7 @@ public:
             "7:1=2:Ach2b:Desc2b::::Auth2b:25:1234554321:1234555555:::54321\n"
         );
 
-        game.LoadGame(1U);
-        game.mockThreadPool.ExecuteNextTask(); // FetchUserUnlocks and FetchCodeNotes are async
-        game.mockThreadPool.ExecuteNextTask();
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         const auto* pAch1 = game.Assets().FindAchievement(5U);
         Assert::IsNotNull(pAch1);
@@ -1286,76 +1053,57 @@ public:
         const auto* pPopup = game.mockOverlayManager.GetMessage(1);
         Expects(pPopup != nullptr);
         Assert::IsNotNull(pPopup);
-        Assert::AreEqual(std::wstring(L"Loaded My Game"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Loaded GameTitle"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"2 achievements, 15 points"), pPopup->GetDescription());
         Assert::AreEqual(std::wstring(L"You have earned 0 achievements"), pPopup->GetDetail());
-        Assert::AreEqual(std::string("3333"), pPopup->GetImage().Name());
+        Assert::AreEqual(std::string("9743"), pPopup->GetImage().Name());
     }
 
     TEST_METHOD(TestLoadGamePausesRuntime)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response&)
-        {
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
 
-        game.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([](const ra::api::FetchUserUnlocks::Request&, ra::api::FetchUserUnlocks::Response&)
-        {
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchCodeNotes>([](const ra::api::FetchCodeNotes::Request&, ra::api::FetchCodeNotes::Response&)
-        {
-            return true;
-        });
+        bool bBeforeResponseCalled = false;
+        game.mockRcheevosClient.OnBeforeResponse("r=patch&u=Username&t=ApiToken&g=1",
+            [&game, &bBeforeResponseCalled]() {
+                bBeforeResponseCalled = true;
+                Assert::IsTrue(game.runtime.IsPaused());
+            });
 
-        game.LoadGame(1U);
-        Assert::IsTrue(game.runtime.IsPaused());
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
-        game.mockThreadPool.ExecuteNextTask(); // FetchUserUnlocks and FetchCodeNotes are async
-        game.mockThreadPool.ExecuteNextTask();
-        Assert::IsFalse(game.runtime.IsPaused());
+        Assert::IsTrue(bBeforeResponseCalled);    // paused during load
+        Assert::IsFalse(game.runtime.IsPaused()); // not paused after load
     }
 
     TEST_METHOD(TestLoadGameWhileRuntimePaused)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response&)
-        {
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
 
-        game.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([](const ra::api::FetchUserUnlocks::Request&, ra::api::FetchUserUnlocks::Response&)
-        {
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchCodeNotes>([](const ra::api::FetchCodeNotes::Request&, ra::api::FetchCodeNotes::Response&)
-        {
-            return true;
-        });
+        bool bBeforeResponseCalled = false;
+        game.mockRcheevosClient.OnBeforeResponse("r=patch&u=Username&t=ApiToken&g=1",
+            [&game, &bBeforeResponseCalled]() {
+                bBeforeResponseCalled = true;
+                Assert::IsTrue(game.runtime.IsPaused());
+            });
 
         game.runtime.SetPaused(true);
 
-        game.LoadGame(1U);
-        Assert::IsTrue(game.runtime.IsPaused());
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
-        game.mockThreadPool.ExecuteNextTask(); // FetchUserUnlocks and FetchCodeNotes are async
-        game.mockThreadPool.ExecuteNextTask();
-        Assert::IsTrue(game.runtime.IsPaused());
+        Assert::IsTrue(bBeforeResponseCalled);   // paused during load
+        Assert::IsTrue(game.runtime.IsPaused()); // paused after load
     }
 
     TEST_METHOD(TestReloadRichPresenceScriptNoFile)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.RichPresence = "Display:\nHello, World";
-            return true;
-        });
+        const std::string sRichPresence = "Display:\nHello, World";
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", sRichPresence);
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         /* load game will write the server RP to storage */
         game.ReloadRichPresenceScript();
@@ -1376,13 +1124,10 @@ public:
     TEST_METHOD(TestReloadRichPresenceScriptWindowsLineEndings)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.RichPresence = "Display:\r\nHello, World\r\n";
-            return true;
-        });
+        const std::string sRichPresence = "Display:\nHello, World";
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", sRichPresence);
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         /* load game will write the server RP to storage */
         game.ReloadRichPresenceScript();
@@ -1403,13 +1148,10 @@ public:
     TEST_METHOD(TestReloadRichPresenceScript)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.RichPresence = "Display:\nHello, World";
-            return true;
-        });
+        const std::string sRichPresence = "Display:\nHello, World";
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", sRichPresence);
 
-        game.LoadGame(1U);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         /* load game will write the server RP to storage, so overwrite it now */
         game.mockStorage.MockStoredData(ra::services::StorageItemType::RichPresence, L"1", "Display:\nFrom File");
@@ -1423,16 +1165,12 @@ public:
     TEST_METHOD(TestReloadRichPresenceScriptBOM)
     {
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.RichPresence = "Display:\nHello, World";
-            return true;
-        });
-
-        game.LoadGame(1U);
+        const std::string sRichPresence = "Display:\nHello, World";
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321", "", "", "", sRichPresence);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
         /* load game will write the server RP to storage, so overwrite it now */
-        std::string sFileContents = "...Display:\nFrom File";
+        std::string sFileContents = "\xEF\xBB\xBF" "Display:\nFrom File";
         sFileContents.at(0) = gsl::narrow_cast<char>(0xef);
         sFileContents.at(1) = gsl::narrow_cast<char>(0xbb);
         sFileContents.at(2) = gsl::narrow_cast<char>(0xbf);
@@ -1445,7 +1183,7 @@ public:
         Assert::IsTrue(game.IsRichPresenceFromFile());
 
         /* hash should ignore BOM and convert windows newlines to unix newlines */
-        sFileContents = "...Display:\r\nHello, World";
+        sFileContents = "\xEF\xBB\xBF" "Display:\r\nHello, World";
         sFileContents.at(0) = gsl::narrow_cast<char>(0xef);
         sFileContents.at(1) = gsl::narrow_cast<char>(0xbb);
         sFileContents.at(2) = gsl::narrow_cast<char>(0xbf);
@@ -2791,17 +2529,12 @@ public:
         NotifyHarness notifyHarness;
 
         GameContextHarness game;
-        game.mockServer.HandleRequest<ra::api::FetchGameData>([](const ra::api::FetchGameData::Request&, ra::api::FetchGameData::Response& response)
-        {
-            response.Title = L"GameTitle";
-            response.ImageIcon = "9743";
-            return true;
-        });
+        game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321");
 
         Assert::AreEqual(GameContext::Mode::Normal, game.GetMode());
 
         game.AddNotifyTarget(notifyHarness);
-        game.LoadGame(0U);
+        game.LoadGame(0U, "");
 
         Assert::AreEqual(0U, game.GameId());
         Assert::IsFalse(notifyHarness.m_bNotified);
@@ -2810,7 +2543,7 @@ public:
         Assert::AreEqual(GameContext::Mode::CompatibilityTest, game.GetMode());
         Assert::IsFalse(notifyHarness.m_bNotified);
 
-        game.LoadGame(1U, GameContext::Mode::CompatibilityTest);
+        game.LoadGame(1U, "0123456789abcdeffedcba987654321", GameContext::Mode::CompatibilityTest);
         Assert::AreEqual(1U, game.GameId());
         Assert::AreEqual(GameContext::Mode::CompatibilityTest, game.GetMode());
         Assert::IsTrue(notifyHarness.m_bNotified);
diff --git a/tests/mocks/MockGameContext.hh b/tests/mocks/MockGameContext.hh
index 4fce5253..d89e9c49 100644
--- a/tests/mocks/MockGameContext.hh
+++ b/tests/mocks/MockGameContext.hh
@@ -19,7 +19,7 @@ public:
     {
     }
 
-    void LoadGame(unsigned int nGameId, ra::data::context::GameContext::Mode nMode) noexcept override
+    void LoadGame(unsigned int nGameId, const std::string&, ra::data::context::GameContext::Mode nMode) noexcept override
     {
         m_nGameId = nGameId;
         m_nMode = nMode;
diff --git a/tests/mocks/MockRcheevosClient.cpp b/tests/mocks/MockRcheevosClient.cpp
new file mode 100644
index 00000000..becc8290
--- /dev/null
+++ b/tests/mocks/MockRcheevosClient.cpp
@@ -0,0 +1,177 @@
+#include "MockRcheevosClient.hh"
+#include "services\ServiceLocator.hh"
+
+#include "CppUnitTest.h"
+#include "RA_StringUtils.h"
+
+#include <rcheevos\src\rapi\rc_api_common.h>
+#include <rcheevos\src\rcheevos\rc_client_internal.h>
+
+namespace ra {
+namespace services {
+namespace mocks {
+
+void MockRcheevosClient::OnBeforeResponse(const std::string& sRequestParams, std::function<void()>&& fHandler)
+{
+    for (auto& pResponse : m_vResponses)
+    {
+        if (pResponse.sRequestParams == sRequestParams)
+        {
+            pResponse.fBeforeResponse = fHandler;
+            return;
+        }
+    }
+
+    Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(
+        ra::StringPrintf(L"No response registered for: %s", sRequestParams).c_str());
+}
+
+void MockRcheevosClient::MockUser(const std::string& sUsername, const std::string& sApiToken)
+{
+    m_sUsername = sUsername;
+    m_sApiToken = sApiToken;
+    GetClient()->user.username = m_sUsername.c_str();
+    GetClient()->user.display_name = m_sUsername.c_str();
+    GetClient()->user.token = m_sApiToken.c_str();
+    GetClient()->state.user = RC_CLIENT_USER_STATE_LOGGED_IN;
+}
+
+void MockRcheevosClient::MockGame()
+{
+    rc_client_game_info_t* game = (rc_client_game_info_t*)calloc(1, sizeof(rc_client_game_info_t));
+    Expects(game != nullptr);
+    rc_buf_init(&game->buffer);
+    rc_runtime_init(&game->runtime);
+
+    game->public_.id = 1;
+    game->public_.console_id = RC_CONSOLE_NINTENDO;
+    game->public_.badge_name = "012345";
+    game->public_.title = "Game Title";
+
+    GetClient()->game = game;
+}
+
+static rc_client_subset_info_t* GetCoreSubset(rc_client_game_info_t* game)
+{
+    rc_client_subset_info_t* subset = game->subsets;
+    if (subset == nullptr)
+    {
+        subset = (rc_client_subset_info_t*)rc_buf_alloc(&game->buffer, sizeof(rc_client_subset_info_t));
+        memset(subset, 0, sizeof(*subset));
+        subset->public_.id = game->public_.id;
+        strcpy_s(subset->public_.badge_name, sizeof(subset->public_.badge_name), game->public_.badge_name);
+        subset->public_.title = game->public_.title;
+
+        game->subsets = subset;
+    }
+
+    return subset;
+}
+
+rc_client_achievement_info_t* MockRcheevosClient::MockAchievement(uint32_t nId, const char* sTitle)
+{
+    rc_client_game_info_t* game = GetClient()->game;
+    rc_client_subset_info_t* subset = GetCoreSubset(game);
+
+    if (subset->public_.num_achievements % 8 == 0)
+    {
+        const uint32_t new_count = subset->public_.num_achievements + 8;
+        rc_client_achievement_info_t* new_achievements =
+            (rc_client_achievement_info_t*)rc_buf_alloc(&game->buffer,
+                sizeof(rc_client_achievement_info_t) * new_count);
+
+        if (subset->public_.num_achievements > 0)
+        {
+            memcpy(new_achievements, subset->achievements,
+                   sizeof(rc_client_achievement_info_t) * subset->public_.num_achievements);
+        }
+
+        subset->achievements = new_achievements;
+    }
+
+    rc_client_achievement_info_t* achievement = &subset->achievements[subset->public_.num_achievements++];
+    memset(achievement, 0, sizeof(*achievement));
+    achievement->public_.id = nId;
+
+    if (sTitle)
+    {
+        achievement->public_.title = rc_buf_strcpy(&game->buffer, sTitle);
+    }
+    else
+    {
+        const std::string sGeneratedTitle = ra::StringPrintf("Achievement %u", nId);
+        achievement->public_.title = rc_buf_strcpy(&game->buffer, sGeneratedTitle.c_str());
+    }
+
+    const std::string sGeneratedDescripton = ra::StringPrintf("Description %u", nId);
+    achievement->public_.description = rc_buf_strcpy(&game->buffer, sGeneratedDescripton.c_str());
+
+    achievement->public_.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE;
+    achievement->public_.points = 5;
+
+    return achievement;
+}
+
+void MockRcheevosClient::AssertCalled(const std::string& sRequestParams) const
+{
+    for (auto& pResponse : m_vResponses)
+    {
+        if (pResponse.sRequestParams == sRequestParams)
+        {
+            Microsoft::VisualStudio::CppUnitTestFramework::Assert::IsTrue(pResponse.bSeen);
+            return;
+        }
+    }
+
+    Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(
+        ra::StringPrintf(L"Could not find mock response for %s", sRequestParams).c_str());
+}
+
+void MockRcheevosClient::AssertNoPendingRequests() const
+{
+    for (auto& pResponse : m_vResponses)
+    {
+        if (pResponse.fAsyncCallback != nullptr && !pResponse.bSeen)
+        {
+            Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(
+                ra::StringPrintf(L"Unexpected request pending for %s", pResponse.sRequestParams).c_str());
+            return;
+        }
+    }
+}
+
+void MockRcheevosClient::MockServerCall(const rc_api_request_t* pRequest,
+     rc_client_server_callback_t fCallback, void* pCallbackData, rc_client_t*)
+{
+    auto* pClient = dynamic_cast<MockRcheevosClient*>(&ra::services::ServiceLocator::GetMutable<ra::services::RcheevosClient>());
+    std::string sRequestParams = pRequest->post_data;
+
+    for (auto& pResponse : pClient->m_vResponses)
+    {
+        if (pResponse.sRequestParams == sRequestParams)
+        {
+            pResponse.bSeen = true;
+
+            if (pResponse.fBeforeResponse)
+                pResponse.fBeforeResponse();
+
+            rc_api_server_response_t pServerResponse;
+            memset(&pServerResponse, 0, sizeof(pServerResponse));
+            pServerResponse.http_status_code = pResponse.nHttpStatus;
+            pServerResponse.body = pResponse.sResponseBody.c_str();
+            pServerResponse.body_length = pResponse.sResponseBody.length();
+
+            fCallback(&pServerResponse, pCallbackData);
+            return;
+        }
+    }
+
+    auto& pResponse = pClient->m_vResponses.emplace_back();
+    pResponse.sRequestParams = sRequestParams;
+    pResponse.fAsyncCallback = fCallback;
+    pResponse.pAsyncCallbackData = pCallbackData;
+}
+
+} // namespace mocks
+} // namespace services
+} // namespace ra
diff --git a/tests/mocks/MockRcheevosClient.hh b/tests/mocks/MockRcheevosClient.hh
index 3fdf367f..5d5b394a 100644
--- a/tests/mocks/MockRcheevosClient.hh
+++ b/tests/mocks/MockRcheevosClient.hh
@@ -5,9 +5,6 @@
 #include "services\RcheevosClient.hh"
 #include "services\ServiceLocator.hh"
 
-#include "CppUnitTest.h"
-#include "RA_StringUtils.h"
-
 #include <rcheevos\src\rcheevos\rc_client_internal.h>
 
 namespace ra {
@@ -34,63 +31,20 @@ public:
         pResponse.nHttpStatus = 200;
     }
 
-    void AssertCalled(const std::string& sRequestParams) const
-    {
-        for (auto& pResponse : m_vResponses)
-        {
-            if (pResponse.sRequestParams == sRequestParams)
-            {
-                Microsoft::VisualStudio::CppUnitTestFramework::Assert::IsTrue(pResponse.bSeen);
-                return;
-            }
-        }
-
-        Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(
-            ra::StringPrintf(L"Could not find mock response for %s", sRequestParams).c_str());
-    }
+    void OnBeforeResponse(const std::string& sRequestParams, std::function<void()>&& fHandler);
 
-    void AssertNoPendingRequests() const
-    {
-        for (auto& pResponse : m_vResponses)
-        {
-            if (pResponse.fAsyncCallback != nullptr && !pResponse.bSeen)
-            {
-                Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(
-                    ra::StringPrintf(L"Unexpected request pending for %s", pResponse.sRequestParams).c_str());
-                return;
-            }
-        }
-    }
+    void MockUser(const std::string& sUsername, const std::string& sApiToken);
+
+    void MockGame();
+    rc_client_achievement_info_t* MockAchievement(uint32_t nId, const char* sTitle = nullptr);
+
+    void AssertCalled(const std::string& sRequestParams) const;
+
+    void AssertNoPendingRequests() const;
 
 private:
     static void MockServerCall(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback,
-                               void* pCallbackData, rc_client_t*)
-    {
-        auto* pClient = dynamic_cast<MockRcheevosClient*>(&ra::services::ServiceLocator::GetMutable<ra::services::RcheevosClient>());
-        std::string sRequestParams = pRequest->post_data;
-
-        for (auto& pResponse : pClient->m_vResponses)
-        {
-            if (pResponse.sRequestParams == sRequestParams)
-            {
-                pResponse.bSeen = true;
-
-                rc_api_server_response_t pServerResponse;
-                memset(&pServerResponse, 0, sizeof(pServerResponse));
-                pServerResponse.http_status_code = pResponse.nHttpStatus;
-                pServerResponse.body = pResponse.sResponseBody.c_str();
-                pServerResponse.body_length = pResponse.sResponseBody.length();
-
-                fCallback(&pServerResponse, pCallbackData);
-                return;
-            }
-        }
-
-        auto& pResponse = pClient->m_vResponses.emplace_back();
-        pResponse.sRequestParams = sRequestParams;
-        pResponse.fAsyncCallback = fCallback;
-        pResponse.pAsyncCallbackData = pCallbackData;
-    }
+                               void* pCallbackData, rc_client_t*);
 
     ra::services::ServiceLocator::ServiceOverride<ra::services::RcheevosClient> m_Override;
 
@@ -102,9 +56,13 @@ private:
         bool bSeen = 0;
         rc_client_server_callback_t fAsyncCallback = nullptr;
         void* pAsyncCallbackData = nullptr;
+        std::function<void()> fBeforeResponse = nullptr;
     } MockApiResponse;
 
     std::vector<MockApiResponse> m_vResponses;
+
+    std::string m_sUsername;
+    std::string m_sApiToken;
 };
 
 } // namespace mocks
diff --git a/tests/mocks/MockServer.hh b/tests/mocks/MockServer.hh
index 8c61bea9..3731d857 100644
--- a/tests/mocks/MockServer.hh
+++ b/tests/mocks/MockServer.hh
@@ -68,11 +68,6 @@ public:
         return HandleRequest<ra::api::Ping>(request);
     }
 
-    FetchUserUnlocks::Response FetchUserUnlocks(const FetchUserUnlocks::Request& request) override
-    {
-        return HandleRequest<ra::api::FetchUserUnlocks>(request);
-    }
-
     AwardAchievement::Response AwardAchievement(const AwardAchievement::Request& request) override
     {
         return HandleRequest<ra::api::AwardAchievement>(request);
@@ -95,11 +90,6 @@ public:
         return HandleRequest<ra::api::ResolveHash>(request);
     }
 
-    FetchGameData::Response FetchGameData(const FetchGameData::Request& request) override
-    {
-        return HandleRequest<ra::api::FetchGameData>(request);
-    }
-
     FetchCodeNotes::Response FetchCodeNotes(const FetchCodeNotes::Request& request) override
     {
         return HandleRequest<ra::api::FetchCodeNotes>(request);
diff --git a/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp b/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp
index 2bd7f5c9..e8ee62ef 100644
--- a/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp
@@ -24,6 +24,7 @@
 #include "tests\mocks\MockEmulatorContext.hh"
 #include "tests\mocks\MockGameContext.hh"
 #include "tests\mocks\MockLocalStorage.hh"
+#include "tests\mocks\MockRcheevosClient.hh"
 #include "tests\mocks\MockServer.hh"
 #include "tests\mocks\MockThreadPool.hh"
 #include "tests\mocks\MockUserContext.hh"
@@ -50,10 +51,16 @@ private:
         ra::services::mocks::MockAchievementRuntime mockAchievementRuntime;
         ra::services::mocks::MockConfiguration mockConfiguration;
         ra::services::mocks::MockLocalStorage mockLocalStorage;
+        ra::services::mocks::MockRcheevosClient mockRcheevosClient;
         ra::services::mocks::MockThreadPool mockThreadPool;
         ra::ui::mocks::MockDesktop mockDesktop;
         ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;
 
+        IntegrationMenuViewModelHarness()
+        {
+            mockRcheevosClient.MockGame();
+        }
+
         void BuildMenu()
         {
             IntegrationMenuViewModel::BuildMenu(m_vmItems);
diff --git a/tests/ui/viewmodels/OverlayRecentGamesPageViewModel_Tests.cpp b/tests/ui/viewmodels/OverlayRecentGamesPageViewModel_Tests.cpp
index 3a94e798..b1ca15b2 100644
--- a/tests/ui/viewmodels/OverlayRecentGamesPageViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/OverlayRecentGamesPageViewModel_Tests.cpp
@@ -2,12 +2,15 @@
 
 #include "ui\viewmodels\OverlayRecentGamesPageViewModel.hh"
 
+#include "tests\mocks\MockConfiguration.hh"
+#include "tests\mocks\MockHttpRequester.hh"
 #include "tests\mocks\MockImageRepository.hh"
 #include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockOverlayManager.hh"
 #include "tests\mocks\MockServer.hh"
 #include "tests\mocks\MockSessionTracker.hh"
 #include "tests\mocks\MockThreadPool.hh"
+#include "tests\mocks\MockUserContext.hh"
 #include "tests\RA_UnitTestHelpers.h"
 
 using namespace Microsoft::VisualStudio::CppUnitTestFramework;
@@ -25,6 +28,8 @@ private:
     public:
         ra::api::mocks::MockServer mockServer;
         ra::data::context::mocks::MockSessionTracker mockSessions;
+        ra::data::context::mocks::MockUserContext mockUserContext;
+        ra::services::mocks::MockConfiguration mockConfiguration;
         ra::services::mocks::MockLocalStorage mockLocalStorage;
         ra::services::mocks::MockThreadPool mockTheadPool;
         ra::ui::mocks::MockImageRepository mockImageRepository;
@@ -79,15 +84,19 @@ public:
     {
         OverlayRecentGamesPageViewModelHarness gamesPage;
         gamesPage.mockSessions.MockSession(3U, 1234567890U, std::chrono::seconds(5000));
-        gamesPage.mockServer.HandleRequest<ra::api::FetchGameData>(
-            [](const ra::api::FetchGameData::Request& request, ra::api::FetchGameData::Response & response)
-        {
-            Assert::AreEqual(3U, request.GameId);
-
-            response.Result = ra::api::ApiResult::Success;
-            response.Title = L"Game Name";
-            response.ImageIcon = "BADGE";
-            return true;
+        gamesPage.mockUserContext.Initialize("Username", "APITOKEN");
+
+        ra::services::mocks::MockHttpRequester mockHttp([](const ra::services::Http::Request&) {
+            return ra::services::Http::Response(ra::services::Http::StatusCode::OK,
+                                                "{\"Success\":true,\"PatchData\":{"
+                                                    "\"ID\":99,"
+                                                    "\"Title\":\"Game Name\","
+                                                    "\"ConsoleID\":5,"
+                                                    "\"ImageIcon\":\"/Images/BADGE.png\","
+                                                    "\"Achievements\":[],"
+                                                    "\"Leaderboards\":[],"
+                                                    "\"RichPresencePatch\":\"\""
+                                                "}}");
         });
 
         gamesPage.Refresh();
@@ -136,7 +145,13 @@ public:
     {
         OverlayRecentGamesPageViewModelHarness gamesPage;
         gamesPage.mockSessions.MockSession(3U, 1234567890U, std::chrono::seconds(5000));
-        gamesPage.mockServer.ExpectUncalled<ra::api::FetchGameData>();
+
+        bool bHttpRequesterCalled = false;
+        ra::services::mocks::MockHttpRequester mockHttp([&bHttpRequesterCalled](const ra::services::Http::Request&) {
+            bHttpRequesterCalled = true;
+            return ra::services::Http::Response();
+        });
+
         gamesPage.mockLocalStorage.MockStoredData(ra::services::StorageItemType::GameData, L"3",
             "{\"ID\":99, \"Title\":\"Game Name\", \"ConsoleID\":5, \"ImageIcon\":\"/Images/BADGE.png\", \"Achievements\":[], \"Leaderboards\":[]}");